Skip to content

Commit

Permalink
task: Port home page (product listing)
Browse files Browse the repository at this point in the history
  • Loading branch information
YoNG-Zaii committed Mar 11, 2023
1 parent 3c9b8c6 commit 689aa31
Show file tree
Hide file tree
Showing 20 changed files with 842 additions and 1 deletion.
144 changes: 144 additions & 0 deletions apps/web/lib/merch/context/cart/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React, { useEffect, useReducer, useMemo, useContext } from "react";
import { CartStateType, CartItemType } from "../../typings/cart";

type ContextType = {
state: CartStateType;
dispatch: React.Dispatch<any>;
} | null;

export enum CartActionType {
RESET_CART = "RESET_CART",
INITALIZE = "initialize",
ADD_ITEM = "add_item",
UPDATE_QUANTITY = "update_quantity",
REMOVE_ITEM = "remove_item",
VALID_VOUCHER = "valid_voucher",
REMOVE_VOUCHER = "remove_voucher",
UPDATE_NAME = "update_name",
UPDATE_BILLING_EMAIL = "update_billing_email",
}

export type CartAction =
| {
type: CartActionType.RESET_CART;
}
| {
type: CartActionType.INITALIZE;
payload: CartStateType;
}
| { type: CartActionType.ADD_ITEM; payload: CartItemType }
| {
type: CartActionType.UPDATE_QUANTITY;
payload: { productId: string; size: string; quantity: number };
}
| { type: CartActionType.REMOVE_ITEM; payload: { productId: string; size: string } }
| { type: CartActionType.VALID_VOUCHER; payload: string }
| { type: CartActionType.REMOVE_VOUCHER; payload: null }
| { type: CartActionType.UPDATE_NAME; payload: string }
| { type: CartActionType.UPDATE_BILLING_EMAIL; payload: string };

const CartContext = React.createContext<ContextType>(null);

const initState: CartStateType = {
items: [],
voucher: "",
name: "",
billingEmail: "",
};

export const cartReducer = (state: CartStateType, action: CartAction) => {
switch (action.type) {
case CartActionType.RESET_CART: {
return JSON.parse(JSON.stringify(initState));
}
case CartActionType.INITALIZE: {
return { ...state, ...action.payload };
}
case CartActionType.ADD_ITEM: {
// Find if there's an existing item already:
const { productId, size, quantity } = action.payload;
const idx = state.items.findIndex((x) => x.productId === productId && x.size === size);
const newQuantity = Math.min((state?.items[idx]?.quantity ?? 0) + quantity, 99);
return {
...state,
items:
idx === -1
? [...state.items, action.payload]
: [
...state.items.slice(0, idx),
{ ...state.items[idx], quantity: newQuantity },
...state.items.slice(idx + 1),
],
};
}

case CartActionType.UPDATE_QUANTITY: {
const { productId, size, quantity } = action.payload;
const idx = state.items.findIndex((x) => x.productId === productId && x.size === size);
return {
...state,
items:
idx === -1
? [...state.items]
: [...state.items.slice(0, idx), { ...state.items[idx], quantity }, ...state.items.slice(idx + 1)],
};
}
case CartActionType.REMOVE_ITEM: {
const { productId, size } = action.payload;
return {
...state,
items: [...state.items.filter((x) => !(x.productId === productId && x.size === size))],
};
}

case CartActionType.VALID_VOUCHER: {
return { ...state, voucher: action.payload };
}

case CartActionType.REMOVE_VOUCHER: {
return { ...state, voucher: "" };
}

case CartActionType.UPDATE_NAME: {
return { ...state, name: action.payload };
}

case CartActionType.UPDATE_BILLING_EMAIL: {
return { ...state, billingEmail: action.payload };
}

default: {
throw new Error(`Unhandled action type - ${JSON.stringify(action)}`);
}
}
};

export const useCartStore = () => {
const context = useContext(CartContext);
if (context === null) {
throw new Error("useCardStore must be used within a CartProvider.");
}
return context;
};

const initStorageCart: CartStateType = { voucher: "", name: "", billingEmail: "", items: [] };

export const CartProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, initState);
const value = useMemo(() => ({ state, dispatch }), [state]);

useEffect(() => {
const cartState: CartStateType = JSON.parse(JSON.stringify(initState));
const storedCartData: CartStateType = JSON.parse(localStorage.getItem("cart") as string) ?? initStorageCart;
cartState.items = storedCartData.items;
cartState.name = storedCartData.name;
cartState.billingEmail = storedCartData.billingEmail;
dispatch({ type: CartActionType.INITALIZE, payload: cartState });
}, []);

useEffect(() => {
localStorage.setItem("cart", JSON.stringify(state));
}, [state]);

return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};
113 changes: 113 additions & 0 deletions apps/web/lib/merch/services/api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { CartItemType } from "../typings/cart";
import { fakeDelay } from "../utils/functions/random";
import { ProductType } from "../typings/product";

const QUERY_DELAY_TIME = 1000;

export class Api {
private API_ORIGIN: string;

constructor() {
if (!process.env.REACT_APP_API_ORIGIN) {
throw new Error("API_ORIGIN environment variable is not set")
}
this.API_ORIGIN = process.env.REACT_APP_API_ORIGIN || "";
}

// http methods
async get(urlPath: string): Promise<Record<string, any>> {
const response = await fetch(`${this.API_ORIGIN}${urlPath}`);
return response.json();
}

// eslint-disable-next-line class-methods-use-this
async post(urlPath: string, data: any): Promise<any> {
const response = await fetch(`${this.API_ORIGIN}${urlPath}`, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
// 'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: "follow", // manual, *follow, error
referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify(data), // body data type must match
});
return response.json();
}

// eslint-disable-next-line class-methods-use-this
async getProducts(): Promise<ProductType[]> {
try {
const res = await this.get("/products");
console.log("product-list", res);
return res?.products ?? [];
} catch (e: any) {
throw new Error(e);
}
}

// eslint-disable-next-line class-methods-use-this
async getProduct(productId: string) {
try {
const res = await this.get(`/products/${productId}`);
console.log("product res", res);
return res;
} catch (e: any) {
throw new Error(e);
}
}

async getOrder(userId: string, orderId: string) {
try {
const res = await this.get(`/orders/${orderId}`);
console.log("Order Summary response:", res);
return res;
} catch (e: any) {
throw new Error(e);
}
}

async getOrderHistory(userId: string) {
try {
const res = await this.get(`/orders/${userId}`);
console.log("Order Summary response:", res);
return res.json();
} catch (e: any) {
throw new Error(e);
}
}

async postCheckoutCart(
items: CartItemType[],
email: string,
promoCode: string | null
) {
try {
const res = await this.post(`/cart/checkout`, {
items,
promoCode: promoCode ?? "",
email,
});
return res;
} catch (e: any) {
throw new Error(e);
}
}

async postQuotation(items: CartItemType[], promoCode: string | null) {
try {
const res = await this.post(`/cart/quotation`, {
items,
promoCode: promoCode ?? "",
});
return res;
} catch (e: any) {
throw new Error(e);
}
}
}

export const api = new Api();
80 changes: 80 additions & 0 deletions apps/web/lib/merch/typings/cart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
export type CartItemType = {
productId: string;
size: string;
colorway: string;
quantity: number;
};

/**
* @Title CartItemType
* @description Displayed on FE in shopping-cart
*/

export type CartStateType = {
voucher: string | null;
items: CartItemType[];
name: string;
billingEmail: string;
};
export type ProductInfoType = {
name: string;
image: string;
price: number;
};

export type CartPriceType = {
currency: string;
subtotal: number;
discount: number;
grandTotal: number;
};

export type CartResponseDto = {
items: [
{
id: string;
name: string;
price: number;
images: string[];
sizes: string;
productCategory: string;
isAvailable: boolean;
quantity: number;
}
];
price: {
currency: string;
subtotal: number;
discount: number;
grandTotal: number;
};
};

export type CheckoutResponseDto = {
orderId: string;
items: [
{
id: string;
name: string;
price: number;
images: string[];
sizes: string[];
productCategory: string;
isAvailable: boolean;
quantity: number;
}
];
price: {
currency: string;
subtotal: number;
discount: number;
grandTotal: number;
};
payment: {
paymentGateway: string;
clientSecret: string;
};
email: string;
};

export type ProductInfoMapType = Record<string, ProductInfoType>;
34 changes: 34 additions & 0 deletions apps/web/lib/merch/typings/order.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// eslint-disable-next-line no-shadow
export enum OrderStatusType {
RECEIVED,
PROCESSING,
READY_TO_COLLECT,
DELAY,
COLLECTED,
}

export type OrderItemType = {
id: string;
image: string;
size: string;
colorway: string;
price: number;
quantity: number;
name: string;
};

export type OrderBillingType = {
total: number;
subtotal: number;
appliedVoucher?: null;
};

export type OrderType = {
userId: string;
orderID: string;
orderItems: OrderItemType[];
status: OrderStatusType;
billing: OrderBillingType;
orderDateTime: string | Date;
lastUpdate: string | Date | undefined;
};
14 changes: 14 additions & 0 deletions apps/web/lib/merch/typings/product.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type ProductCategoryType = string;

export type ProductType = {
id: string;
name: string;
price: number;
stock: { [colorway: string]: { [sizeIndex: string]: number } }; // stock[colorway][size] = qty
sizes: string[];
sizeChart: string;
colorways: string[];
images?: string[];
productCategory?: ProductCategoryType;
isAvailable: boolean;
};
Loading

0 comments on commit 689aa31

Please sign in to comment.