-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
task: Port home page (product listing)
- Loading branch information
Showing
15 changed files
with
562 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export enum QueryKeys { | ||
PRODUCTS = "PRODUCTS", | ||
PRODUCT = "PRODUCT", | ||
VOUCHER = "VOUCHER", | ||
ORDER = "ORDER", | ||
ORDERS = "ORDERS", | ||
EMAIL = "EMAIL", | ||
CHECKOUT = "CHECKOUT", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
type Routes = { | ||
HOME: string; | ||
PRODUCT: string; | ||
CART: string; | ||
CHECKOUT: string; | ||
ORDER_SUMMARY: string; | ||
}; | ||
|
||
export const routes: Routes = { | ||
HOME: "/merch", | ||
PRODUCT: "/merch/product", | ||
CART: "/merch/cart", | ||
CHECKOUT: "/merch/checkout", | ||
ORDER_SUMMARY: "/merch/order-summary", | ||
}; | ||
|
||
export default routes; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const displayPrice = (amountInCents: number): string => { | ||
const amountStr = amountInCents.toString() | ||
const dollarStr = amountStr.slice(0, -2).padStart(1, '0') | ||
const centStr = amountStr.slice(-2).padStart(2, '0') | ||
const priceStr = dollarStr.concat(".").concat(centStr) | ||
return `$${priceStr}` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { Product } from "types/lib/merch" | ||
|
||
/* | ||
export const getQtyInStock = (product: ProductType, colorway: string, size: string): number => { | ||
// returns remaining stock for specified colorway and size | ||
if (product.stock[colorway] && product.stock[colorway][size]) { | ||
return product.stock[colorway][size] | ||
} | ||
return 0; | ||
} | ||
export const displayStock = (product: ProductType, colorway: string, size: string): string => { | ||
// returns string describing remaining stock | ||
if (product.stock[colorway] && product.stock[colorway][size]) { | ||
const qty = product.stock[colorway][size]; | ||
if (qty > 0) { | ||
return `${qty} available`; | ||
} | ||
return "OUT OF STOCK"; | ||
} | ||
return "ERROR: invalid color/size selected"; | ||
} | ||
*/ | ||
export const isOutOfStock = (product: Product): boolean => { | ||
// returns true if product is out of stock in all colorways and sizes | ||
const totalQty = Object.values(product.stock).reduce((acc, stockByColor)=>{ | ||
const colorQty = Object.values(stockByColor).reduce((acc2, qty)=>acc2+qty, 0); | ||
return acc+colorQty | ||
}, 0); | ||
return totalQty <= 0; | ||
|
||
} | ||
|
||
/* | ||
export const isColorwayAvailable = (product: ProductType, colorway: string): boolean => { | ||
// returns true if colorway is available in any size | ||
// returns false if colorway is out of stock in all sizes | ||
const colorwayStock = Object.values(product.stock[colorway]).reduce( | ||
(acc: any, size: any)=>acc+size, 0 | ||
); | ||
return (colorwayStock > 0); | ||
} | ||
export const isSizeAvailable = (product: ProductType, size: string): boolean => { | ||
// returns true if size is available in any colorway | ||
// returns false if size is out of stock in all colorways | ||
const sizeStock = Object.values(product.stock).map(d => d[size]||0); | ||
const totalQty = sizeStock.reduce((a, b) => { | ||
return a + b; | ||
}, 0); | ||
return (totalQty > 0); | ||
} | ||
export const getDefaultSize = (product: ProductType): string => { | ||
const index = product.sizes.findIndex((size) => isSizeAvailable(product, size)); | ||
if (index !== -1) { | ||
return product.sizes[index]; | ||
} | ||
return ""; | ||
} | ||
export const getDefaultColorway = (product: ProductType, size: string): string => { | ||
const sizeIndex = product.sizes.indexOf(size); | ||
if (sizeIndex === -1) { // no such size | ||
return ""; | ||
} | ||
const colorwayStock = Object.values(product.stock).map(d => d[sizeIndex]); | ||
const availColorwayIndex = colorwayStock.map((qty, idx) => qty > 0 ? idx : -1).filter(idx => idx !== -1); | ||
if (availColorwayIndex.length > 0) { | ||
return product.colorways[availColorwayIndex[0]]; | ||
} | ||
return ""; | ||
} | ||
export const getDefaults = (product: ProductType): [string, string] => { | ||
const size = getDefaultSize(product); | ||
const colorway = getDefaultColorway(product, size); | ||
return [colorway, size]; | ||
} | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { Product } from "types/lib/merch"; | ||
|
||
export class Api { | ||
private API_ORIGIN: string; | ||
|
||
constructor() { | ||
if (!process.env.NEXT_PUBLIC_MERCH_API_ORIGIN) { | ||
throw new Error("NEXT_PUBLIC_MERCH_API_ORIGIN environment variable is not set") | ||
} | ||
this.API_ORIGIN = process.env.NEXT_PUBLIC_MERCH_API_ORIGIN || ""; | ||
} | ||
|
||
// http methods | ||
async get(urlPath: string): Promise<Record<string, Product[]>> { | ||
const response = await fetch(`${this.API_ORIGIN}${urlPath}`); | ||
const convert = response.json() as unknown; // Convert to unknown type | ||
return convert as Record<string, Product[]> | ||
} | ||
|
||
/* | ||
// 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<Product[]> { | ||
try { | ||
const res = await this.get("/products"); | ||
console.log("product-list", res); | ||
return res?.products ?? []; | ||
} catch (e) { | ||
if(e instanceof Error){ | ||
throw new Error(e.message); | ||
} | ||
return [] | ||
} | ||
} | ||
|
||
/* | ||
// 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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import React, { useState } from "react"; | ||
import { Flex, Divider, Select, Heading, Grid } from "@chakra-ui/react"; | ||
import { useQuery } from "@tanstack/react-query"; | ||
import Card from "ui/components/merch/Card"; | ||
import Page from "ui/components/merch/Page"; | ||
import { QueryKeys } from "../../features/merch/constants/queryKeys"; | ||
import { api } from "../../features/merch/services/api"; | ||
import { Product } from "types/lib/merch"; | ||
import ProductListSkeleton from "ui/components/merch/Skeleton"; | ||
import { isOutOfStock } from "../../features/merch/functions/stock"; | ||
|
||
const MerchandiseList = () => { | ||
const [selectedCategory, setSelectedCategory] = useState<string>(""); | ||
|
||
const { data: products, isLoading } = useQuery([QueryKeys.PRODUCTS], () => api.getProducts(), {}); | ||
|
||
const categories = products?.map((product: Product) => product?.category); | ||
const uniqueCategories = categories | ||
?.filter((c, idx) => categories.indexOf(c) === idx) | ||
.filter(Boolean); | ||
|
||
const handleCategoryChange = (event: React.ChangeEvent<HTMLSelectElement>) => { | ||
setSelectedCategory(event.target.value); | ||
}; | ||
|
||
return ( | ||
<Page> | ||
<Flex justifyContent="space-between" my={5} alignItems="center"> | ||
<Heading fontSize={["md", "2xl"]} textColor={["primary.600", "black"]}> | ||
New Drop | ||
</Heading> | ||
<Select | ||
bgColor={["white", "gray.100"]} | ||
w="fit-content" | ||
textAlign="center" | ||
alignSelf="center" | ||
placeholder="All Product Type" | ||
size="sm" | ||
disabled={isLoading} | ||
value={selectedCategory} | ||
onChange={handleCategoryChange} | ||
> | ||
{uniqueCategories?.map((category, idx) => ( | ||
<option key={idx.toString()} value={category}> | ||
{category} | ||
</option> | ||
))} | ||
</Select> | ||
</Flex> | ||
<Divider borderColor="blackAlpha.500" mt={[5, 10]} /> | ||
{isLoading ? ( | ||
<ProductListSkeleton /> | ||
) : ( | ||
<Grid | ||
templateColumns={{ base: "repeat(2, 1fr)", md: "repeat(3, 1fr)", lg: "repeat(4, 1fr)" }} | ||
columnGap={4} | ||
rowGap={2} | ||
> | ||
{products | ||
?.filter((product: Product) => { | ||
if (!product?.is_available) return false; | ||
if (selectedCategory === "") return true; | ||
return product?.category === selectedCategory; | ||
}) | ||
?.map((item: Product, idx: number) => ( | ||
<Card | ||
_productId={item.id} | ||
key={idx.toString()} | ||
text={item?.name} | ||
price={item?.price} | ||
imgSrc={item?.images?.[0]} | ||
sizeRange={`${item?.sizes?.[0]} - ${item.sizes?.[item.sizes.length - 1]}`} | ||
isOutOfStock={isOutOfStock(item)} | ||
/> | ||
))} | ||
</Grid> | ||
)} | ||
</Page> | ||
); | ||
}; | ||
|
||
export default MerchandiseList; |
Oops, something went wrong.