<?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>gee.hsu &#8211; wordpress_blog</title>
	<atom:link href="/wordpress_blog/author/gee-hsu/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>gee.hsu &#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>Vue3 複習10</title>
		<link>/wordpress_blog/reviewvue3_10/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Tue, 06 Aug 2024 06:24:43 +0000</pubDate>
				<category><![CDATA[六角學院]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=870</guid>

					<description><![CDATA[從頭開始 – Pinia 製作一個購物車 2023 新增章節 P [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">從頭開始 – Pinia 製作一個購物車 2023 新增章節</h2>



<h3 class="wp-block-heading">Pinia 相關資源</h3>



<ul class="wp-block-list">
<li><a href="https://pinia.vuejs.org/" target="_blank" rel="noreferrer noopener">Pinia 官方文件</a></li>



<li><a href="https://www.youtube.com/watch?v=_vFuDQ_6Xt8" target="_blank" rel="noreferrer noopener">陣列方法補充</a></li>



<li>課程 Pinia CDN 範例</li>
</ul>



<pre class="wp-block-code"><code>// 課程 Pinia CDN 範例
&lt;!-- VueDemi，使用 Pinia 必要的相依套件 --&gt;
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue-demi/0.13.11/index.iife.js"&gt;&lt;/script&gt;
&lt;script&gt;const I = VueDemi; const vueDemi = VueDemi;&lt;/script&gt;

&lt;!-- Pinia 網頁版，實戰中還是以 npm 為主，這是比較少見的使用方式 --&gt;
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pinia/2.0.22/pinia.iife.js"&gt;&lt;/script&gt;</code></pre>



<h3 class="wp-block-heading">01. pinia 簡介</h3>



<h4 class="wp-block-heading">元件資料獨立與傳遞</h4>



<h4 class="wp-block-heading">頁面元件結構</h4>



<h4 class="wp-block-heading">Pinia 好處</h4>



<ul class="wp-block-list">
<li>跨元件的狀態、方法管理</li>



<li>易於學習，許多觀念與 Vue.js 連貫</li>



<li>相對於其他狀態管理工具更容易上手</li>
</ul>



<h3 class="wp-block-heading">02. 專案簡介</h3>



<ul class="wp-block-list">
<li>完成版型製作</li>



<li>將產品資料渲染至畫面上</li>



<li>最終範例</li>
</ul>



<h3 class="wp-block-heading">03. 版型製作</h3>



<ul class="wp-block-list">
<li>新增 layout.html 檔案</li>



<li>開啟 Bootstrap 5 文件<br>Components > Navbar > Text<br>選擇單純的結構複製程式碼</li>



<li>修改 layout.html 檔案<br>品牌名稱、按鈕<br>Components > Buttons<br>選擇單純的結構複製程式碼</li>



<li>在購物車的地方加上數字<br>Components > Badge > Pill Badges，選擇 danger 的顏色<br>修改 Badge 的 class</li>



<li>完成購物車版型<br>Form > Select<br>Utilities > Vertical align</li>



<li>製作產品卡片結構<br>Components > Card</li>
</ul>



<pre class="wp-block-code"><code>// pinia/layout.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;title&gt;完整版型製作&lt;/title&gt;
    &lt;!-- Bootstarp 5 CSS CDN --&gt;
    &lt;link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    /&gt;
    &lt;style&gt;
      .table-image {
        height: 100px;
        width: 100px;
        object-fit: cover;
      }

      .card-img-top {
        height: 200px;
        object-fit: cover;
      }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="app"&gt;
      &lt;div class="container py-5"&gt;
        &lt;h2&gt;完成版型製作&lt;/h2&gt;
        &lt;nav class="navbar bg-body-tertiary"&gt;
          &lt;div class="container-fluid"&gt;
            &lt;span class="navbar-brand mb-0 h1"&gt;香香餅乾店&lt;/span&gt;
            &lt;button type="button" class="btn"&gt;購物車
              &lt;span class="badge rounded-pill bg-danger text-white"&gt;0&lt;/span&gt;
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/nav&gt;

        &lt;div class="bg-light my-4 p-4"&gt;
          &lt;div&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
          &lt;!-- v-else --&gt;
          &lt;table class="table align-middle"&gt;
          &lt;tbody&gt;
            &lt;tr&gt;
              &lt;td&gt;
                &lt;a href="#" class="text-dark"&gt;x&lt;/a&gt;
              &lt;/td&gt;
              &lt;td&gt;
                &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80" class="table-image" alt=""&gt;
              &lt;/td&gt;
              &lt;td&gt;好吃的餅乾&lt;/td&gt;
              &lt;td&gt;
                &lt;select name="" id="" class="form-select"&gt;
                  &lt;option value=""&gt;1&lt;/option&gt;
                &lt;/select&gt;
              &lt;/td&gt;
              &lt;td class="text-end"&gt;
                $900
              &lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
          &lt;tfoot&gt;
            &lt;td colspan="5" class="text-end"&gt;總金額 NT$ 900&lt;/td&gt;
          &lt;/tfoot&gt;
          &lt;/table&gt;
        &lt;/div&gt;

        &lt;div class="row row-cols-3 my-4"&gt;
          &lt;div class="col"&gt;
            &lt;div class="card"&gt;
              &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80"
              class="card-img-top" alt=""&gt;
              &lt;div class="card-body"&gt;
                &lt;h6 class="card-title"&gt;好吃的餅乾
                  &lt;span class="float-end"&gt;$900&lt;/span&gt;
                &lt;/h6&gt;
                &lt;a href="#" class="btn btn-outline-primary w-100"&gt;加入購物車&lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- Vue 3 CDN --&gt;
    &lt;script src="https://unpkg.com/vue@3/dist/vue.global.js"&gt;&lt;/script&gt;
    &lt;!-- Bootstrap 5 JS CDN --&gt;
    &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"&gt;&lt;/script&gt;
    &lt;script type="module"&gt;
      const app = Vue.createApp({
        // 資料 (函式)
        data() {
          return {};
        },
        // 生命週期 (函式)
        created() {
          console.log(this);
        },
        // 方法 (物件)
        methods: {},
      });

      app.mount("#app");
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h3 class="wp-block-heading">04. 轉換vue元件</h3>



<ul class="wp-block-list">
<li>修改 layout.html 檔案，增加 script 片段</li>



<li>新增 homeworkComponents 資料夾</li>



<li>製作 Navbar 元件，避免出錯可以先直接在目前的專案位置進行撰寫</li>



<li>新增 navbarComponent.js 檔案</li>



<li>修改 navbarComponent.js 檔案</li>



<li>修改 layout.html 檔案，匯入 NavbarComponent</li>



<li>新增 cartComponent.js 檔案</li>



<li>修改 cartComponent.js 檔案</li>



<li>修改 layout.html 檔案，匯入 CartComponent</li>



<li>新增 productsComponent.js 檔案</li>



<li>修改 prodcutsComponent.js 檔案</li>



<li>修改 layout.html 檔案，匯入 ProductComponent</li>
</ul>



<pre class="wp-block-code"><code>// pinia/layout.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;title&gt;完整版型製作&lt;/title&gt;
    &lt;!-- Bootstarp 5 CSS CDN --&gt;
    &lt;link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    /&gt;
    &lt;style&gt;
      .table-image {
        height: 100px;
        width: 100px;
        object-fit: cover;
      }

      .card-img-top {
        height: 200px;
        object-fit: cover;
      }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="app"&gt;
      &lt;div class="container py-5"&gt;
        &lt;h2&gt;完成版型製作&lt;/h2&gt;

        &lt;Navbar-Component&gt;&lt;/Navbar-Component&gt;

        &lt;Cart-Component&gt;&lt;/Cart-Component&gt;

        &lt;Product-Component&gt;&lt;/Product-Component&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- Vue 3 CDN --&gt;
    &lt;script src="https://unpkg.com/vue@3/dist/vue.global.js"&gt;&lt;/script&gt;
    &lt;!-- Bootstrap 5 JS CDN --&gt;
    &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"&gt;&lt;/script&gt;
    &lt;script type="module"&gt;
      const { createApp } = Vue;

      import NavbarComponent from './homeworkComponents/navbarComponent.js'
      import CartComponent from './homeworkComponents/cartComponent.js'
      import ProductComponent from './homeworkComponents/productsComponent.js'

      const app =createApp({
        components: {
          NavbarComponent,
          CartComponent,
          ProductComponent
        }
      }).mount('#app');

    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/navbarComponent.js

export default {
  template: `&lt;nav class="navbar bg-body-tertiary"&gt;
    &lt;div class="container-fluid"&gt;
      &lt;span class="navbar-brand mb-0 h1"&gt;香香餅乾店&lt;/span&gt;
      &lt;button type="button" class="btn"&gt;購物車
        &lt;span class="badge rounded-pill bg-danger text-white"&gt;0&lt;/span&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/nav&gt;`
}
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/cartComponent.js

export default {
  template: `&lt;div class="bg-light my-4 p-4"&gt;
    &lt;div&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
    &lt;!-- v-else --&gt;
    &lt;table class="table align-middle"&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;
          &lt;a href="#" class="text-dark"&gt;x&lt;/a&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80" class="table-image" alt=""&gt;
        &lt;/td&gt;
        &lt;td&gt;好吃的餅乾&lt;/td&gt;
        &lt;td&gt;
          &lt;select name="" id="" class="form-select"&gt;
            &lt;option value=""&gt;1&lt;/option&gt;
          &lt;/select&gt;
        &lt;/td&gt;
        &lt;td class="text-end"&gt;
          $900
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
      &lt;td colspan="5" class="text-end"&gt;總金額 NT$ 900&lt;/td&gt;
    &lt;/tfoot&gt;
    &lt;/table&gt;
  &lt;/div&gt;`
}
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/productsComponent.js

export default {
  template: `&lt;div class="row row-cols-3 my-4"&gt;
    &lt;div class="col"&gt;
      &lt;div class="card"&gt;
        &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80"
        class="card-img-top" alt=""&gt;
        &lt;div class="card-body"&gt;
          &lt;h6 class="card-title"&gt;好吃的餅乾
            &lt;span class="float-end"&gt;$900&lt;/span&gt;
          &lt;/h6&gt;
          &lt;a href="#" class="btn btn-outline-primary w-100"&gt;加入購物車&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;`
}
</code></pre>



<h3 class="wp-block-heading">05. 導入產品資料</h3>



<ul class="wp-block-list">
<li>從範例程式碼 productComponet.js 檔案複製產品資料</li>



<li>修改 productsComponent.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/productsComponent.js

export default {
  data() {
    return {
      products: &#91;
        {
          id: 1,
          title: '多色餅乾',
          imageUrl: 'https://images.unsplash.com/photo-1576717585968-8ea8166b89b8?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
          price: 80
        },
        {
          id: 2,
          title: '綠色馬卡龍',
          imageUrl: 'https://images.unsplash.com/photo-1623066463831-3f7f6762734d?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1135&amp;q=80',
          price: 120
        },
        {
          id: 3,
          title: '甜蜜左擁右抱',
          imageUrl: 'https://images.unsplash.com/photo-1558312657-b2dead03d494?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
          price: 200,
        },
        {
          id: 4,
          title: '巧克力心連心',
          imageUrl: 'https://images.unsplash.com/photo-1606913084603-3e7702b01627?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
          price: 160
        },
        {
          id: 5,
          title: '粉係馬卡龍',
          imageUrl: 'https://images.unsplash.com/photo-1612201142855-7873bc1661b4?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
          price: 120
        }
      ]
    }
  },
  template: `&lt;div class="row row-cols-3 my-4 g-4"&gt;
    &lt;div class="col" v-for="product in products" :key="product.id"&gt;
      &lt;div class="card"&gt;
        &lt;img :src="product.imageUrl"
        class="card-img-top" alt=""&gt;
        &lt;div class="card-body"&gt;
          &lt;h6 class="card-title"&gt;{{ product.title }}
            &lt;span class="float-end"&gt;$ {{ product.price }}&lt;/span&gt;
          &lt;/h6&gt;
          &lt;a href="#" class="btn btn-outline-primary w-100"&gt;加入購物車&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;`
}
</code></pre>



<h3 class="wp-block-heading">06. 建立 store</h3>



<ul class="wp-block-list">
<li>修改 layout.html 檔案，複製範例片段程式碼、載入套件程式碼，實戰中比較少用這種方式</li>



<li>使用 log 查詢 Pinia 是否有正確匯入</li>



<li>在 pinia 資料夾裡面建立 store 資料夾</li>



<li>在 store 資料夾裡面建立 productsStore.js 檔案</li>



<li>會使用到的 layout.html、productsComponent.js、productsStore.js 檔案</li>



<li>修改 layout.html 檔案，匯入 productStore</li>



<li>修改 productsStore.js 檔案</li>



<li>修改 productsComponent.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/layout.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;title&gt;完整版型製作&lt;/title&gt;
    &lt;!-- Bootstarp 5 CSS CDN --&gt;
    &lt;link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
    /&gt;
    &lt;style&gt;
      .table-image {
        height: 100px;
        width: 100px;
        object-fit: cover;
      }

      .card-img-top {
        height: 200px;
        object-fit: cover;
      }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="app"&gt;
      &lt;div class="container py-5"&gt;
        &lt;h2&gt;完成版型製作&lt;/h2&gt;

        &lt;Navbar-Component&gt;&lt;/Navbar-Component&gt;

        &lt;Cart-Component&gt;&lt;/Cart-Component&gt;

        &lt;Product-Component&gt;&lt;/Product-Component&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- Vue 3 CDN --&gt;
    &lt;script src="https://unpkg.com/vue@3/dist/vue.global.js"&gt;&lt;/script&gt;
    &lt;!-- Bootstrap 5 JS CDN --&gt;
    &lt;script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"&gt;&lt;/script&gt;
    &lt;!-- VueDemi，使用 Pinia 必要的相依套件 --&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue-demi/0.13.11/index.iife.js"&gt;&lt;/script&gt;
    &lt;script&gt;const I = VueDemi; const vueDemi = VueDemi;&lt;/script&gt;
    &lt;!-- Pinia 網頁版，實戰中還是以 npm 為主，這是比較少見的使用方式 --&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/pinia/2.0.22/pinia.iife.js"&gt;&lt;/script&gt;
    &lt;script type="module"&gt;
      const { createApp } = Vue;
      const { createPinia } = Pinia;

      import NavbarComponent from './homeworkComponents/navbarComponent.js'
      import CartComponent from './homeworkComponents/cartComponent.js'
      import ProductComponent from './homeworkComponents/productsComponent.js'

      const app =createApp({
        components: {
          NavbarComponent,
          CartComponent,
          ProductComponent
        }
      })

      const pinia = createPinia()
      app.use(pinia)      
      app.mount('#app')

    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// pinia/store/productsStore.js
const { defineStore } = Pinia;

export default defineStore('productsStore', {
  // data, methods, computed
  // state, action, getters
  state: () =&gt; ({
    products: &#91;
      {
        id: 1,
        title: '多色餅乾',
        imageUrl: 'https://images.unsplash.com/photo-1576717585968-8ea8166b89b8?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 80
      },
      {
        id: 2,
        title: '綠色馬卡龍',
        imageUrl: 'https://images.unsplash.com/photo-1623066463831-3f7f6762734d?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1135&amp;q=80',
        price: 120
      },
      {
        id: 3,
        title: '甜蜜左擁右抱',
        imageUrl: 'https://images.unsplash.com/photo-1558312657-b2dead03d494?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 200,
      },
      {
        id: 4,
        title: '巧克力心連心',
        imageUrl: 'https://images.unsplash.com/photo-1606913084603-3e7702b01627?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 160
      },
      {
        id: 5,
        title: '粉係馬卡龍',
        imageUrl: 'https://images.unsplash.com/photo-1612201142855-7873bc1661b4?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 120
      }
    ]
  }),
  getters: {
    sortProducts: ({ products }) =&gt; {
      return products 
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/productsComponent.js
import productsStore from "../store/productsStore.js"
const { mapState } = Pinia

export default {
  data() {
    return {
      
    }
  },
  template: `&lt;div class="row row-cols-3 my-4 g-4"&gt;
    &lt;div class="col" v-for="product in sortProducts" :key="product.id"&gt;
      &lt;div class="card"&gt;
        &lt;img :src="product.imageUrl"
        class="card-img-top" alt=""&gt;
        &lt;div class="card-body"&gt;
          &lt;h6 class="card-title"&gt;{{ product.title }}
            &lt;span class="float-end"&gt;$ {{ product.price }}&lt;/span&gt;
          &lt;/h6&gt;
          &lt;a href="#" class="btn btn-outline-primary w-100"&gt;加入購物車&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(productsStore, &#91;'sortProducts'])
  }
}
</code></pre>



<h3 class="wp-block-heading">07. 建立購物車 Store</h3>



<ul class="wp-block-list">
<li>修改 productsStore.js 檔案</li>



<li>加入購物車的行為<br>在 store 資料夾裡面建立 cartStore.js 檔案，專門管理購物車所有方法</li>



<li>修改 cartStore.js 檔案</li>



<li>修改 productsComponent 檔案，匯入 cartStore</li>



<li>修改 cartStore.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/store/productsStore.js
const { defineStore } = Pinia;

export default defineStore('productsStore', {
  // data, methods, computed
  // state, action, getters
  state: () =&gt; ({
    products: &#91;
      {
        id: 1,
        title: '多色餅乾',
        imageUrl: 'https://images.unsplash.com/photo-1576717585968-8ea8166b89b8?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 80
      },
      {
        id: 2,
        title: '綠色馬卡龍',
        imageUrl: 'https://images.unsplash.com/photo-1623066463831-3f7f6762734d?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1135&amp;q=80',
        price: 120
      },
      {
        id: 3,
        title: '甜蜜左擁右抱',
        imageUrl: 'https://images.unsplash.com/photo-1558312657-b2dead03d494?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 200,
      },
      {
        id: 4,
        title: '巧克力心連心',
        imageUrl: 'https://images.unsplash.com/photo-1606913084603-3e7702b01627?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 160
      },
      {
        id: 5,
        title: '粉係馬卡龍',
        imageUrl: 'https://images.unsplash.com/photo-1612201142855-7873bc1661b4?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1170&amp;q=80',
        price: 120
      }
    ]
  }),
  getters: {
    sortProducts: ({ products }) =&gt; {
      return products.sort((a, b) =&gt; a.price - b.price)
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      console.log(productId, qty);
      this.cart.push({
        id: new Date().getTime(),
        productId,
        qty
      })
      console.log(this.cart);
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/productsComponent.js
import productsStore from "../store/productsStore.js"
import cartStore from '../store/cartStore.js'
const { mapState, mapActions } = Pinia

export default {
  data() {
    return {
      
    }
  },
  template: `&lt;div class="row row-cols-3 my-4 g-4"&gt;
    &lt;div class="col" v-for="product in sortProducts" :key="product.id"&gt;
      &lt;div class="card"&gt;
        &lt;img :src="product.imageUrl"
        class="card-img-top" alt=""&gt;
        &lt;div class="card-body"&gt;
          &lt;h6 class="card-title"&gt;{{ product.title }}
            &lt;span class="float-end"&gt;$ {{ product.price }}&lt;/span&gt;
          &lt;/h6&gt;
          &lt;a href="#" class="btn btn-outline-primary w-100" @click.prevent="addToCart(product.id)"&gt;加入購物車&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(productsStore, &#91;'sortProducts'])
  },
  methods: {
    ...mapActions(cartStore, &#91;'addToCart'])
  },
}
</code></pre>



<h3 class="wp-block-heading">08. 購物車資訊 Store</h3>



<ul class="wp-block-list">
<li>使用簡報解釋頁面元件結構</li>



<li>修改 cartStore.js 檔案</li>



<li>修改 cartComponent.js 檔案，匯入 cartStore、使用 mapState</li>



<li>修改 cartStore.js 檔案，調整 cartList</li>
</ul>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;
import productsStore from './productsStore.js';

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      console.log(productId, qty);
      this.cart.push({
        id: new Date().getTime(),
        productId,
        qty
      })
      // console.log(this.cart);
    }
  },
  getters: {
    cartList: ({ cart }) =&gt; {
      // 1. 購物車的品項資訊，需要整合產品資訊
      // 2. 必須計算小計的金額
      // 3. 必須提供總金額
      const { products } = productsStore();
      // console.log(products);
      // console.log(cart);
      const carts = cart.map((item) =&gt; {
        // console.log(item);
        // 單一產品取出
        const product = products.find((product) =&gt; product.id === item.productId);
        // console.log('相同 id 的產品', product);
        return {
          ...item,
          product,
          subtotal: product.price * item.qty
        }
      })
      // console.log(carts);
      const total = carts.reduce((a, b) =&gt; a + b.subtotal ,0);
      // console.log(total);

      return {
        carts, // 列表
        total
      }
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/cartComponent.js
import cartStore from '../store/cartStore.js'
const { mapState } = Pinia;

export default {
  template: `&lt;div class="bg-light my-4 p-4"&gt;
    &lt;div&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
    &lt;!-- v-else --&gt;
    &lt;table class="table align-middle"&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in cartList.carts"&gt;
        &lt;td&gt;
          &lt;a href="#" class="text-dark"&gt;x&lt;/a&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;img src="https://images.unsplash.com/photo-1597733153203-a54d0fbc47de?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=1090&amp;q=80" class="table-image" alt=""&gt;
        &lt;/td&gt;
        &lt;td&gt;好吃的餅乾&lt;/td&gt;
        &lt;td&gt;
          &lt;select name="" id="" class="form-select"&gt;
            &lt;option value=""&gt;1&lt;/option&gt;
          &lt;/select&gt;
        &lt;/td&gt;
        &lt;td class="text-end"&gt;
          $900
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
      &lt;td colspan="5" class="text-end"&gt;總金額 NT$ {{cartList.total}}&lt;/td&gt;
    &lt;/tfoot&gt;
    &lt;/table&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(cartStore, &#91;'cartList'])
  }
}
</code></pre>



<h3 class="wp-block-heading">09. 呈現購物車列表並刪除品項</h3>



<ul class="wp-block-list">
<li>修改 cartComponent.js 檔案</li>



<li>修改 cartStore.js 檔案，撰寫刪除的方法</li>
</ul>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/cartComponent.js
import cartStore from '../store/cartStore.js'
const { mapState, mapActions } = Pinia;

export default {
  template: `&lt;div class="bg-light my-4 p-4"&gt;
    &lt;div v-if="!cartList.carts.length"&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
    &lt;table v-else class="table align-middle"&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in cartList.carts" :key="item.id"&gt;
        &lt;td width="100"&gt;
          &lt;a href="#" class="text-dark"
          @click.prevent="removeCartItem(item.id)"&gt;x&lt;/a&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;img :src="item.product.imageUrl" class="table-image" alt=""&gt;
        &lt;/td&gt;
        &lt;td&gt;{{ item.product.title }}&lt;/td&gt;
        &lt;td&gt;
          &lt;select name="" id="" class="form-select"&gt;
            &lt;option value=""&gt;1&lt;/option&gt;
          &lt;/select&gt;
        &lt;/td&gt;
        &lt;td class="text-end"&gt;
          $ {{ item.subtotal }}
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
      &lt;td colspan="5" class="text-end"&gt;總金額 NT$ {{cartList.total}}&lt;/td&gt;
    &lt;/tfoot&gt;
    &lt;/table&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(cartStore, &#91;'cartList'])
  },
  methods: {
    ...mapActions(cartStore, &#91;'removeCartItem'])
  }
}
</code></pre>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;
import productsStore from './productsStore.js';

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      console.log(productId, qty);
      this.cart.push({
        id: new Date().getTime(),
        productId,
        qty
      })
      // console.log(this.cart);
    },
    removeCartItem(id) {
      const index = this.cart.findIndex((item) =&gt; item.id === id);
      this.cart.splice(index, 1);
    }
  },
  getters: {
    cartList: ({ cart }) =&gt; {
      // 1. 購物車的品項資訊，需要整合產品資訊
      // 2. 必須計算小計的金額
      // 3. 必須提供總金額
      const { products } = productsStore();
      // console.log(products);
      // console.log(cart);
      const carts = cart.map((item) =&gt; {
        // console.log(item);
        // 單一產品取出
        const product = products.find((product) =&gt; product.id === item.productId);
        // console.log('相同 id 的產品', product);
        return {
          ...item,
          product,
          subtotal: product.price * item.qty
        }
      })
      // console.log(carts);
      const total = carts.reduce((a, b) =&gt; a + b.subtotal ,0);
      // console.log(total);

      return {
        carts, // 列表
        total
      }
    }
  }
})
</code></pre>



<h3 class="wp-block-heading">10. 新增品項加總至原品項</h3>



<ul class="wp-block-list">
<li>修改 cartStore.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;
import productsStore from './productsStore.js';

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      // 取得已經有加入購物車的項目
      // 進行判斷，如果購物車有該項目則 +1，如果沒有則是新增一個購物車項目
      const currentCart = this.cart.find((item) =&gt; item.productId === productId)

      if (currentCart) {
        currentCart.qty += qty;
      } else {
        this.cart.push({
          id: new Date().getTime(),
          productId,
          qty
        });
      }
      console.log(this.cart);
      // console.log(this.cart);
    },
    removeCartItem(id) {
      const index = this.cart.findIndex((item) =&gt; item.id === id);
      this.cart.splice(index, 1);
    }
  },
  getters: {
    cartList: ({ cart }) =&gt; {
      // 1. 購物車的品項資訊，需要整合產品資訊
      // 2. 必須計算小計的金額
      // 3. 必須提供總金額
      const { products } = productsStore();
      // console.log(products);
      // console.log(cart);
      const carts = cart.map((item) =&gt; {
        // console.log(item);
        // 單一產品取出
        const product = products.find((product) =&gt; product.id === item.productId);
        // console.log('相同 id 的產品', product);
        return {
          ...item,
          product,
          subtotal: product.price * item.qty
        }
      })
      // console.log(carts);
      const total = carts.reduce((a, b) =&gt; a + b.subtotal ,0);
      // console.log(total);

      return {
        carts, // 列表
        total
      }
    }
  }
})
</code></pre>



<h3 class="wp-block-heading">11. 設定數量</h3>



<ul class="wp-block-list">
<li>修改 cartComponent.js 檔案</li>



<li>修改 cartStore.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/homeworkComponent/cartComponent.js
import cartStore from '../store/cartStore.js'
const { mapState, mapActions } = Pinia;

export default {
  template: `&lt;div class="bg-light my-4 p-4"&gt;
    &lt;div v-if="!cartList.carts.length"&gt;購物車沒有任何品項&lt;/div&gt; &lt;!-- v-if --&gt;
    &lt;table v-else class="table align-middle"&gt;
    &lt;tbody&gt;
      &lt;tr v-for="item in cartList.carts" :key="item.id"&gt;
        &lt;td width="100"&gt;
          &lt;a href="#" class="text-dark"
          @click.prevent="removeCartItem(item.id)"&gt;x&lt;/a&gt;
        &lt;/td&gt;
        &lt;td&gt;
          &lt;img :src="item.product.imageUrl" class="table-image" alt=""&gt;
        &lt;/td&gt;
        &lt;td&gt;{{ item.product.title }}&lt;/td&gt;
        &lt;td&gt;
          &lt;select name="" id="" class="form-select" :value="item.qty"
          @change="(evt) =&gt; setCartQty(item.id, evt)"&gt;
            &lt;option :value="i" v-for="i in 20" :key="i"&gt;{{ i }}&lt;/option&gt;
          &lt;/select&gt;
        &lt;/td&gt;
        &lt;td class="text-end"&gt;
          $ {{ item.subtotal }}
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
    &lt;tfoot&gt;
      &lt;td colspan="5" class="text-end"&gt;總金額 NT$ {{cartList.total}}&lt;/td&gt;
    &lt;/tfoot&gt;
    &lt;/table&gt;
  &lt;/div&gt;`,
  computed: {
    ...mapState(cartStore, &#91;'cartList'])
  },
  methods: {
    ...mapActions(cartStore, &#91;'removeCartItem', 'setCartQty'])
  }
}
</code></pre>



<pre class="wp-block-code"><code>// pinia/store/cartStore.js
const { defineStore } = Pinia;
import productsStore from './productsStore.js';

export default defineStore('cart', {
  // methods
  // actions
  state: () =&gt; ({
    cart: &#91;]
  }),
  actions: {
    addToCart(productId, qty = 1) {
      // 取得已經有加入購物車的項目
      // 進行判斷，如果購物車有該項目則 +1，如果沒有則是新增一個購物車項目
      const currentCart = this.cart.find((item) =&gt; item.productId === productId)

      if (currentCart) {
        currentCart.qty += qty;
      } else {
        this.cart.push({
          id: new Date().getTime(),
          productId,
          qty
        });
      }
      console.log(this.cart);
      // console.log(this.cart);
    },
    setCartQty(id, event) {
      // console.log(id, event);
      // console.log(event.target.value, typeof event.target.value);
      const currentCart = this.cart.find((item) =&gt; item.id === id);
      // console.log(currentCart);
      currentCart.qty = event.target.value * 1;
    },
    removeCartItem(id) {
      const index = this.cart.findIndex((item) =&gt; item.id === id);
      this.cart.splice(index, 1);
    }
  },
  getters: {
    cartList: ({ cart }) =&gt; {
      // 1. 購物車的品項資訊，需要整合產品資訊
      // 2. 必須計算小計的金額
      // 3. 必須提供總金額
      const { products } = productsStore();
      // console.log(products);
      // console.log(cart);
      const carts = cart.map((item) =&gt; {
        // console.log(item);
        // 單一產品取出
        const product = products.find((product) =&gt; product.id === item.productId);
        // console.log('相同 id 的產品', product);
        return {
          ...item,
          product,
          subtotal: product.price * item.qty
        }
      })
      // console.log(carts);
      const total = carts.reduce((a, b) =&gt; a + b.subtotal ,0);
      // console.log(total);

      return {
        carts, // 列表
        total
      }
    }
  }
})
</code></pre>



<h3 class="wp-block-heading">12. Navbar 數量呈現</h3>



<ul class="wp-block-list">
<li>修改 navbarComponent.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pinia/homeworkComponents/navbarComponent.js
const { mapState } = Pinia;
import cartStore from "../store/cartStore.js";

export default {
  template: `&lt;nav class="navbar bg-body-tertiary"&gt;
    &lt;div class="container-fluid"&gt;
      &lt;span class="navbar-brand mb-0 h1"&gt;香香餅乾店&lt;/span&gt;
      &lt;button type="button" class="btn"&gt;購物車
        &lt;span class="badge rounded-pill bg-danger text-white"&gt;{{ cart.length }}&lt;/span&gt;
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/nav&gt;`,
  computed: {
    ...mapState(cartStore, &#91;'cart'])
  }
}</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>MERN Authentication Tutorial</title>
		<link>/wordpress_blog/mern-authentication-tutorial/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Mon, 08 Jul 2024 09:29:35 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=865</guid>

					<description><![CDATA[Learning From Youtube Channel: N [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Learning From Youtube Channel: Net Ninja<br>Video:&nbsp;<a href="https://www.youtube.com/watch?v=WsRBmwNkv3Q&amp;list=PL4cUxeGkcC9g8OhpOZxNdhXggFz2lOuCT&amp;index=1" target="_blank" rel="noreferrer noopener">MERN Authentication Tutorial</a><br>Thank you.</p>



<h2 class="wp-block-heading">#1 – Intro &amp; Starter Project</h2>



<p>Learn how to implement authentication (using JSON web tokens), within the MERN stack, in this tutorial series.</p>



<ul class="wp-block-list">
<li>下載初始專案、講解專案檔案結構、內容</li>



<li>移動到 backend 資料夾 – cd backend</li>



<li>安裝 npm 套件 – npm install</li>



<li>移動到 frontend 資料夾 – cd frontend</li>



<li>安裝 npm 套件 – npm install</li>



<li>執行後端 backend – npm run dev</li>



<li>執行前端 frontend – npm start</li>
</ul>



<h2 class="wp-block-heading">#2 – User Routes, Controller &amp; Model</h2>



<p>In this MERN auth tutorial, you’ll make a user controller &amp; model, and set up some routes for authentication.</p>



<ul class="wp-block-list">
<li>在 routes 資料夾裡面建立 user.js 檔案</li>



<li>修改 user.js 檔案</li>



<li>在 controllers 資料夾裡面建立 userController.js 檔案</li>



<li>修改 userController.js 檔案</li>



<li>修改 user.js 檔案，controller functions</li>



<li>修改 server.js 檔案</li>



<li>在 models 資料夾裡面建立 userModel.js 檔案</li>



<li>修改 useModel.js 檔案</li>



<li>修改 userController.js 檔案，User</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/login，POST 方法<br>Body > raw > JSON</li>



<li>使用 API 測試工具<br>http://localhost:4000/api/user/signup，POST 方法<br>Body > raw > JSON</li>
</ul>



<pre class="wp-block-code"><code>// backend/routes/user.js
const express = require('express')

// controller functions
const { signupUser, loginUser } = require('../controllers/userController')

const router = express.Router()

// login route
router.post('/login', loginUser)

// signup route
router.post('/signup', signupUser)

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



<pre class="wp-block-code"><code>// backend/controllers/userController.js
const User = require('../models/userModel')

// login user
const loginUser = async (req, res) =&gt; {
  res.json({mssg: 'login user'})
}

// signup user
const signupUser = async (req, res) =&gt; {
  res.json({mssg: 'signup user'})
}

module.exports = { signupUser, loginUser }
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
require('dotenv').config()

const express = require('express')
const mongoose = require('mongoose')
const workoutRoutes = require('./routes/workouts')
const userRoutes = require('./routes/user')

// express app
const app = express()

// middleware
app.use(express.json())

app.use((req, res, next) =&gt; {
  console.log(req.path, req.method)
  next()
})

// routes
app.use('/api/workouts', workoutRoutes)
app.use('/api/user', userRoutes)

// connect to db
mongoose.connect(process.env.MONGO_URI)
  .then(() =&gt; {
    // listen for requests
    app.listen(process.env.PORT, () =&gt; {
      console.log('connected to db &amp; listening on port', process.env.PORT)
    })
  })
  .catch((error) =&gt; {
    console.log(error)
  })
</code></pre>



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

const Schema = mongoose.Schema

const userSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true,
  }
})

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



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON - login
{
  "email": "yoshi@netninja.dev",
  "password": "abc123"
}
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON - signup
{
  "email": "yoshi@netninja.dev",
  "password": "abc123"
}
</code></pre>



<h2 class="wp-block-heading">#3 – Signing Up &amp; Hashing Passwords</h2>



<ul class="wp-block-list">
<li>修改 userModel.js 檔案，static signup method</li>



<li>安裝 bcrypt 套件 – npm install bcrypt<br>修改 userModel.js 檔案</li>



<li>修改 userController.js 檔案</li>



<li>修改 userModels.js 檔案，function</li>



<li>重新執行後端 server – nodemon server</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/signup，POST 方法<br>Body > raw > JSON</li>
</ul>



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

const Schema = mongoose.Schema

const userSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true,
  }
})

// static signup method
userSchema.statics.signup = async function (email, password) {

  const exists = await this.findOne({ email })

  if (exists) {
    throw Error('Email already in use')
  }

  const salt = await bcrypt.genSalt(10)
  const hash = await bcrypt.hash(password, salt)

  const user = await this.create({ email, password: hash })

  return user
}

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



<pre class="wp-block-code"><code>// backend/controllers/userController.js
const User = require('../models/userModel')

// login user
const loginUser = async (req, res) =&gt; {
  res.json({mssg: 'login user'})
}

// signup user
const signupUser = async (req, res) =&gt; {
  const { email, password } = req.body

  try {
    const user = await User.signup(email, password)

    res.status(200).json({ email, user })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

module.exports = { signupUser, loginUser }
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "email": "yoshi@netninja.dev",
  "password": "abc123"
}
</code></pre>



<h2 class="wp-block-heading">#4 – Email &amp; Password Validation</h2>



<ul class="wp-block-list">
<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/signup，POST 方法<br>使用錯誤的 email 格式測試，沒有完整格式、空值</li>



<li>安裝 validator 套件 – npm install validator</li>



<li>修改 userModel.js 檔案，validator</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/signup，POST 方法<br>使用錯誤的 email 格式測試，沒有完整格式、空值</li>
</ul>



<pre class="wp-block-code"><code>// backend/models/userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcrypt')
const validator = require('validator')

const Schema = mongoose.Schema

const userSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true,
  }
})

// static signup method
userSchema.statics.signup = async function (email, password) {

  // validation
    if (!email || !password) {
      throw Error('All fields must be filled')
    }
    if (!validator.isEmail(email)) {
      throw Error('Email not valid')
    }
    if (!validator.isStrongPassword(password)) {
      throw Error('Password not strong enough')
    }

  const exists = await this.findOne({ email })

  if (exists) {
    throw Error('Email already in use')
  }

  const salt = await bcrypt.genSalt(10)
  const hash = await bcrypt.hash(password, salt)

  const user = await this.create({ email, password: hash })

  return user
}

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



<h2 class="wp-block-heading">#5 – JSON Web Tokens (theory)</h2>



<p>In this MERN Authentication tutorial you’ll learn about JSON Web Tokens (JWT’s) and how they work under the hood when it comes to authentication.</p>



<ul class="wp-block-list">
<li>講解 JSON Web Tokens</li>



<li>介紹 JWT 的 Encoded、Decoded</li>



<li>講解 Header、Payload、Signature</li>
</ul>



<h3 class="wp-block-heading">JSON Web Tokens</h3>



<p>Header<br>Contains the algorithm used for the JWT</p>



<p>Payload<br>Contains non-sensitive user data (e.g. a user id)</p>



<p>Signature<br>Used to verify the token by the server</p>



<h2 class="wp-block-heading">#6 – Signing Tokens</h2>



<p>In this MERN Authentication tutorial, we’ll see how to sign tokens and send them back to the client.</p>



<ul class="wp-block-list">
<li>安裝 jsonwebtoken 套件 – npm install jsonwebtoken</li>



<li>修改 userController.js 檔案</li>



<li>修改 .env 檔案，避免敏感、重要的資料上傳到 GitHub</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/user/signup，POST 方法<br>Body > raw > JSON</li>
</ul>



<pre class="wp-block-code"><code>// backend/controllers/userController.js
const User = require('../models/userModel')
const jwt = require('jsonwebtoken')

const createToken = (_id) =&gt; {
  return jwt.sign({_id}, process.env.SECRET, { expiresIn: '3d' })
}

// login user
const loginUser = async (req, res) =&gt; {
  res.json({mssg: 'login user'})
}

// signup user
const signupUser = async (req, res) =&gt; {
  const { email, password } = req.body

  try {
    const user = await User.signup(email, password)

    // create a token
    const token = createToken(user._id)

    res.status(200).json({ email, token })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

module.exports = { signupUser, loginUser }
</code></pre>



<pre class="wp-block-code"><code>// backend/.env
PORT=4000
MONGO_URI=mongodb+srv://mario:test1234@mernapp.bldt4v0.mongodb.net/?retryWrites=true&amp;w=majority&amp;appName=MERNapp
SECRET =learnfromnetninjathankyou
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "email": "luigi@netninja.dev",
  "password": "ABCabc123!"
}
</code></pre>



<h2 class="wp-block-heading">#7 – Logging Users In</h2>



<ul class="wp-block-list">
<li>修改 userModel.js 檔案，static login method</li>



<li>修改 userController.js 檔案，login user</li>



<li>使用 API 測試工具 Postman，測試各種資料情形<br>http://localhost:4000/api/user/login，POST 方法<br>Body > raw > JSON</li>
</ul>



<pre class="wp-block-code"><code>// backend/models/userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcrypt')
const validator = require('validator')

const Schema = mongoose.Schema

const userSchema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true,
  }
})

// static signup method
userSchema.statics.signup = async function (email, password) {

  // validation
    if (!email || !password) {
      throw Error('All fields must be filled')
    }
    if (!validator.isEmail(email)) {
      throw Error('Email not valid')
    }
    if (!validator.isStrongPassword(password)) {
      throw Error('Password not strong enough')
    }

  const exists = await this.findOne({ email })

  if (exists) {
    throw Error('Email already in use')
  }

  const salt = await bcrypt.genSalt(10)
  const hash = await bcrypt.hash(password, salt)

  const user = await this.create({ email, password: hash })

  return user
}

// static login method
userSchema.statics.login = async function (email, password) {

  if (!email || !password) {
    throw Error('All fields must be filled')
  }

  const user = await this.findOne({ email })

  if (!user) {
    throw Error('Incorrect email')
  }

  const match = await bcrypt.compare(password, user.password)

  if (!match) {
    throw Error('Incorrect password')
  }

  return user
}

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



<pre class="wp-block-code"><code>// backend/controllers/userController.js
const User = require('../models/userModel')
const jwt = require('jsonwebtoken')

const createToken = (_id) =&gt; {
  return jwt.sign({_id}, process.env.SECRET, { expiresIn: '3d' })
}

// login user
const loginUser = async (req, res) =&gt; {
  const { email, password } = req.body

  try {
    const user = await User.login(email, password)

    // create a token
    const token = createToken(user._id)

    res.status(200).json({ email, token })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// signup user
const signupUser = async (req, res) =&gt; {
  const { email, password } = req.body

  try {
    const user = await User.signup(email, password)

    // create a token
    const token = createToken(user._id)

    res.status(200).json({ email, token })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

module.exports = { signupUser, loginUser }
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "email": "luigi@netninja.dev",
  "password": "ABCabc123!"
}
</code></pre>



<h2 class="wp-block-heading">#8 – React Auth Context</h2>



<ul class="wp-block-list">
<li>在 context 資料夾裡面建立 AuthContext.js 檔案</li>



<li>修改 AuthContext.js 檔案</li>



<li>修改 index.js 檔案，匯入 AuthContextProvider</li>



<li>在 hook 資料夾裡面建立 useAuthContext.js 檔案</li>



<li>修改 useAuthContext.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// backend/src/context/AuthContext.js
import { createContext, useReducer } from 'react'

export const AuthContext = createContext()

export const authReducer = (state, action) =&gt; {
  switch (action.type) {
    case 'LOGIN':
      return { user: action.payload }
    case 'LOGOUT':
      return { user: null}
    default:
      return state
  }
}

export const AuthContextProvider = ({ children }) =&gt; {
  const &#91;state, dispatch] = useReducer(authReducer, {
    user: null
  })

  console.log('AuthContext State: ', state)

  return (
    &lt;AuthContext.Provider value={{...state, dispatch}}&gt;
      { children }
    &lt;/AuthContext.Provider&gt;
  )
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { WorkoutsContextProvider } from './context/WorkoutContext';
import { AuthContextProvider } from './context/AuthContext';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  &lt;React.StrictMode&gt;
    &lt;AuthContextProvider&gt;
      &lt;WorkoutsContextProvider&gt;
        &lt;App /&gt;
      &lt;/WorkoutsContextProvider&gt;
    &lt;/AuthContextProvider&gt;
  &lt;/React.StrictMode&gt;
);
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/hooks/useAuthContext.js
import { AuthContext } from "../context/AuthContext";
import { useContext } from 'react'

export const useAuthContext = () =&gt; {
  const context = useContext(AuthContext)

  if (!context) {
    throw Error('useAuthContext must be used inside an AuthContextProvider')
  }

  return context
}
</code></pre>



<h2 class="wp-block-heading">#9 – Login &amp; Signup Forms</h2>



<p>In this MERN auth tutorial, we’ll flesh out the login and signup forms.</p>



<ul class="wp-block-list">
<li>在 pages 資料夾裡面建立 Signup.js 檔案</li>



<li>修改 Signup.js 檔案</li>



<li>在 pages 資料夾裡面建立 Login.js 檔案</li>



<li>修改 Login.js 檔案，從 Signup.js 複製修改</li>



<li>修改 App.js 檔案</li>



<li>修改 Navbar.js 檔案</li>



<li>修改 index.css 檔案</li>



<li>測試網頁表單 Sign up 關於 console 查詢顯示</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/Signup.js
import { useState } from 'react'

const Signup = () =&gt; {
  const &#91;email, setEmail] = useState('')
  const &#91;password, setPassword] = useState('')

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    console.log(email, password)
  }

  return (
    &lt;form className='signup' onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Sign up&lt;/h3&gt;

      &lt;label&gt;Email:&lt;/label&gt;
      &lt;input
      type="email"
      onChange={(e) =&gt; setEmail(e.target.value)}
      value={email}
      /&gt;
      &lt;label&gt;Password:&lt;/label&gt;
      &lt;input
      type="password"
      onChange={(e) =&gt; setPassword(e.target.value)}
      value={password}
      /&gt;
      
      &lt;button&gt;Sign up&lt;/button&gt;
    &lt;/form&gt;
  )
}

export default Signup
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Login.js
import { useState } from 'react'

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

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    console.log(email, password)
  }

  return (
    &lt;form className='login' onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Log in&lt;/h3&gt;

      &lt;label&gt;Email:&lt;/label&gt;
      &lt;input
      type="email"
      onChange={(e) =&gt; setEmail(e.target.value)}
      value={email}
      /&gt;
      &lt;label&gt;Password:&lt;/label&gt;
      &lt;input
      type="password"
      onChange={(e) =&gt; setPassword(e.target.value)}
      value={password}
      /&gt;
      
      &lt;button&gt;Log in&lt;/button&gt;
    &lt;/form&gt;
  )
}

export default Login
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.js
import { BrowserRouter , Routes, Route } from 'react-router-dom'

// pages &amp; components
import Home from './pages/Home'
import Login from './pages/Login';
import Signup from './pages/Signup';
import Navbar from './components/Navbar'

function App() {
  return (
    &lt;div className="App"&gt;
        &lt;BrowserRouter&gt;
          &lt;Navbar /&gt;
          &lt;div className="pages"&gt;
            &lt;Routes&gt;
              &lt;Route
                path="/"
                element={&lt;Home /&gt;} 
              /&gt;
              &lt;Route
                path="/login"
                element={&lt;Login /&gt;} 
              /&gt;
              &lt;Route
                path="/signup"
                element={&lt;Signup /&gt;} 
              /&gt;
            &lt;/Routes&gt;
          &lt;/div&gt;
        &lt;/BrowserRouter&gt;
    &lt;/div&gt;
  );
}

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



<pre class="wp-block-code"><code>// frontend/src/components/Navbar.js
import { Link } from "react-router-dom"

const Navbar = () =&gt; {

  return (
    &lt;header&gt;
      &lt;div className="container"&gt;
        &lt;Link to="/"&gt;
          &lt;h1&gt;Workout Buddy&lt;/h1&gt;
        &lt;/Link&gt;
        &lt;nav&gt;
          &lt;div&gt;
            &lt;Link to="/login"&gt;Login&lt;/Link&gt;
            &lt;Link to="/signup"&gt;Signup&lt;/Link&gt;
          &lt;/div&gt;
        &lt;/nav&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}

input.error {
  border: 1px solid var(--error);
}

/* navbar */
nav {
  display: flex;
  align-items: center;
}

nav a {
  margin-left: 10px;
}

/* auth forms */
form.signup, form.login {
  max-width: 400px;
  margin: 40px auto;
  padding: 20px;
  background: #fff;
  border-radius: 4px;
}
</code></pre>



<h2 class="wp-block-heading">#10 – Making a useSignup Hook</h2>



<p>In this MERN auth lesson, you’ll create a custom, reusable React hook that we can use to sign new users up to the application.</p>



<ul class="wp-block-list">
<li>在 hooks 資料夾裡面建立 useSignup.js 檔案</li>



<li>修改 useSignup.js 檔案</li>



<li>修改 Signup.js 檔案，useSignup</li>



<li>測試 Sign up 表單能否正常運作</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/hooks/useSignup.js
import { useState } from 'react'
import { useAuthContext } from './useAuthContext'

export const useSignup = () =&gt; {
  const &#91;error, setError] = useState(null)
  const &#91;isLoading, setIsLoading] = useState(null)
  const { dispatch } = useAuthContext()

  const signup = async (email, password) =&gt; {
    setIsLoading(true)
    setError(null)

    const response = await fetch('/api/user/signup', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({email, password})
    })
    const json = await response.json()

    if (!response.ok) {
      setIsLoading(false)
      setError(json.error)
    }
    if (response.ok) {
      // save the user to local storage
      localStorage.setItem('user', JSON.stringify(json))

      // update the auth context
      dispatch({type: 'LOGIN', payload: json})

      setIsLoading(false)
    }
  }

  return { signup, isLoading, error}
} 
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Signup.js
import { useState } from 'react'
import { useSignup } from '../hooks/useSignup'

const Signup = () =&gt; {
  const &#91;email, setEmail] = useState('')
  const &#91;password, setPassword] = useState('')
  const {signup, error, isLoading} = useSignup()

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    await signup(email, password)
  }

  return (
    &lt;form className='signup' onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Sign up&lt;/h3&gt;

      &lt;label&gt;Email:&lt;/label&gt;
      &lt;input
      type="email"
      onChange={(e) =&gt; setEmail(e.target.value)}
      value={email}
      /&gt;
      &lt;label&gt;Password:&lt;/label&gt;
      &lt;input
      type="password"
      onChange={(e) =&gt; setPassword(e.target.value)}
      value={password}
      /&gt;
      
      &lt;button disabled={isLoading}&gt;Sign up&lt;/button&gt;
      {error &amp;&amp; &lt;div className='error'&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default Signup
</code></pre>



<h2 class="wp-block-heading">#11 – Making a useLogout Hook</h2>



<p>In this MERN auth tutorial, we’ll make a custom useLogout hook to log users out of the application.</p>



<ul class="wp-block-list">
<li>在 hooks 資料夾裡面建立 useLogout.js 檔案</li>



<li>修改 useLogout.js 檔案</li>



<li>修改 Navbar.js 檔案</li>



<li>修改 index.css 檔案</li>



<li>測試 Log out 能否正確刪除 Local Storage 資料<br>console 檢視狀態</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/hooks/useLogout.js
import { useAuthContext } from './useAuthContext'

export const useLogout = () =&gt; {
  const { dispatch } = useAuthContext()

  const logout = () =&gt; {
    // remove user from storage
    localStorage.removeItem('user')

    // dispatch logout action
    dispatch({type: 'LOGOUT'})
  }

  return {logout}
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Navbar.js
import { Link } from "react-router-dom"
import { useLogout } from '../hooks/useLogout'

const Navbar = () =&gt; {
  const { logout } = useLogout()

  const handleClick = () =&gt; {
    logout()
  }

  return (
    &lt;header&gt;
      &lt;div className="container"&gt;
        &lt;Link to="/"&gt;
          &lt;h1&gt;Workout Buddy&lt;/h1&gt;
        &lt;/Link&gt;
        &lt;nav&gt;
          &lt;div&gt;
            &lt;button onClick={handleClick}&gt;Log out&lt;/button&gt;
          &lt;/div&gt;
          &lt;div&gt;
            &lt;Link to="/login"&gt;Login&lt;/Link&gt;
            &lt;Link to="/signup"&gt;Signup&lt;/Link&gt;
          &lt;/div&gt;
        &lt;/nav&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}

input.error {
  border: 1px solid var(--error);
}

/* navbar */
nav {
  display: flex;
  align-items: center;
}

nav a {
  margin-left: 10px;
}

nav button {
  background: #fff;
  color: var(--primary);
  border: 2px solid var(--primary);
  padding: 6px 10px;
  border-radius: 4px;
  font-family: "Poppins";
  cursor: pointer;
  font-size: 1em;
}

/* auth forms */
form.signup, form.login {
  max-width: 400px;
  margin: 40px auto;
  padding: 20px;
  background: #fff;
  border-radius: 4px;
}
</code></pre>



<h2 class="wp-block-heading">#12 – Making a useLogin Hook</h2>



<p>In this MERN auth tutorial, we’ll make a custom useLogin hook to log users in to the application.</p>



<ul class="wp-block-list">
<li>在 hooks 資料夾裡面建立 useLogin.js 檔案</li>



<li>修改 useLogin.js 檔案，複製 useSignup.js 程式碼做修改</li>



<li>修改 Login.js 檔案</li>



<li>註冊好帳號，測試登入帳號<br>講解 Local Storage、Token、Refresh</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/hooks/useLogin.js
import { useState } from 'react'
import { useAuthContext } from './useAuthContext'

export const useLogin = () =&gt; {
  const &#91;error, setError] = useState(null)
  const &#91;isLoading, setIsLoading] = useState(null)
  const { dispatch } = useAuthContext()

  const login = async (email, password) =&gt; {
    setIsLoading(true)
    setError(null)

    const response = await fetch('/api/user/login', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({email, password})
    })
    const json = await response.json()

    if (!response.ok) {
      setIsLoading(false)
      setError(json.error)
    }
    if (response.ok) {
      // save the user to local storage
      localStorage.setItem('user', JSON.stringify(json))

      // update the auth context
      dispatch({type: 'LOGIN', payload: json})

      setIsLoading(false)
    }
  }

  return { login, isLoading, error}
} 
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Login.js
import { useState } from 'react'
import { useLogin } from '../hooks/useLogin'

const Login = () =&gt; {
  const &#91;email, setEmail] = useState('')
  const &#91;password, setPassword] = useState('')
  const {login, error, isLoading} = useLogin()

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    await login(email, password)
  }

  return (
    &lt;form className='login' onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Log in&lt;/h3&gt;

      &lt;label&gt;Email:&lt;/label&gt;
      &lt;input
      type="email"
      onChange={(e) =&gt; setEmail(e.target.value)}
      value={email}
      /&gt;
      &lt;label&gt;Password:&lt;/label&gt;
      &lt;input
      type="password"
      onChange={(e) =&gt; setPassword(e.target.value)}
      value={password}
      /&gt;
      
      &lt;button disabled={isLoading}&gt;Log in&lt;/button&gt;
      {error &amp;&amp; &lt;div className='error'&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default Login
</code></pre>



<h2 class="wp-block-heading">#13 – Setting the Initial Auth Status</h2>



<ul class="wp-block-list">
<li>修改 Navbar.js 檔案，匯入 useAuthContext</li>



<li>測試登入表單確認功能是否正常</li>



<li>重整頁面後會回到登入頁面</li>



<li>修改 AuthContext.js 檔案，useEffect</li>



<li>重整頁面後仍會保持在登入狀態</li>



<li>測試登出、登入功能是否正常</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/Navbar.js
import { Link } from "react-router-dom"
import { useLogout } from '../hooks/useLogout'
import { useAuthContext } from "../hooks/useAuthContext"

const Navbar = () =&gt; {
  const { logout } = useLogout()
  const { user } = useAuthContext()

  const handleClick = () =&gt; {
    logout()
  }

  return (
    &lt;header&gt;
      &lt;div className="container"&gt;
        &lt;Link to="/"&gt;
          &lt;h1&gt;Workout Buddy&lt;/h1&gt;
        &lt;/Link&gt;
        &lt;nav&gt;
          {user &amp;&amp; (
            &lt;div&gt;
              &lt;span&gt;{user.email}&lt;/span&gt;
              &lt;button onClick={handleClick}&gt;Log out&lt;/button&gt;
            &lt;/div&gt;
          )}
          {!user &amp;&amp; (
            &lt;div&gt;
              &lt;Link to="/login"&gt;Login&lt;/Link&gt;
              &lt;Link to="/signup"&gt;Signup&lt;/Link&gt;
            &lt;/div&gt;
          )}
        &lt;/nav&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/context/AuthContext.js
import { createContext, useReducer, useEffect } from 'react'

export const AuthContext = createContext()

export const authReducer = (state, action) =&gt; {
  switch (action.type) {
    case 'LOGIN':
      return { user: action.payload }
    case 'LOGOUT':
      return { user: null}
    default:
      return state
  }
}

export const AuthContextProvider = ({ children }) =&gt; {
  const &#91;state, dispatch] = useReducer(authReducer, {
    user: null
  })

  useEffect(() =&gt; {
    const user = JSON.parse(localStorage.getItem('user'))

    if (user) {
      dispatch({ type: 'LOGIN', payload: user })
    }
  }, &#91;])

  console.log('AuthContext State: ', state)

  return (
    &lt;AuthContext.Provider value={{...state, dispatch}}&gt;
      { children }
    &lt;/AuthContext.Provider&gt;
  )
}
</code></pre>



<h2 class="wp-block-heading has-background" style="background-color:#cf2e2e">#14 – Protecting API Routes</h2>



<p>In this MERN auth tutorial, you’ll learn how to protect certain API routes from unauthenticated users.</p>



<ul class="wp-block-list">
<li>在 backend 資料夾建立 middleware 資料夾</li>



<li>在 middleware 資料夾裡面建立 requireAuth.js 檔案</li>



<li>查看 workouts.js 檔案</li>



<li>修改 requireAuth.js 檔案</li>



<li>修改 workouts.js 檔案，requireAuth</li>



<li>使用 API 測試工具 Postman<br>http://localhost:4000/api/workouts/，GET 方法<br>Auth > Bearer > Bearer Token<br>http://localhost:4000/api/user/login，POST 方法<br>取得 token 資料</li>
</ul>



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

const requireAuth = async (req, res, next) =&gt; {

  // verify authentication
  const { authorization } = req.headers

  if (!authorization) {
    return res.status(401).json({error: 'Authorization token required'})
  }

  const token = authorization.split(' ')&#91;1]

  try {
    const {_id} = jwt.verify(token, process.env.SECRET)

    req.user = await User.findOne({ _id }).select('_id')
    next()

  } catch (error) {
    console.log(error);
    res.status(401).json({error: 'Request is not authorized'})
  }

}

module.exports = requireAuth
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/workouts.js
const express = require('express')
const {
  createWorkout,
  getWorkouts,
  getWorkout,
  deleteWorkout,
  updateWorkout
} = require('../controllers/workoutController')
const requireAuth = require('../middleware/requireAuth')

const router = express.Router()

// require auth for all workout routes
router.use(requireAuth)

// GET all workouts
router.get('/', getWorkouts)

// GET a single workout
router.get('/:id', getWorkout)

// POST a new workout
router.post('/', createWorkout)

// DELETE a workout
router.delete('/:id', deleteWorkout)

// UPDATE a workout
router.patch('/:id', updateWorkout)

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



<h2 class="wp-block-heading">#15 – Making Authorized Requests</h2>



<ul class="wp-block-list">
<li>需要調整的有三個地方，Home.js、WorkoutForm.js、WorkoutDetails.js 檔案</li>



<li>修改 Home.js 檔案</li>



<li>修改 WorkoutForm.js 檔案</li>



<li>修改 WorkoutDetails.js 檔案</li>



<li>使用 Add a New Workout 測試功能是否正常</li>



<li>登入帳號才能新增、刪除 Workout</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.js
import { useEffect } from 'react'
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'
import { useAuthContext } from '../hooks/useAuthContext'

// components
import WorkoutDetails from '../components/WorkoutDetails'
import WorkoutForm from '../components/WorkoutForm'

const Home = () =&gt; {
  const {workouts, dispatch} = useWorkoutsContext()
  const {user} = useAuthContext()
  
  useEffect(() =&gt; {
    const fetchWorkouts = async () =&gt; {
      const response = await fetch('/api/workouts', {
        headers: {
          'Authorization': `Bearer ${user.token}`
        }
      })
      const json = await response.json()

      if (response.ok) {
        dispatch({type: 'SET_WORKOUTS', payload: json})
      }
    }

    if (user) {
      fetchWorkouts()
    }
  }, &#91;dispatch, user])
  
  return (
    &lt;div className="home"&gt;
      &lt;div className="workouts"&gt;
        {workouts &amp;&amp; workouts.map((workout) =&gt; {
          return &lt;WorkoutDetails key={workout._id} workout={workout} /&gt;
        })}
      &lt;/div&gt;
      &lt;WorkoutForm /&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutForm.js
import { useState } from "react"
import { useWorkoutsContext } from "../hooks/useWorkoutsContext"
import { useAuthContext } from "../hooks/useAuthContext"

const WorkoutForm = () =&gt; {
  const { dispatch } = useWorkoutsContext()
  const { user } = useAuthContext()

  const &#91;title, setTitle] = useState('')
  const &#91;load, setLoad] = useState('')
  const &#91;reps, setReps] = useState('')
  const &#91;error, setError] = useState(null)
  const &#91;emptyFields, setEmptyFields] = useState(&#91;])

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    if (!user) {
      setError('You must be logged in')
      return
    }

    const workout = {title, load, reps}

    const response = await fetch('/api/workouts', {
      method: 'POST',
      body: JSON.stringify(workout),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${user.token}`
      }
    })
    const json = await response.json()

    if (!response.ok) {
      setError(json.error)
      setEmptyFields(json.emptyFields)
    }
    if (response.ok) {
      setTitle('')
      setLoad('')
      setReps('')
      setError(null)
      setEmptyFields(&#91;])
      console.log('new workout added', json);
      dispatch({type: 'CREATE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;form className="create" onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Add a New Workout&lt;/h3&gt;

      &lt;label&gt;Exercise Title:&lt;/label&gt;
      &lt;input
        type="text"
        onChange={(e) =&gt; setTitle(e.target.value)}
        value={title}
        className={emptyFields.includes('title') ? 'error' : ''}
      /&gt;

      &lt;label&gt;Load (in kg):&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setLoad(e.target.value)}
        value={load}
        className={emptyFields.includes('load') ? 'error' : ''}
      /&gt;

      &lt;label&gt;Reps:&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setReps(e.target.value)}
        value={reps}
        className={emptyFields.includes('reps') ? 'error' : ''}
      /&gt;

      &lt;button&gt;Add Workout&lt;/button&gt;
      {error &amp;&amp; &lt;div className="error"&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default WorkoutForm
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutDetails.js
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'
import { useAuthContext } from '../hooks/useAuthContext'

// date fns
import formatDistanceToNow from 'date-fns/formatDistanceToNow'

const WorkoutDetails = ({ workout }) =&gt; {
  const { dispatch } = useWorkoutsContext()
  const { user } = useAuthContext()

  const handleClick = async () =&gt; {
    if (!user) {
      return
    }
    const response = await fetch('/api/workouts/' + workout._id, {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${user.token}`
      }
    })
    const json = await response.json()

    if (response.ok) {
      dispatch({type: 'DELETE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;div className="workout-details"&gt;
      &lt;h4&gt;{workout.title}&lt;/h4&gt;
      &lt;p&gt;&lt;strong&gt;Load (kg): &lt;/strong&gt;{workout.load}&lt;/p&gt;
      &lt;p&gt;&lt;strong&gt;Reps: &lt;/strong&gt;{workout.reps}&lt;/p&gt;
      &lt;p&gt;{formatDistanceToNow(new Date(workout.createdAt), { addSuffix: true })}&lt;/p&gt;
      &lt;span className='material-symbols-outlined' onClick={handleClick}&gt;delete&lt;/span&gt;
    &lt;/div&gt;
  )
}

export default WorkoutDetails
</code></pre>



<h2 class="wp-block-heading">#16 – Protecting React Routes</h2>



<p>In this MERN auth tutorial we’ll protect some of the React routes from users that are not authenticated.</p>



<ul class="wp-block-list">
<li>修改 App.js 檔案，匯入 Navigate、useAuthContext</li>



<li>修改 index.css 檔案</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/App.js
import { BrowserRouter , Routes, Route, Navigate } from 'react-router-dom'
import { useAuthContext } from './hooks/useAuthContext'

// pages &amp; components
import Home from './pages/Home'
import Login from './pages/Login';
import Signup from './pages/Signup';
import Navbar from './components/Navbar'

function App() {
  const { user } = useAuthContext()

  return (
    &lt;div className="App"&gt;
        &lt;BrowserRouter&gt;
          &lt;Navbar /&gt;
          &lt;div className="pages"&gt;
            &lt;Routes&gt;
              &lt;Route
                path="/"
                element={user ? &lt;Home /&gt; : &lt;Navigate to="/login" /&gt;} 
              /&gt;
              &lt;Route
                path="/login"
                element={!user ? &lt;Login /&gt; : &lt;Navigate to="/" /&gt;} 
              /&gt;
              &lt;Route
                path="/signup"
                element={!user ? &lt;Signup /&gt; : &lt;Navigate to="/" /&gt;} 
              /&gt;
            &lt;/Routes&gt;
          &lt;/div&gt;
        &lt;/BrowserRouter&gt;
    &lt;/div&gt;
  );
}

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



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}

input.error {
  border: 1px solid var(--error);
}

/* navbar */
nav {
  display: flex;
  align-items: center;
}

nav a {
  margin-left: 10px;
}

nav button {
  background: #fff;
  color: var(--primary);
  border: 2px solid var(--primary);
  padding: 6px 10px;
  border-radius: 4px;
  font-family: "Poppins";
  cursor: pointer;
  font-size: 1em;
  margin-left: 10px;
}

/* auth forms */
form.signup, form.login {
  max-width: 400px;
  margin: 40px auto;
  padding: 20px;
  background: #fff;
  border-radius: 4px;
}
</code></pre>



<h2 class="wp-block-heading">#17 – Assigning Workouts to Users</h2>



<ul class="wp-block-list">
<li>刪除所有的 Workouts</li>



<li>修改 workoutModel.js 檔案，user_id</li>



<li>修改 workoutController.js 檔案，user_id</li>



<li>增加 Workout 內容<br>目前還是會在不同帳號看到相同內容<br>分別在不同帳號增加 Workout 內容</li>



<li>使用 API 測試工具 Postman，GET 方法<br>http://localhost:4000/api/workouts/<br>必須加上 Bearer<br>可以看到 Response 的 user_id 的差異</li>



<li>修改 workoutController.js，get all workouts 加入 user_id</li>



<li>登入不同的帳號確認 Workout 內容<br>問題: 會短暫看到其他人的 Workout 內容</li>



<li>clear global workouts state<br>修改 useLogout.js 檔案，匯入 useWorkoutsContext</li>
</ul>



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

const Schema =  mongoose.Schema

const workoutSchema = new Schema({
  title: {
    type:String,
    required: true
  },
  reps: {
    type: Number,
    required: true
  },
  load: {
    type: Number,
    required: true
  },
  user_id: {
    type: String,
    required: true
  }
}, { timestamps: true })

module.exports = mongoose.model('Workout', workoutSchema)
</code></pre>



<pre class="wp-block-code"><code>// backend/controllers/workoutController.js
const Workout = require('../models/workoutModel')
const mongoose = require('mongoose')

// get all workouts
const getWorkouts = async (req, res) =&gt; {
  const user_id = req.user._id

  const workouts = await Workout.find({ user_id }).sort({createdAt: -1})

  res.status(200).json(workouts)
}


// get a single workout
const getWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findById(id)

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


// create new workout
const createWorkout = async (req, res) =&gt; {
  const { title, load, reps } = req.body

  let emptyFields = &#91;]

  if(!title) {
    emptyFields.push('title')
  }
  if(!load) {
    emptyFields.push('load')
  }
  if(!reps) {
    emptyFields.push('reps')
  }
  if(emptyFields.length &gt; 0) {
    return res.status(400).json({ error: 'Please fill in all fields', emptyFields })
  }

  // add doc to db
  try {
    const user_id = req.user._id
    const workout = await Workout.create({title, load, reps, user_id})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// delete a workout
const deleteWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndDelete({_id: id})

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}

// update a workout
const updateWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndUpdate({_id: id}, {
    ...req.body
  })

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


module.exports = {
  getWorkouts,
  getWorkout,
  createWorkout,
  deleteWorkout,
  updateWorkout
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/hooks/useLogout.js
import { useAuthContext } from './useAuthContext'
import { useWorkoutsContext } from './useWorkoutsContext'

export const useLogout = () =&gt; {
  const { dispatch } = useAuthContext()
  const { dispatch: workoutsDispatch } = useWorkoutsContext()

  const logout = () =&gt; {
    // remove user from storage
    localStorage.removeItem('user')

    // dispatch logout action
    dispatch({type: 'LOGOUT'})
    workoutsDispatch({type: 'SET_WORKOUTS', payload: null})
  }

  return {logout}
}</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>MERN Stack Tutorial</title>
		<link>/wordpress_blog/mern-stack-tutorial/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Sat, 22 Jun 2024 04:57:04 +0000</pubDate>
				<category><![CDATA[Net Ninja]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=862</guid>

					<description><![CDATA[Learning From Youtube Channel: N [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Learning From Youtube Channel: Net Ninja<br>Video: MERN Stack Tutorial<br>Thank you.</p>



<h2 class="wp-block-heading">#1 – What is the MERN Stack?</h2>



<p>Learn how to create a web app using the MERN stack (MongoDB, Express, React &amp; Node.js).</p>



<h3 class="wp-block-heading">MERN Stack</h3>



<ul class="wp-block-list">
<li>Mongo、Express、React、Node.js</li>



<li>講解 Front-end (browser) 和 Back-end (server) 關係</li>



<li>介紹製作的專案，這個課程不介紹 Auth 功能</li>
</ul>



<h3 class="wp-block-heading">Before You Start</h3>



<ul class="wp-block-list">
<li>Node.js、MongoDB、React 相關知識</li>
</ul>



<h3 class="wp-block-heading">安裝 node.js</h3>



<ul class="wp-block-list">
<li><a href="https://nodejs.org/en" target="_blank" rel="noreferrer noopener">Node.js</a> – 安裝長期穩定版本</li>



<li>查詢 Node.js 版本 – node -v</li>
</ul>



<h3 class="wp-block-heading">課程檔案資源</h3>



<ul class="wp-block-list">
<li><a href="https://github.com/iamshaunjp/MERN-Stack-Tutorial" target="_blank" rel="noreferrer noopener">Course Files</a></li>
</ul>



<h2 class="wp-block-heading">#2 – Express App Setup</h2>



<p>In this MERN tutorial we’ll set up our initial Express app to power the backend api.</p>



<h3 class="wp-block-heading">MERN Stack</h3>



<ul class="wp-block-list">
<li>建立 mern stack 專案資料夾</li>



<li>在 mern stack 資料夾裡面建立 backend 資料夾<br>也可以命名為 server 資料夾</li>



<li>在 backend 資料夾裡面建立 server.js 檔案</li>



<li>建立 package.json 檔案<br>使用終端機移動到 backend 資料夾 – cd backend<br>npm init -y 快速建立 package.json 檔案</li>



<li>安裝 express 套件 – npm install express</li>



<li>修改 server.js 檔案</li>



<li>使用終端機執行 server – node server.js</li>



<li>因為需要不斷重新執行 server 所以需要使用 nodemon 套件幫助網頁開發<br>安裝 nodemon 套件 – npm install -g nodemon</li>



<li>執行 server – nodemon server.js<br>可避免需要一直不斷重新啟動伺服器的動作</li>



<li>修改 package.json 檔案 – 新增 scripts 指令</li>



<li>執行 npm run dev 運作 server</li>



<li>修改 server.js 檔案，新增 routes</li>



<li>建立 .env 檔案，儲存環境變數<br>有需要上傳到 Github 可建立 .gitignore 檔案忽略文件</li>



<li>安裝 dotenv 套件 – npm install dotenv</li>



<li>修改 server.js 檔案，使用 dotenv</li>



<li>執行指令 npm run dev</li>



<li>介紹 API 測試工具 – Postman<br>http://localhost:4000/，GET 方法、建立 MERN app Collection</li>



<li>修改 server.js 檔案，middleware</li>
</ul>



<pre class="wp-block-code"><code>// backend/server.js
require('dotenv').config()

const express = require('express')

// express app
const app = express()

// middleware
app.use((req, res, next) =&gt; {
  console.log(req.path, req.method)
  next()
})

// routes
app.get('/', (req, res) =&gt; {
  res.json({mssg: 'Welcome to the app'})
})

// listen for requrests
app.listen(process.env.PORT, () =&gt; {
  console.log('listening on port', process.env.PORT);
})
</code></pre>



<pre class="wp-block-code"><code>// package.json
{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1",
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": &#91;],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.19.2"
  }
}
</code></pre>



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



<h2 class="wp-block-heading">#3 – Express Router &amp; API Routes</h2>



<p>In this MERN tutorial we’ll create all of the workout routes we need for the api and test them out using POSTMAN.</p>



<h3 class="wp-block-heading">API Endpoints</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>GET</td><td>/workouts</td><td>Gets all the workout documents</td></tr><tr><td>POST</td><td>/workouts</td><td>Creates a new workout document</td></tr><tr><td>GET</td><td>/workouts/:id</td><td>Gets a single workout document</td></tr><tr><td>DELETE</td><td>/workouts/:id</td><td>Deletes a single workout</td></tr><tr><td>PATCH</td><td>/workouts/:id</td><td>Updates a single workout</td></tr></tbody></table></figure>



<ul class="wp-block-list">
<li>在 backend 資料夾裡面建立 routes 資料夾</li>



<li>在 routes 資料夾裡面建立 workouts.js 檔案</li>



<li>修改 workouts.js 檔案</li>



<li>修改 server.js 檔案，workoutRoutes</li>



<li>使用 API 測試工具 – POSTMAN<br>也可以使用其他 API 測試工具</li>
</ul>



<pre class="wp-block-code"><code>// backend/routes/workouts.js
const express = require('express')

const router = express.Router()

// GET all workouts
router.get('/', (req, res) =&gt; {
  res.json({mssg: 'GET all workouts'})
})

// GET a single workout
router.get('/:id', (req, res) =&gt; {
  res.json({mssg: 'GET a single workout'})
})

// POST a new workout
router.post('/', (req, res) =&gt; {
  res.json({mssg: 'POST a new workout'})
})

// DELETE a workout
router.delete('/:id', (req, res) =&gt; {
  res.json({mssg: 'DELETE a workout'})
})

// UPDATE a workout
router.patch('/:id', (req, res) =&gt; {
  res.json({mssg: 'UPDATE a workout'})
})

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



<pre class="wp-block-code"><code>// server.js
require('dotenv').config()

const express = require('express')
const workoutRoutes = require('./routes/workouts')

// express app
const app = express()

// middleware
app.use(express.json())

app.use((req, res, next) =&gt; {
  console.log(req.path, req.method)
  next()
})

// routes
app.use('/api/workouts', workoutRoutes)

// listen for requests
app.listen(process.env.PORT, () =&gt; {
  console.log('listening on port', process.env.PORT)
})
</code></pre>



<h2 class="wp-block-heading">#4 – MongoDB Altas &amp; Mongoose</h2>



<p>In this MERN tutorial we’ll set up a database using MongoDB Atlas, and then connect to it from our application using a package called Mongoose.</p>



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



<li>建立 Shared Cluster</li>



<li>Free Shared</li>



<li>Cloud Provider &amp; Region<br>aws、地區可自行選擇</li>



<li>Cluster Name: MERNapp</li>



<li>SECURITY > Database Access<br>Username and Password</li>



<li>SECURITY > Network Access</li>



<li>DEPLOYMENT > Database > Connect<br>Connect to your application<br>Add your connection string into your application code 複製</li>



<li>修改 .env 檔案</li>



<li>安裝 mongoose 套件 – npm install mongoose</li>



<li>修改 server.js 檔案</li>



<li>執行 npm run dev</li>
</ul>



<pre class="wp-block-code"><code>// .env
PORT=4000
MONGO_URI="connection string"
</code></pre>



<pre class="wp-block-code"><code>// server.js
require('dotenv').config()

const express = require('express')
const mongoose = require('mongoose')
const workoutRoutes = require('./routes/workouts')

// express app
const app = express()

// middleware
app.use(express.json())

app.use((req, res, next) =&gt; {
  console.log(req.path, req.method)
  next()
})

// routes
app.use('/api/workouts', workoutRoutes)

// connect to db
mongoose.connect(process.env.MONGO_URI)
  .then(() =&gt; {
    // listen for requests
    app.listen(process.env.PORT, () =&gt; {
      console.log('connected to db &amp; listening on port', process.env.PORT)
    })
  })
  .catch((error) =&gt; {
    console.log(error)
  })
</code></pre>



<h2 class="wp-block-heading">#5 – Models &amp; Schemas</h2>



<p>In this MERN tutorial you’ll create a new model &amp; schema for the dtabase collection we’ll be using (workouts).</p>



<ul class="wp-block-list">
<li>建立 models 資料夾</li>



<li>在 models 資料夾裡面建立 workoutModel.js 檔案</li>



<li>修改 workoutModel.js 檔案</li>



<li>修改 workouts.js 檔案</li>



<li>打開終端機查看 server.js 是否正常執行</li>



<li>使用 API 測試工具 Postman<br>localhost:4000/api/workouts/、POST 方法<br>Body > raw > JSON</li>



<li>講解物件少了一個屬性會產生錯誤</li>



<li>使用 MongoDB Atlas<br>DEPLOYMENT > Database > Browse Collections<br>查看是否有正確儲存到線上資料庫</li>
</ul>



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

const Schema =  mongoose.Schema

const workoutSchema = new Schema({
  title: {
    type:String,
    required: true
  },
  reps: {
    type: Number,
    required: true
  },
  load: {
    type: Number,
    required: true
  }
}, { timestamps: true })

module.exports = mongoose.model('Workout', workoutSchema)
</code></pre>



<pre class="wp-block-code"><code>// routes/workouts.js
const express = require('express')
const Workout = require('../models/workoutModel')

const router = express.Router()

// GET all workouts
router.get('/', (req, res) =&gt; {
  res.json({mssg: 'GET all workouts'})
})

// GET a single workout
router.get('/:id', (req, res) =&gt; {
  res.json({mssg: 'GET a single workout'})
})

// POST a new workout
router.post('/', async (req, res) =&gt; {
  const { title, load, reps } = req.body

  try {
    const workout = await Workout.create({title, load, reps})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

// DELETE a workout
router.delete('/:id', (req, res) =&gt; {
  res.json({mssg: 'DELETE a workout'})
})

// UPDATE a workout
router.patch('/:id', (req, res) =&gt; {
  res.json({mssg: 'UPDATE a workout'})
})

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



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "title": "Situps",
  "load": 0,
  "reps": 50
}
</code></pre>



<h2 class="wp-block-heading">#6 – Controllers (part 1)</h2>



<p>In this MERN tutorial we’ll make some controller functions for the workout routes.</p>



<ul class="wp-block-list">
<li>在 backend 資料夾裡面建立 controllers 資料夾</li>



<li>在 controllers 資料夾裡面建立 workoutController.js 檔案</li>



<li>修改 workouts.js 檔案</li>



<li>修改 workoutController.js 檔案</li>



<li>使用 API 測試工具 Postman 測試<br>localhost:4000/api/workouts/，POST 方法<br>新增資料內容<br>Body > raw > JSON</li>



<li>複製剛建立好的資料庫資料的id</li>



<li>使用 API 測試工具<br>http://localhost:4000/api/workouts/，GET 方法</li>



<li>使用 API 測試工具取得特定資料<br>http://localhost:4000/api/workouts/666ffbd9a0facf510722c94c，GET 方法<br>http://localhost:4000/api/workouts/43423，GET 方法</li>



<li>修改 workoutController.js 檔案，mongoose</li>



<li>再次使用 API 測試工具測試</li>
</ul>



<pre class="wp-block-code"><code>// backend/controllers/workoutController.js
const Workout = require('../models/workoutModel')
const mongoose = require('mongoose')

// get all workouts
const getWorkouts = async (req, res) =&gt; {
  const workouts = await Workout.find({}).sort({createdAt: -1})

  res.status(200).json(workouts)
}


// get a single workout
const getWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findById(id)

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


// create new workout
const createWorkout = async (req, res) =&gt; {
  const { title, load, reps } = req.body

  // add doc to db
  try {
    const workout = await Workout.create({title, load, reps})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// delete a workout


// update a workout


module.exports = {
  getWorkouts,
  getWorkout,
  createWorkout
}
</code></pre>



<pre class="wp-block-code"><code>// backend/workouts.js
const express = require('express')
const {
  createWorkout,
  getWorkouts,
  getWorkout
} = require('../controllers/workoutController')

const router = express.Router()

// GET all workouts
router.get('/', getWorkouts)

// GET a single workout
router.get('/:id', getWorkout)

// POST a new workout
router.post('/', createWorkout)

// DELETE a workout
router.delete('/:id', (req, res) =&gt; {
  res.json({mssg: 'DELETE a workout'})
})

// UPDATE a workout
router.patch('/:id', (req, res) =&gt; {
  res.json({mssg: 'UPDATE a workout'})
})

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



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "title": "Bench press",
  "load": 20,
  "reps": 40
}
</code></pre>



<h2 class="wp-block-heading">#7 – Controllers (part 2)</h2>



<p>In this MERN auth tutorial we’ll finish off the controller functions that we started in the last lesson.</p>



<ul class="wp-block-list">
<li>修改 workoutController.js 檔案，delete、update</li>



<li>修改 workouts.js 檔案</li>



<li>使用 API 測試工具 Postman，PATCH 方法<br>取得其中一筆資料 id<br>http://localhost:4000/api/workouts/666ffbd9a0facf510722c94c，PATCH 方法<br>Body > raw > JSON</li>



<li>使用 API 測試工具，GET 方法<br>查看資料是否有修改成功</li>



<li>使用 API 測試工具 Postman，DELETE 方法<br>取得其中一筆要刪除資料的 id<br>http://localhost:4000/api/workouts/666ffbd9a0facf510722c94c，DELETE 方法</li>



<li>使用 API 測試工具，GET 方法<br>查看資料是否有刪除成功</li>
</ul>



<pre class="wp-block-code"><code>// backend/controllers/workoutController.js
const Workout = require('../models/workoutModel')
const mongoose = require('mongoose')

// get all workouts
const getWorkouts = async (req, res) =&gt; {
  const workouts = await Workout.find({}).sort({createdAt: -1})

  res.status(200).json(workouts)
}


// get a single workout
const getWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findById(id)

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


// create new workout
const createWorkout = async (req, res) =&gt; {
  const { title, load, reps } = req.body

  // add doc to db
  try {
    const workout = await Workout.create({title, load, reps})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// delete a workout
const deleteWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndDelete({_id: id})

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}

// update a workout
const updateWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndUpdate({_id: id}, {
    ...req.body
  })

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


module.exports = {
  getWorkouts,
  getWorkout,
  createWorkout,
  deleteWorkout,
  updateWorkout
}
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/workouts.js
const express = require('express')
const {
  createWorkout,
  getWorkouts,
  getWorkout,
  deleteWorkout,
  updateWorkout
} = require('../controllers/workoutController')

const router = express.Router()

// GET all workouts
router.get('/', getWorkouts)

// GET a single workout
router.get('/:id', getWorkout)

// POST a new workout
router.post('/', createWorkout)

// DELETE a workout
router.delete('/:id', deleteWorkout)

// UPDATE a workout
router.patch('/:id', updateWorkout)

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



<pre class="wp-block-code"><code>// Body &gt; raw &gt; JSON
{
  "reps": 50
}
</code></pre>



<h2 class="wp-block-heading">#8 – Making a React App</h2>



<p>In this MERN tutorial, we’ll start our React application and set up a homepage route.</p>



<ul class="wp-block-list">
<li>終止後端 server 運行</li>



<li>在 mern stack 專案下建立 React App<br>npx create-react-app frontend</li>



<li>刪除 App.css、App.test.js、logo.svg、reportWebVitals.js、setupTests.js 檔案</li>



<li>修改 index.js 檔案</li>



<li>修改 App.js 檔案</li>



<li>安裝 react router 套件<br>移動到 frontend 資料夾位置<br>npm install react-router-dom</li>



<li>修改 App.js 檔案，匯入 react-router-dom</li>



<li>在 src 資料夾裡面建立 pages 資料夾</li>



<li>在 pages 資料夾裡面建立 Home.js 檔案</li>



<li>修改 Home.js 檔案</li>



<li>修改 App.js 檔案</li>



<li>打開終端機執行指令 – npm start<br>http://localhost:3000/</li>



<li>在 src 資料夾裡面建立 components 資料夾</li>



<li>在 components 資料夾裡面建立 Navbar.js 檔案</li>



<li>修改 Navbar.js 檔案</li>



<li>修改 App.js 檔案，匯入 Navbar</li>



<li>修改 index.css 檔案</li>



<li>檢查網頁畫面是否正常</li>
</ul>



<pre class="wp-block-code"><code>//  frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  &lt;React.StrictMode&gt;
    &lt;App /&gt;
  &lt;/React.StrictMode&gt;
)
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom'

// pages &amp; components
import Home from './pages/Home'
import Navbar from './components/Navbar'

function App() {

  return (
    &lt;div className="App"&gt;
      &lt;BrowserRouter&gt;
        &lt;Navbar /&gt;
        &lt;div className="pages"&gt;
          &lt;Routes&gt;
            &lt;Route 
              path="/" 
              element={&lt;Home /&gt;} 
            /&gt;
          &lt;/Routes&gt;
        &lt;/div&gt;
      &lt;/BrowserRouter&gt;
    &lt;/div&gt;
  );
}

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



<pre class="wp-block-code"><code>// forntend/src/pages/Home.js

const Home = () =&gt; {

  return (
    &lt;div className="home"&gt;
      &lt;h2&gt;Home&lt;/h2&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Navbar.js
import { Link } from 'react-router-dom'

const Navbar = () =&gt; {

  return (
    &lt;header&gt;
      &lt;div className="container"&gt;
        &lt;Link to="/"&gt;
          &lt;h1&gt;Workout Buddy&lt;/h1&gt;
        &lt;/Link&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}
</code></pre>



<h2 class="wp-block-heading">#9 – Fetching Data</h2>



<p>In this MERN stack tutorial we’ll make a request to the backend api to fetch workouts data and output it in the React template.</p>



<ul class="wp-block-list">
<li>修改 Home.js 檔案</li>



<li>運行後端 server<br>移動到 backend 資料夾<br>終端機指令運行 npm run dev</li>



<li>查看 Google Console，有 Cors 錯誤</li>



<li>修改 package.json 檔案，新增 proxy 屬性、值</li>



<li>修改 Home.js 檔案</li>



<li>前端終端機重新運行</li>



<li>在 components 資料夾裡面建立 WorkoutDetails.js 檔案</li>



<li>修改 Home.js 檔案</li>



<li>修改 WorkoutDetails.js 檔案</li>



<li>注意: 需加上 return 才不會出現錯誤</li>



<li>修改 index.css 檔案</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.js
import { useEffect, useState } from 'react'

// components
import WorkoutDetails from '../components/WorkoutDetails'

const Home = () =&gt; {
  const &#91;workouts, setWorkouts] = useState(null)
  
  useEffect(() =&gt; {
    const fetchWorkouts = async () =&gt; {
      const response = await fetch('/api/workouts')
      const json = await response.json()

      if (response.ok) {
        setWorkouts(json)
      }
    }

    fetchWorkouts()
  }, &#91;])
  
  return (
    &lt;div className="home"&gt;
      &lt;div className="workouts"&gt;
        {workouts &amp;&amp; workouts.map((workout) =&gt; {
          return &lt;WorkoutDetails key={workout._id} workout={workout} /&gt;
        })}
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/package.json
{
  "proxy": "http://localhost:4000/",
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.23.1",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": &#91;
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": &#91;
      "&gt;0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": &#91;
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutDetails.js
const WorkoutDetails = ({ workout }) =&gt; {
  return (
    &lt;div className="workout-details"&gt;
      &lt;h4&gt;{workout.title}&lt;/h4&gt;
      &lt;p&gt;&lt;strong&gt;Load (kg): &lt;/strong&gt;{workout.load}&lt;/p&gt;
      &lt;p&gt;&lt;strong&gt;Reps: &lt;/strong&gt;{workout.reps}&lt;/p&gt;
      &lt;p&gt;{workout.createdAt}&lt;/p&gt;
    &lt;/div&gt;
  )
}

export default WorkoutDetails
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}
</code></pre>



<h2 class="wp-block-heading">#10 – New Workout Form</h2>



<p>In this lesson we’ll make a form to add new workouts.</p>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 WorkoutForm.js 檔案</li>



<li>修改 WorkoutForm.js 檔案</li>



<li>查看後端 server.js 檔案，routes</li>



<li>修改 WorkoutForm.js 檔案，response</li>



<li>查看後端 workoutController.js 檔案</li>



<li>修改 WorkoutForm.js 檔案，error</li>



<li>修改 Home.js 檔案</li>



<li>修改 WorkoutForm.js 檔案</li>



<li>在網頁測試增加功能，資料不完整與完整的差別</li>



<li>修改 index.css 檔案</li>



<li>在網頁測試增加功能，樣式修改後的變化</li>
</ul>



<pre class="wp-block-code"><code>// forntend/src/components/WorkoutForm.js
import { useState } from "react"

const WorkoutForm = () =&gt; {
  const &#91;title, setTitle] = useState('')
  const &#91;load, setLoad] = useState('')
  const &#91;reps, setReps] = useState('')
  const &#91;error, setError] = useState(null)

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    const workout = {title, load, reps}

    const response = await fetch('/api/workouts', {
      method: 'POST',
      body: JSON.stringify(workout),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    const json = await response.json()

    if (!response.ok) {
      setError(json.error)
    }
    if (response.ok) {
      setTitle('')
      setLoad('')
      setReps('')
      setError(null)
      console.log('new workout added', json);
    }
  }

  return (
    &lt;form className="create" onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Add a New Workout&lt;/h3&gt;

      &lt;label&gt;Exercise Title:&lt;/label&gt;
      &lt;input
        type="text"
        onChange={(e) =&gt; setTitle(e.target.value)}
        value={title}
      /&gt;

      &lt;label&gt;Load (in kg):&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setLoad(e.target.value)}
        value={load}
      /&gt;

      &lt;label&gt;Reps:&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setReps(e.target.value)}
        value={reps}
      /&gt;

      &lt;button&gt;Add Workout&lt;/button&gt;
      {error &amp;&amp; &lt;div className="error"&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default WorkoutForm
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.js
import { useEffect, useState } from 'react'

// components
import WorkoutDetails from '../components/WorkoutDetails'
import WorkoutForm from '../components/WorkoutForm'

const Home = () =&gt; {
  const &#91;workouts, setWorkouts] = useState(null)
  
  useEffect(() =&gt; {
    const fetchWorkouts = async () =&gt; {
      const response = await fetch('/api/workouts')
      const json = await response.json()

      if (response.ok) {
        setWorkouts(json)
      }
    }

    fetchWorkouts()
  }, &#91;])
  
  return (
    &lt;div className="home"&gt;
      &lt;div className="workouts"&gt;
        {workouts &amp;&amp; workouts.map((workout) =&gt; {
          return &lt;WorkoutDetails key={workout._id} workout={workout} /&gt;
        })}
      &lt;/div&gt;
      &lt;WorkoutForm /&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}
</code></pre>



<h2 class="wp-block-heading has-background" style="background-color:#cf2e2e">#11 – Adding React Context</h2>



<p>In this MERN tutorial we’ll create a React Context to provide some global workouts state to the entire React application</p>



<ul class="wp-block-list">
<li>在 src 資料夾裡面建立 context 資料夾</li>



<li>在 context 資料夾裡面建立 WorkoutContext.js 檔案</li>



<li>修改 WorkoutContext.js 檔案</li>



<li>修改 index.js 檔案</li>



<li>修改 WorkoutContext.js 檔案</li>



<li>在 src 資料夾裡面建立 hooks 資料夾</li>



<li>在 hooks 資料夾裡面建立 useWorkoutsContext.js 檔案</li>



<li>修改 useWorkoutsContext.js 檔案</li>



<li>講解 Home.js 檔案</li>



<li>講解 WorkoutContext.js 檔案</li>



<li>修改 Home.js 檔案，匯入 useWorkoutsContext、移除 useState</li>



<li>修改 WorkoutForm.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/context/WorkoutContext.js
import { createContext, useReducer } from 'react'

export const WorkoutsContext = createContext()

export const workoutsReducer = (state, action) =&gt; {
  switch (action.type) {
    case 'SET_WORKOUTS':
      return {
        workouts: action.payload
      }
    case 'CREATE_WORKOUT':
      return {
        workouts: &#91;action.payload, ...state.workouts]
      }
    default:
      return state
  }
}

export const WorkoutsContextProvider = ({ children }) =&gt; {
  const &#91;state, dispatch] = useReducer(workoutsReducer, {
    workouts: null
  })



  return (
    &lt;WorkoutsContext.Provider value={{...state, dispatch}}&gt;
      { children }
    &lt;/WorkoutsContext.Provider&gt;
  )
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { WorkoutsContextProvider } from './context/WorkoutContext';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  &lt;React.StrictMode&gt;
    &lt;WorkoutsContextProvider&gt;
      &lt;App /&gt;
    &lt;/WorkoutsContextProvider&gt;
  &lt;/React.StrictMode&gt;
);
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/hooks/useWorkoutsContext.js
import { WorkoutsContext } from "../context/WorkoutContext";
import { useContext } from 'react'

export const useWorkoutsContext = () =&gt; {
  const context = useContext(WorkoutsContext)

  if (!context) {
    throw Error('useWorkoutsContext must be used inside an WorkoutsContextProvider')
  }

  return context
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.js
import { useEffect } from 'react'
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'

// components
import WorkoutDetails from '../components/WorkoutDetails'
import WorkoutForm from '../components/WorkoutForm'

const Home = () =&gt; {
  const {workouts, dispatch} = useWorkoutsContext()
  
  useEffect(() =&gt; {
    const fetchWorkouts = async () =&gt; {
      const response = await fetch('/api/workouts')
      const json = await response.json()

      if (response.ok) {
        dispatch({type: 'SET_WORKOUTS', payload: json})
      }
    }

    fetchWorkouts()
  }, &#91;dispatch])
  
  return (
    &lt;div className="home"&gt;
      &lt;div className="workouts"&gt;
        {workouts &amp;&amp; workouts.map((workout) =&gt; {
          return &lt;WorkoutDetails key={workout._id} workout={workout} /&gt;
        })}
      &lt;/div&gt;
      &lt;WorkoutForm /&gt;
    &lt;/div&gt;
  )
}

export default Home
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutForm.js
import { useState } from "react"
import { useWorkoutsContext } from "../hooks/useWorkoutsContext"

const WorkoutForm = () =&gt; {
  const { dispatch } = useWorkoutsContext()

  const &#91;title, setTitle] = useState('')
  const &#91;load, setLoad] = useState('')
  const &#91;reps, setReps] = useState('')
  const &#91;error, setError] = useState(null)

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    const workout = {title, load, reps}

    const response = await fetch('/api/workouts', {
      method: 'POST',
      body: JSON.stringify(workout),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    const json = await response.json()

    if (!response.ok) {
      setError(json.error)
    }
    if (response.ok) {
      setTitle('')
      setLoad('')
      setReps('')
      setError(null)
      console.log('new workout added', json);
      dispatch({type: 'CREATE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;form className="create" onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Add a New Workout&lt;/h3&gt;

      &lt;label&gt;Exercise Title:&lt;/label&gt;
      &lt;input
        type="text"
        onChange={(e) =&gt; setTitle(e.target.value)}
        value={title}
      /&gt;

      &lt;label&gt;Load (in kg):&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setLoad(e.target.value)}
        value={load}
      /&gt;

      &lt;label&gt;Reps:&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setReps(e.target.value)}
        value={reps}
      /&gt;

      &lt;button&gt;Add Workout&lt;/button&gt;
      {error &amp;&amp; &lt;div className="error"&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default WorkoutForm
</code></pre>



<h2 class="wp-block-heading">#12 – Deleting Data</h2>



<ul class="wp-block-list">
<li>修改 WorkoutDetails.js 檔案</li>



<li>修改 WorkoutContext.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutDetails.js
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'

const WorkoutDetails = ({ workout }) =&gt; {
  const { dispatch } = useWorkoutsContext()

  const handleClick = async () =&gt; {
    const response = await fetch('/api/workouts/' + workout._id, {
      method: 'DELETE'
    })
    const json = await response.json()

    if (response.ok) {
      dispatch({type: 'DELETE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;div className="workout-details"&gt;
      &lt;h4&gt;{workout.title}&lt;/h4&gt;
      &lt;p&gt;&lt;strong&gt;Load (kg): &lt;/strong&gt;{workout.load}&lt;/p&gt;
      &lt;p&gt;&lt;strong&gt;Reps: &lt;/strong&gt;{workout.reps}&lt;/p&gt;
      &lt;p&gt;{workout.createdAt}&lt;/p&gt;
      &lt;span onClick={handleClick}&gt;delete&lt;/span&gt;
    &lt;/div&gt;
  )
}

export default WorkoutDetails
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/context/WorkoutContext.js
import { createContext, useReducer } from 'react'

export const WorkoutsContext = createContext()

export const workoutsReducer = (state, action) =&gt; {
  switch (action.type) {
    case 'SET_WORKOUTS':
      return {
        workouts: action.payload
      }
    case 'CREATE_WORKOUT':
      return {
        workouts: &#91;action.payload, ...state.workouts]
      }
    case 'DELETE_WORKOUT':
      return {
        workouts: state.workouts.filter((w) =&gt; w._id !== action.payload._id)
      }
    default:
      return state
  }
}

export const WorkoutsContextProvider = ({ children }) =&gt; {
  const &#91;state, dispatch] = useReducer(workoutsReducer, {
    workouts: null
  })

  return (
    &lt;WorkoutsContext.Provider value={{...state, dispatch}}&gt;
      { children }
    &lt;/WorkoutsContext.Provider&gt;
  )
}
</code></pre>



<h2 class="wp-block-heading">#13 – Handling Error Responses</h2>



<ul class="wp-block-list">
<li>講解後端 workoutController.js 檔案</li>



<li>講解後端 workoutModel.js 檔案</li>



<li>修改 workoutController.js 檔案</li>



<li>修改 WorkoutForm.js 檔案</li>



<li>修改 index.css 檔案</li>
</ul>



<pre class="wp-block-code"><code>// backend/controllers/workoutController.js
const Workout = require('../models/workoutModel')
const mongoose = require('mongoose')

// get all workouts
const getWorkouts = async (req, res) =&gt; {
  const workouts = await Workout.find({}).sort({createdAt: -1})

  res.status(200).json(workouts)
}


// get a single workout
const getWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findById(id)

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


// create new workout
const createWorkout = async (req, res) =&gt; {
  const { title, load, reps } = req.body

  let emptyFields = &#91;]

  if(!title) {
    emptyFields.push('title')
  }
  if(!load) {
    emptyFields.push('load')
  }
  if(!reps) {
    emptyFields.push('reps')
  }
  if(emptyFields.length &gt; 0) {
    return res.status(400).json({ error: 'Please fill in all fields', emptyFields })
  }

  // add doc to db
  try {
    const workout = await Workout.create({title, load, reps})
    res.status(200).json(workout)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
}

// delete a workout
const deleteWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndDelete({_id: id})

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}

// update a workout
const updateWorkout = async (req, res) =&gt; {
  const { id } = req.params

  if (!mongoose.Types.ObjectId.isValid(id)) {
    return res.status(404).json({error: 'No such workout'})
  }

  const workout = await Workout.findOneAndUpdate({_id: id}, {
    ...req.body
  })

  if (!workout) {
    return res.status(404).json({error: 'No such workout'})
  }

  res.status(200).json(workout)
}


module.exports = {
  getWorkouts,
  getWorkout,
  createWorkout,
  deleteWorkout,
  updateWorkout
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutForm.js
import { useState } from "react"
import { useWorkoutsContext } from "../hooks/useWorkoutsContext"

const WorkoutForm = () =&gt; {
  const { dispatch } = useWorkoutsContext()

  const &#91;title, setTitle] = useState('')
  const &#91;load, setLoad] = useState('')
  const &#91;reps, setReps] = useState('')
  const &#91;error, setError] = useState(null)
  const &#91;emptyFields, setEmptyFields] = useState(&#91;])

  const handleSubmit = async (e) =&gt; {
    e.preventDefault()

    const workout = {title, load, reps}

    const response = await fetch('/api/workouts', {
      method: 'POST',
      body: JSON.stringify(workout),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    const json = await response.json()

    if (!response.ok) {
      setError(json.error)
      setEmptyFields(json.emptyFields)
    }
    if (response.ok) {
      setTitle('')
      setLoad('')
      setReps('')
      setError(null)
      setEmptyFields(&#91;])
      console.log('new workout added', json);
      dispatch({type: 'CREATE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;form className="create" onSubmit={handleSubmit}&gt;
      &lt;h3&gt;Add a New Workout&lt;/h3&gt;

      &lt;label&gt;Exercise Title:&lt;/label&gt;
      &lt;input
        type="text"
        onChange={(e) =&gt; setTitle(e.target.value)}
        value={title}
        className={emptyFields.includes('title') ? 'error' : ''}
      /&gt;

      &lt;label&gt;Load (in kg):&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setLoad(e.target.value)}
        value={load}
        className={emptyFields.includes('load') ? 'error' : ''}
      /&gt;

      &lt;label&gt;Reps:&lt;/label&gt;
      &lt;input
        type="number"
        onChange={(e) =&gt; setReps(e.target.value)}
        value={reps}
        className={emptyFields.includes('reps') ? 'error' : ''}
      /&gt;

      &lt;button&gt;Add Workout&lt;/button&gt;
      {error &amp;&amp; &lt;div className="error"&gt;{error}&lt;/div&gt;}
    &lt;/form&gt;
  )
}

export default WorkoutForm
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/index.css
/* google font */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;800&amp;family=VT323&amp;display=swap');

/* layout */
:root {
  --primary: #1aac83;
  --error: #e7195a;
}
body {
  background: #f1f1f1;
  margin: 0;
  font-family: "Poppins";
}
header {
  background: #fff;
}
header .container {
  max-width: 1400px;
  margin: 0 auto;
  padding: 10px 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
header a {
  color: #333;
  text-decoration: none;
}
.pages{
  max-width: 1400px;
  padding: 20px;
  margin: 0 auto;
}

/* homepage */
.home {
  display: grid;
  grid-template-columns: 3fr 1fr;
  gap: 100px;
}

.workout-details {
  background: #fff;
  border-radius: 4px;
  margin: 20px auto;
  padding: 20px;
  position: relative;
  box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
}

.workout-details h4 {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: var(--primary);
}

.workout-details p {
  margin: 0;
  font-size: 0.9em;
  color: #555;
}

.workout-details span {
  position: absolute;
  top: 20px;
  right: 20px;
  cursor: pointer;
  background: #f1f1f1;
  padding: 6px;
  border-radius: 50%;
  color: #333;
}

/* new workout form */
label, input {
  display: block;
}

input {
  padding: 10px;
  margin-top: 10px;
  margin-bottom: 20px;
  width: 100%;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

form button {
  background: var(--primary);
  border: 0;
  color: #fff;
  padding: 10px;
  font-family: "Poppins";
  border-radius: 4px;
  cursor: pointer;
}

div.error {
  padding: 10px;
  background: #ffefef;
  border: 1px solid var(--error);
  color: var(--error);
  border-radius: 4px;
  margin: 20px 0;
}

input.error {
  border: 1px solid var(--error);
}
</code></pre>



<h2 class="wp-block-heading">#14 – Finishing Touches</h2>



<ul class="wp-block-list">
<li>修改 index.html 檔案，匯入 Google Material Symbols &amp; Icons</li>



<li>修改 WorkoutDetails.js 檔案</li>



<li>測試刪除按鈕是否能正常運作</li>



<li>使用、安裝 date-fns 套件<br>npm install date-fns</li>



<li>修改 WorkoutDetails.js 檔案</li>



<li>修改 Home.js 檔案 (Debug)，dispatch 之前已自行修正</li>
</ul>



<pre class="wp-block-code"><code>// frontend/public/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="%PUBLIC_URL%/favicon.ico" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1" /&gt;
    &lt;meta name="theme-color" content="#000000" /&gt;
    &lt;meta
      name="description"
      content="Web site created using create-react-app"
    /&gt;
    &lt;link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /&gt;
    &lt;!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    --&gt;
    &lt;link rel="manifest" href="%PUBLIC_URL%/manifest.json" /&gt;
    &lt;!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    --&gt;
    &lt;link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" /&gt;
    &lt;title&gt;React App&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;noscript&gt;You need to enable JavaScript to run this app.&lt;/noscript&gt;
    &lt;div id="root"&gt;&lt;/div&gt;
    &lt;!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the &lt;body&gt; tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    --&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/WorkoutDetails.js
import { useWorkoutsContext } from '../hooks/useWorkoutsContext'

// date fns
import formatDistanceToNow from 'date-fns/formatDistanceToNow'

const WorkoutDetails = ({ workout }) =&gt; {
  const { dispatch } = useWorkoutsContext()

  const handleClick = async () =&gt; {
    const response = await fetch('/api/workouts/' + workout._id, {
      method: 'DELETE'
    })
    const json = await response.json()

    if (response.ok) {
      dispatch({type: 'DELETE_WORKOUT', payload: json})
    }
  }

  return (
    &lt;div className="workout-details"&gt;
      &lt;h4&gt;{workout.title}&lt;/h4&gt;
      &lt;p&gt;&lt;strong&gt;Load (kg): &lt;/strong&gt;{workout.load}&lt;/p&gt;
      &lt;p&gt;&lt;strong&gt;Reps: &lt;/strong&gt;{workout.reps}&lt;/p&gt;
      &lt;p&gt;{formatDistanceToNow(new Date(workout.createdAt), { addSuffix: true })}&lt;/p&gt;
      &lt;span className='material-symbols-outlined' onClick={handleClick}&gt;delete&lt;/span&gt;
    &lt;/div&gt;
  )
}

export default WorkoutDetails</code></pre>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
