Skip to content

Commit

Permalink
Remove merch 2024 (#77)
Browse files Browse the repository at this point in the history
* Rename old merch components folder to merch_2023

* Add merch conditional rendering (WIP)

* Add function for merch is active or not

* Move merch types to data/types.ts

* Make cents mandatory for Price, change getAllPrices to return a map and use it in getAllProductsAndVariants

* Refactor merch.tsx getServerSideProps now that cents is guaranteed to be a member of Price

* Move 2024 MerchCard to its own file in components

* Fix imports for merch.tsx

* Fix cart.tsx import

* Fix pagination issue causing some sweaters (e.g. grey M/L) to show price $0 on cart

* Fix import

* Add merch closed messages to all relevant pages
  • Loading branch information
scorpiontornado authored Jul 24, 2024
1 parent 149335a commit 5d1647d
Show file tree
Hide file tree
Showing 18 changed files with 654 additions and 525 deletions.
28 changes: 28 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "yarn dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "yarn dev",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
}
}
]
}
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,10 @@
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true
"editor.formatOnSave": true,
"cSpell.words": [
"Merch",
"Subcom",
"subcoms"
]
}
6 changes: 5 additions & 1 deletion components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import LogoSmall from "public/img/brand/logo_small.png";
import NextNavbarBrand from "./link/NextNavbarBrand";
import NextNavLink from "./link/NextNavLink";
import NavIcon from "./navigation/NavIcon";
import { isMerchActive } from "scripts/merch";

// Supports both internal and external links, but internal links/redirects are preferred
const navLinks = [
Expand All @@ -53,10 +54,13 @@ const navLinks = [
["Publications", "/publications"],
["Charity", "/charity"],
["Calendar", "/calendar"],
["Merch", "/merch"],
// ["First Year FB", "/fb"],
];

if (isMerchActive()) {
navLinks.push(["Merch", "/merch"]);
}

const Navigation = () => {
const router = useRouter();
const [open, setOpen] = useState<boolean>(false);
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Modal, ModalBody } from "reactstrap";
import Image from "next/legacy/image";

import styles from "styles/modules/Merch.module.scss";
import { Product } from "../../data/types";
import { Product2023 } from "../../data/types";

interface MerchCardProps {
productData: Product;
addToCart: (value: Product) => void;
productData: Product2023;
addToCart: (value: Product2023) => void;
}

const MerchCard = ({ productData, addToCart }: MerchCardProps) => {
Expand Down
File renamed without changes.
File renamed without changes.
288 changes: 288 additions & 0 deletions components/merch_2024/MerchCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import React, { useEffect, useState } from "react";
import Image from "next/legacy/image";

import {
Cart,
Product,
ProductColour,
ProductSize,
Variant,
} from "../../data/types";
import {
CarouselItem,
Card,
Carousel,
CarouselControl,
Container,
Row,
Dropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
Button,
} from "reactstrap";

const MerchCard = ({
product,
setCart,
isInCart,
findVariantID,
findAllVariantsOfProduct,
}: {
product: Product;
setCart: React.Dispatch<React.SetStateAction<Cart>>;
isInCart: (productName: string) => boolean;
findVariantID: (
productName: string,
colour: ProductColour,
size: ProductSize,
) => string | undefined;
findAllVariantsOfProduct: (productName: string) => Variant[];
}) => {
const [colourChoice, setColourChoice] = useState<ProductColour>(
ProductColour.UNKNOWN,
);
const [colourDropdownOpen, setColourDropdownOpen] = useState(false);
const [sizeChoice, setSizeChoice] = useState<ProductSize>(
ProductSize.UNKNOWN,
);
const [sizeDropdownOpen, setSizeDropdownOpen] = useState(false);
const [qtyChoice, setQtyChoice] = useState(0);
const [qtyDropdownOpen, setQtyDropdownOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState("");

const toggleColourDropdown = () => setColourDropdownOpen(!colourDropdownOpen);
const toggleSizeDropdown = () => setSizeDropdownOpen(!sizeDropdownOpen);
const toggleQtyDropdown = () => setQtyDropdownOpen(!qtyDropdownOpen);

const [carouselImages, setCarouselImages] = useState<string[]>([]);
const [carouselIndex, setCarouselIndex] = useState(0);
const [carouselAnimating, setCarouselAnimating] = useState(false);

const carouselNext = () => {
if (carouselAnimating) return;
const nextIndex =
carouselIndex === carouselImages.length - 1 ? 0 : carouselIndex + 1;
setCarouselIndex(nextIndex);
};

const carouselPrevious = () => {
if (carouselAnimating) return;
const nextIndex =
carouselIndex === 0 ? carouselImages.length - 1 : carouselIndex - 1;
setCarouselIndex(nextIndex);
};

const getVariantID = () => {
const variantID = findVariantID(product.name, colourChoice, sizeChoice);
if (!variantID) {
if (colourChoice === ProductColour.UNKNOWN) {
setErrorMessage("Please select a colour.");
} else if (sizeChoice === ProductSize.UNKNOWN) {
setErrorMessage("Please select a size.");
} else {
setErrorMessage(
"Sorry, looks like your colour and size is not available. Please try another combination.",
);
}

return undefined;
}

return variantID;
};

const displayPrice = (cents: number | undefined) => {
if (!cents) return "Price not available.";
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "AUD",
});
return formatter.format((cents * 1.0) / 100);
};

const addToCart = () => {
const variantID = getVariantID();
if (!variantID) return;

if (qtyChoice === 0) {
setErrorMessage("Please select a quantity.");
return;
}

setCart((prevCart) => {
const newCart = new Map(prevCart); // must clone the map to correctly set state
newCart.forEach((_, key) => {
if (key.split("-")[0] === variantID.split("-")[0]) {
newCart.delete(key);
}
});
newCart.set(variantID, qtyChoice);
return newCart;
});
setErrorMessage("");
};

const removeFromCart = () => {
const variantID = getVariantID();
if (!variantID) return;

setCart((prevCart) => {
prevCart.delete(variantID);
return prevCart;
});
setQtyChoice(0);
setErrorMessage("");
};

useEffect(() => {
const imageURLs = findAllVariantsOfProduct(product.name)
.flatMap((variant) => variant.imageURLs)
.map((url) => url.replace(".png", ".jpg"));
console.log(imageURLs);
const dedupedImageURLs = imageURLs.filter(
(value, index) => imageURLs.indexOf(value) === index,
);

setCarouselImages(dedupedImageURLs);
}, []);

const displayAllVariantImages = () => {
return carouselImages.map((url) => {
const path = url.replace("https%3A//www.coopsoc.com.au", "");
return (
<CarouselItem
onExiting={() => setCarouselAnimating(true)}
onExited={() => setCarouselAnimating(false)}
key={path.split("/").at(-1)}
>
<div
style={{
position: "relative",
width: "100%",
paddingTop: "100%",
}}
>
<Image
src={path}
alt={path.split("/").at(-1) ?? "merch item"}
layout="fill"
sizes="(max-width 575px) 100vw, (max-width: 767px) 33vw, 25vw"
// By default, quality = 75 (ranges from 0-100). For now, just converted PNG -> JPEG instead
// quality="40"
/>
</div>
</CarouselItem>
);
});
};

return (
<Card className="m-3">
<Carousel
activeIndex={carouselIndex}
next={carouselNext}
previous={carouselPrevious}
dark
>
{displayAllVariantImages()}
<CarouselControl direction="prev" onClickHandler={carouselPrevious} />
<CarouselControl direction="next" onClickHandler={carouselNext} />
</Carousel>
<Container className="p-3">
<h3>{product.name}</h3>
<p>{displayPrice(product.price.cents)}</p>
<p>{product.description}</p>
<Row className="gap-3 my-2 my-md-3">
<Dropdown
direction="down"
isOpen={colourDropdownOpen}
toggle={toggleColourDropdown}
>
<DropdownToggle caret>
{colourChoice === ProductColour.UNKNOWN ? "Colour" : colourChoice}
</DropdownToggle>
<DropdownMenu>
{colourChoice !== ProductColour.UNKNOWN && (
<DropdownItem
key={ProductColour.UNKNOWN}
onClick={() => setColourChoice(ProductColour.UNKNOWN)}
>
Unselect colour
</DropdownItem>
)}
{product.colours.map((colour) => (
<DropdownItem
key={colour}
onClick={() => setColourChoice(colour)}
>
{colour}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<Dropdown
direction="down"
isOpen={sizeDropdownOpen}
toggle={toggleSizeDropdown}
>
<DropdownToggle caret>
{sizeChoice === ProductSize.UNKNOWN ? "Size" : sizeChoice}
</DropdownToggle>
<DropdownMenu>
{sizeChoice !== ProductSize.UNKNOWN && (
<DropdownItem
key={ProductSize.UNKNOWN}
onClick={() => setSizeChoice(ProductSize.UNKNOWN)}
>
Unselect size
</DropdownItem>
)}
{product.sizes.map((size) => (
<DropdownItem key={size} onClick={() => setSizeChoice(size)}>
{size}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<Dropdown
isOpen={qtyDropdownOpen}
toggle={toggleQtyDropdown}
// disabled={qtyBtnDisabled}
>
<DropdownToggle caret>
{qtyChoice === 0 ? "Qty" : qtyChoice}
</DropdownToggle>
<DropdownMenu>
{[1, 2, 3, 4, 5].map((qty) => (
<DropdownItem key={qty} onClick={() => setQtyChoice(qty)}>
{qty}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
</Row>
<Row className="p-2 d-flex justify-content-center align-items-center flex-col">
<Button className="m-1 bg-green" onClick={() => addToCart()}>
{isInCart(product.name) ? "Update cart" : "Add to cart"}
</Button>
{isInCart(product.name) && (
<Button
className="m-1 bg-warning text-white"
onClick={() => removeFromCart()}
>
Remove from cart
</Button>
)}
</Row>
{errorMessage && (
<Row style={{ textAlign: "center" }}>
<p style={{ color: "#ba3232" }}>{errorMessage}</p>
</Row>
)}
</Container>
</Card>
);
};

export default MerchCard;
15 changes: 15 additions & 0 deletions components/merch_2024/MerchClosed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Row, Col, Container } from "reactstrap";

const MerchClosed = () => (
<Container className="container-md">
<Row className="justify-content-center text-center">
<Col lg="10">
<p className="lead text-muted">
Orders are now closed - check back next year for more merch!
</p>
</Col>
</Row>
</Container>
);

export default MerchClosed;
Loading

0 comments on commit 5d1647d

Please sign in to comment.