學習來自 YT: compiletab
影片: Build & Deploy Full Stack E-commerce Website | Redux | MERN Stack Project
Github assets: 連結
Thank you, teacher
建立 & 部署全端商業網站
影片時間戳
00:00:00 – Introduction
00:01:56 – Demo
00:05:26 – Installation & set up
05:50:25 – Admin UI
07:20:25 – Backend setup
07:32:40 – user routes
08:05:40 – Products routes
09:18:56 – cart routes
10:13:26 – checkout routes
10:57:55 – Admin routes
11:58:30 – Redux
Redux
Redux 透過提供統一的狀態儲存、規範化的狀態更新流程,讓開發者能夠更容易地管理大型應用的狀態,並提高應用的可維護性和可擴展性。
- Redux – A JS library for predictable and maintainable global state management
安裝套件
// frontend
// terminal - 終端機
npm i react-redux @reduxjs/toolkit axios
// frontend/src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: {
},
});
export default store;
// frontend/.env
VITE_PAYPAL_CLIENT_ID=你的client-id
VITE_BACKEND_URL=你後端的localhost
Auth Slice (身份驗證切片)
// 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 }) => {
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 }) => {
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) => {
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) => {
state.guestId = `guest_${new Date().getTime()}`;
localStorage.setItem("guestId", state.guestId);
},
},
extraReducers: (builder) => {
builder
.addCase(loginUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.loading = false;
state.error = action.payload;
})
.addCase(loginUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload.message;
})
.addCase(registerUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(registerUser.fulfilled, (state, action) => {
state.loading = false;
state.error = action.payload;
})
.addCase(registerUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload.message;
});
},
});
export const { logout, generateNewGuestId } = authSlice.actions;
export default authSlice.reducer;
// 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 = () => {
return (
<Provider store={store}>
<BrowserRouter>
<Toaster position="top-right" />
<Routes>
<Route path="/" element={<UserLayout />}>
{/* User Layout - 使用者佈局 */}
<Route index element={<Home />} />
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="profile" element={<Profile />} />
<Route
path="collections/:collection"
element={<CollectionPage />}
/>
<Route path="product/:id" element={<ProductDetails />} />
<Route path="checkout" element={<Checkout />} />
<Route
path="order-confirmation"
element={<OrderConfirmationPage />}
/>
<Route path="order/:id" element={<OrderDetailsPage />} />
<Route path="my-orders" element={<MyOrdersPage />} />
</Route>
<Route path="/admin" element={<AdminLayout />}>
{/* Admin Layout - 管理員佈局 */}
<Route index element={<AdminHomePage />} />
<Route path="users" element={<UserManagement />} />
<Route path="products" element={<ProductManagement />} />
<Route path="products/:id/edit" element={<EditProductPage />} />
<Route path="orders" element={<OrderManagement />} />
</Route>
</Routes>
</BrowserRouter>
</Provider>
);
};
export default App;
// 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 = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
// console.log("User Login:", { email, password });
dispatch(loginUser({ email, password }));
};
return (
<div className="flex">
<div className="w-full md:w-1/2 flex flex-col justify-center items-center p-8 md:p-12">
<form
onSubmit={handleSubmit}
className="w-full max-w-md bg-white p-8 rounded-lg border shadow-sm"
>
<div className="flex justify-center mb-6">
<h2 className="text-xl font-medium">Rabbit</h2>
</div>
<h2 className="text-2xl font-bold text-center mb-6">Hey there! </h2>
<p className="text-center mb-6">
Enter your username and password to Login.
</p>
<div className="mb-4">
<label className="block text-sm font-semibold mb-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded"
placeholder="Enter your email address"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-semibold mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border rounded"
placeholder="Enter your password"
/>
</div>
<button
type="submit"
className="w-full bg-black text-white p-2 rounded-lg font-semibold hover:bg-gray-800 transition"
>
Sign In
</button>
<p className="mt-6 text-center text-sm">
Don't have an account?{" "}
<Link to="/register" className="text-blue-500">
Register
</Link>
</p>
</form>
</div>
<div className="hidden md:block w-1/2 bg-gray-800">
<div className="h-full flex flex-col justify-center items-center">
<img
src={login}
alt="Login to Account"
className="h-[750px] w-full object-cover"
/>
</div>
</div>
</div>
);
};
export default Login;
測試登入頁面功能
- http://localhost:5173/login
- 輸入電子信箱、密碼 > 登入
- 檢查 > Network
可以看到 token 和 user 資料 - 檢查 > Application
從 Local storage 可以看到 userInfo、userToken - 檢查 > Console
出現一些問題,進行除錯
// frontend/src/redux/store.js
// 除錯
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
const store = configureStore({
reducer: {
auth: authReducer,
},
});
export default store;
// frontend/src/pages/Register.jsx
測試註冊頁面功能
- http://localhost:5173/register
- 輸入名字、電子信箱、密碼
- 檢查 > Network
可以看到 user 和 token - 檢查 > Application
從 Local storage 可以看到 userInfo、userToken
查看 MongoDB Atlas 資料庫
- Browse collections (瀏覽集合) > users
查看是否有更新使用者
Product Slice (產品切片)
// 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,
}) => {
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) => {
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 }) => {
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 }) => {
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: [],
selectedProduct: null, // Store the details of the single Product - 儲存單一產品的詳細資料
similarProducts: [],
loading: false,
error: null,
filters: {
category: "",
size: "",
color: "",
gender: "",
brand: "",
minPrice: "",
maxPrice: "",
sortBy: "",
search: "",
material: "",
collection: "",
},
},
reducers: {
setFilters: (state, action) => {
state.filters = { ...state.filters, ...action.payload };
},
clearFilters: (state) => {
state.filters = {
category: "",
size: "",
color: "",
gender: "",
brand: "",
minPrice: "",
maxPrice: "",
sortBy: "",
search: "",
material: "",
collection: "",
};
},
},
extraReducers: (builder) => {
builder
// Handle fetching products with filter - 處理使用篩選條件獲取產品
.addCase(fetchProductsByFilters.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchProductsByFilters.fulfilled, (state, action) => {
state.loading = false;
state.products = Array.isArray(action.payload) ? action.payload : [];
})
.addCase(fetchProductsByFilters.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
// Handle fetching single product details - 處理獲取單一產品詳細資料
.addCase(fetchProductDetails.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchProductDetails.fulfilled, (state, action) => {
state.loading = false;
state.selectedProduct = action.payload;
})
.addCase(fetchProductDetails.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
// Handle updating product - 處理更新產品
.addCase(updateProduct.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(updateProduct.fulfilled, (state, action) => {
state.loading = false;
const updatedProduct = action.payload;
const index = state.products.findIndex(
(product) => product._id === updateProduct._id
);
if (index !== -1) {
state.products[index] = updateProduct;
}
})
.addCase(updateProduct.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
// Handle fetching similar products - 處理獲取相似產品
.addCase(fetchSimilarProducts.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchSimilarProducts.fulfilled, (state, action) => {
state.loading = false;
state.products = action.payload;
})
.addCase(fetchSimilarProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});
export const { setFilters, clearFilters } = productsSlice.actions;
export default productsSlice.reducer;
// 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;
Cart Slice (購物車切片)
// 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 = () => {
const storedCart = localStorage.getItem("cart");
return storedCart ? JSON.parse(storedCart) : { products: [] };
};
// Helper function to save cart to localStorage - 將購物車儲存到本地儲存的輔助函數
const saveCartToStorage = (cart) => {
localStorage.setItem("cart", JSON.stringify(cart));
};
// Fetch cart for a user or guest - 為用戶或訪客獲取購物車
export const fetchCart = createAsyncThunk(
"cart/fetchCart",
async ({ userId, guestId }, { rejectWithValue }) => {
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 }
) => {
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 }
) => {
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 }) => {
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 }) => {
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) => {
state.cart = { products: [] };
localStorage.removeItem("cart");
},
},
extraReducers: (builder) => {
builder
.addCase(fetchCart.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchCart.fulfilled, (state, action) => {
state.loading = false;
state.error = action.payload;
saveCartToStorage(action.payload);
})
.addCase(fetchCart.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || "Failed to fetch cart";
})
.addCase(addToCart.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(addToCart.fulfilled, (state, action) => {
state.loading = false;
state.error = action.payload;
saveCartToStorage(action.payload);
})
.addCase(addToCart.rejected, (state, action) => {
state.loading = false;
state.error = action.payload?.message || "Failed to add to cart";
})
.addCase(updateCartItemQuantity.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(updateCartItemQuantity.fulfilled, (state, action) => {
state.loading = false;
state.error = action.payload;
saveCartToStorage(action.payload);
})
.addCase(updateCartItemQuantity.rejected, (state, action) => {
state.loading = false;
state.error =
action.payload?.message || "Failed to update item quantity";
})
.addCase(removeFromCart.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(removeFromCart.fulfilled, (state, action) => {
state.loading = false;
state.error = action.payload;
saveCartToStorage(action.payload);
})
.addCase(removeFromCart.rejected, (state, action) => {
state.loading = false;
state.error = action.payload?.message || "Failed to remove item";
})
.addCase(mergeCart.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(mergeCart.fulfilled, (state, action) => {
state.loading = false;
state.error = action.payload;
saveCartToStorage(action.payload);
})
.addCase(mergeCart.rejected, (state, action) => {
state.loading = false;
state.error = action.payload?.message || "Failed to merge cart";
});
},
});
export const { clearCart } = cartSlice.actions;
export default cartSlice.reducer;
// 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;
Checkout Slice (結帳切片)
// 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 }) => {
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) => {
builder.addCase(createCheckout.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(createCheckout.fulfilled, (state, action) => {
state.loading = false;
state.checkout = action.payload;
});
.addCase(createCheckout.rejected, (state, action) => {
state.loading = true;
state.error = action.payload.message;
});
},
});
export default checkoutSlice.reducer;
// 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;
Order Slice (訂單切片)
// 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 }) => {
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 }) => {
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: [],
totalOrders: 0,
orderDetails: null,
loading: false,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
// Fetch user order - 獲取用戶訂單
.addCase(fetchUserOrders.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUserOrders.fulfilled, (state, action) => {
state.loading = false;
state.orders = action.payload;
})
.addCase(fetchUserOrders.rejected, (state, action) => {
state.loading = false;
state.error = action.payload.message;
})
// Fetch order details - 獲取訂單詳情
.addCase(fetchOrderDetails.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchOrderDetails.fulfilled, (state, action) => {
state.loading = false;
state.orderDetails = action.payload;
})
.addCase(fetchOrderDetails.rejected, (state, action) => {
state.loading = false;
state.error = action.payload.message;
});
},
});
export default orderSlice.reducer;
// 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;
Admin Slice (管理員切片)
// 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 () => {
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 }) => {
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 }) => {
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) => {
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: [],
loading: false,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.users = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
.addCase(updateUser.fulfilled, (state, action) => {
const updatedUser = action.payload;
const userIndex = state.users.findIndex(
(user) => user._id === updatedUser._id
);
if (userIndex !== -1) {
state.users[userIndex] = updatedUser;
}
})
.addCase(deleteUser.fulfilled, (state, action) => {
state.users = state.users.filter((user) => user._id !== action.payload);
})
.addCase(addUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(addUser.fulfilled, (state, action) => {
state.loading = false;
state.users.push(action.payload.user); // add a new user to the state - 將新用戶增加到狀態中
})
.addCase(addUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload.message;
});
},
});
export default adminSlice.reducer;
// 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;
Admin Product Slice (管理員產品切片)
// 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 () => {
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) => {
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 }) => {
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) => {
await axios.delete(`${API_URL}/api/admin/products/${id}`, {
headers: { Authorization: USER_TOKEN },
});
return id;
}
);
const adminProductSlice = createSlice({
name: "adminProducts",
initialState: {
products: [],
loading: false,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchAdminProducts.pending, (state) => {
state.loading = true;
})
.addCase(fetchAdminProducts.fulfilled, (state, action) => {
state.loading = false;
state.products = action.payload;
})
.addCase(fetchAdminProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
// Create Product - 新增產品
.addCase(createProduct.fulfilled, (state, action) => {
state.products.push(action.payload);
})
// Update Product - 更新產品
.addCase(updateProduct.fulfilled, (state, action) => {
const index = state.products.findIndex(
(product) => product._id === action.payload._id
);
if (index !== -1) {
state.products[index] = action.payload;
}
})
// Delete Product - 刪除產品
.addCase(deleteProduct.fulfilled, (state, action) => {
state.products = state.products.filter(
(product) => product._id !== action.payload
);
});
},
});
export default adminProductSlice.reducer;
// 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;
Admin Order Slice (管理員訂單切片)
// 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 }) => {
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 }) => {
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 }) => {
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: [],
totalOrders: 0,
totalSales: 0,
loading: false,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
// Fetch all orders - 獲取所有訂單
.addCase(fetchAllOrders.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchAllOrders.fulfilled, (state, action) => {
state.loading = false;
state.orders = action.payload;
state.totalOrders = action.payload.length;
// Calculate total sales - 計算總銷售額
const totalSales = action.payload.reduce((acc, order) => {
return acc + order.totalPrice;
}, 0);
state.totalSales = totalSales;
})
.addCase(fetchAllOrders.rejected, (state, action) => {
state.loading = false;
state.error = action.payload.message;
})
// Update order status - 更新訂單狀態
.addCase(updateOrderStatus.fulfilled, (state, action) => {
const updatedOrder = action.payload;
const orderIndex = state.orders.findIndex(
(order) => order._id === updatedOrder._id
);
if (orderIndex !== -1) {
state.orders[orderIndex] = updatedOrder;
}
})
// Delete order - 刪除訂單
.addCase(deleteOrder.fulfilled, (state, action) => {
state.orders = state.orders.filter(
(order) => order._id !== action.payload
);
});
},
});
export default adminOrderSlice.reducer;
// 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;