aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Joe Banks <[email protected]>2024-08-25 04:04:47 +0100
committerGravatar Joe Banks <[email protected]>2024-08-25 04:04:47 +0100
commitd385e73d49b1f78d3187aca692b55a1b3161c5aa (patch)
tree1cd0d5e63a5e0336c7db702990964b0f39bd98f0
parentExtract button to reusable component (diff)
Add an item displayed in the store
-rw-r--r--thallium-frontend/src/components/StoreItem.tsx277
1 files changed, 277 insertions, 0 deletions
diff --git a/thallium-frontend/src/components/StoreItem.tsx b/thallium-frontend/src/components/StoreItem.tsx
new file mode 100644
index 0000000..c0aeed1
--- /dev/null
+++ b/thallium-frontend/src/components/StoreItem.tsx
@@ -0,0 +1,277 @@
+import { useEffect, useMemo, useState } from "react";
+import styled from "styled-components";
+import { Template, Variant } from "../api/templates";
+import Button from "./forms/Button";
+
+interface StoreItemProps {
+ template: Template;
+}
+
+const StoreItemContainer = styled.div`
+ padding: 1rem;
+ box-shadow: 10px 10px 0 ${({ theme }) => theme.cardShadow};
+
+ border: 4px solid ${({ theme }) => theme.borderColor};
+ margin: 1rem 0;
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ h3 {
+ margin: 0;
+ }
+
+ img {
+ width: 75%;
+ border-radius: 0.5rem;
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+ }
+`;
+
+const ColourSwatch = styled.div<{ $colour: string, $selected: boolean }>`
+ width: 1.5rem;
+ height: 1.5rem;
+ background-color: ${({ $colour }) => $colour};
+ border-radius: 1rem;
+ display: inline-block;
+ margin: 0 0.5rem;
+ outline: 2px solid ${({ $selected, theme }) => $selected ? theme.textColor : "transparent"};
+ outline-offset: 2px;
+ transform: ${({ $selected }) => $selected ? "scale(1.05)" : "scale(1)"};
+ transition: transform 0.2s, outline 0.2s;
+
+ &:hover {
+ cursor: pointer;
+ }
+`;
+
+const TwoToneSwatch = styled(ColourSwatch) <{ $colour2: string }>`
+ background: linear-gradient(-45deg, ${({ $colour }) => $colour} 50%, ${({ $colour2 }) => $colour2} 50%);
+`;
+
+const SizeHolder = styled.div`
+margin-top: 1rem;
+`;
+
+const SizeButton = styled.button<{ $selected: boolean }>`
+border: none;
+outline: none;
+height: 2rem;
+min-width: 40px;
+font-family: inherit;
+background-color: ${({ theme, $selected }) => $selected ? "white" : theme.accent};
+border: 3px solid ${({ theme }) => theme.accent};
+${({ theme }) => theme.selectedTheme == "dark" ? "border: none" : ""};
+color: ${({ theme, $selected }) => $selected ? theme.accent : "white"};
+margin-right: 1px;
+margin-left: 1px;
+margin-top: 5px;
+font-size: 1.1em;
+font-weight: 600;
+transition: background-color 0.5s, color 0.5s;
+
+&:hover {
+ filter: brightness(0.8);
+ cursor: pointer;
+}
+
+&:first-child {
+ margin-left: 0;
+}
+
+&:last-child {
+ margin-right: 0;
+}
+`;
+
+const CartButton = styled(Button)`
+margin-top: 1rem;
+`;
+
+
+const StoreItem: React.FC<StoreItemProps> = ({ template }: StoreItemProps) => {
+ let colours = 0;
+
+ template.variants?.forEach((variant) => {
+ if (variant.colour_code2) {
+ colours = 2;
+ } else if (variant.colour_code) {
+ colours = 1;
+ }
+ });
+
+ const { uniqueColours, uniqueSizes, colourToSizes, initialColour } = useMemo(() => {
+
+ const uniqueColours: string[] = [];
+
+ const uniqueSizes: string[] = [];
+
+ const colourToSizes = new Map<string, string[]>();
+
+
+ let initialColour = null;
+
+ if (colours === 1) {
+ template.variants?.forEach((variant) => {
+ if (!variant.colour_code) {
+ return;
+ }
+
+ if (!uniqueColours.includes(variant.colour_code)) {
+ uniqueColours.push(variant.colour_code);
+ }
+ });
+
+ if (template.variants?.[0].colour_code) {
+ initialColour = template.variants[0].colour_code;
+ }
+ } else if (colours === 2) {
+ template.variants?.forEach((variant) => {
+ if (!variant.colour_code || !variant.colour_code2) {
+ return;
+ }
+
+ if (!uniqueColours.includes(`${variant.colour_code}-${variant.colour_code2}`)) {
+ uniqueColours.push(`${variant.colour_code}-${variant.colour_code2}`);
+ }
+ });
+
+ if (template.variants?.[0].colour_code && template.variants[0].colour_code2) {
+ initialColour = `${template.variants[0].colour_code}-${template.variants[0].colour_code2}`;
+ }
+ }
+
+ template.variants?.forEach((variant) => {
+ if (!uniqueSizes.includes(variant.size)) {
+ uniqueSizes.push(variant.size);
+ }
+
+ if (colours === 1) {
+ if (!variant.colour_code) {
+ return;
+ }
+
+ if (!colourToSizes.has(variant.colour_code)) {
+ colourToSizes.set(variant.colour_code, []);
+ }
+
+ colourToSizes.get(variant.colour_code)?.push(variant.size);
+ } else if (colours === 2) {
+ if (!variant.colour_code || !variant.colour_code2) {
+ return;
+ }
+
+ if (!colourToSizes.has(`${variant.colour_code}-${variant.colour_code2}`)) {
+ colourToSizes.set(`${variant.colour_code}-${variant.colour_code2}`, []);
+ }
+
+ colourToSizes.get(`${variant.colour_code}-${variant.colour_code2}`)?.push(variant.size);
+ }
+ });
+
+ return {
+ uniqueColours,
+ uniqueSizes,
+ colourToSizes,
+ initialColour,
+ };
+
+ }, [template, colours]);
+
+
+ const [selectedColour, setSelectedColour] = useState<string | null>(initialColour);
+ const [selectedSize, setSelectedSize] = useState<string | null>(uniqueSizes.length === 1 ? uniqueSizes[0] : null);
+ const [availableSizes, setAvailableSizes] = useState<string[]>(uniqueSizes);
+
+ const [selectedVariant, setSelectedVariant] = useState<Variant | null>(null);
+
+
+ useEffect(() => {
+ if (selectedColour === null) {
+ setAvailableSizes(uniqueSizes);
+ } else {
+ setAvailableSizes(colourToSizes.get(selectedColour) ?? []);
+ }
+ }, [selectedColour, colourToSizes, uniqueSizes]);
+
+ useEffect(() => {
+ if ((selectedColour === null && colours > 0) || selectedSize === null) {
+ setSelectedVariant(null);
+ return;
+ }
+
+ setSelectedVariant(template.variants?.find((variant) => {
+ if (colours === 1) {
+ return variant.colour_code === selectedColour && variant.size === selectedSize;
+ } else if (colours === 2) {
+ if (!variant.colour_code || !variant.colour_code2) {
+ return false;
+ }
+
+ return `${variant.colour_code}-${variant.colour_code2}` === selectedColour && variant.size === selectedSize;
+ } else {
+ return variant.size === selectedSize;
+ }
+ }) ?? null);
+ }, [colours, selectedColour, selectedSize, template.variants]);
+
+ return (
+ <StoreItemContainer>
+ <h3>{template.title}</h3>
+ <img src={template.mockup_file_url} alt={template.title} />
+
+ {colours === 1 && (
+ <div>
+ {uniqueColours.map((colour) => (
+ <ColourSwatch
+ key={colour}
+ $colour={colour}
+ $selected={selectedColour === colour}
+ onClick={() => { setSelectedColour(colour); }}
+ />
+ ))}
+ </div>
+ )}
+
+ {colours === 2 && (
+ <div>
+ {uniqueColours.map((colour) => {
+ const [colour1, colour2] = colour.split("-");
+
+ return (
+ <TwoToneSwatch
+ key={colour}
+ $colour={colour1}
+ $colour2={colour2}
+ $selected={selectedColour === colour}
+ onClick={() => { setSelectedColour(colour); }}
+ />
+ );
+ })}
+ </div>
+ )}
+
+ <SizeHolder>
+ {availableSizes.map((size) => (
+ <SizeButton
+ key={size}
+ $selected={selectedSize === size}
+ onClick={() => { setSelectedSize(size); }}
+ >{size}</SizeButton>
+ ))}
+ </SizeHolder>
+
+ {selectedVariant && (
+ <p>${selectedVariant.price} USD</p>
+ )}
+
+ <CartButton
+ disabled={selectedVariant === null}
+ >Add to Cart</CartButton>
+ </StoreItemContainer>
+ );
+};
+
+export default StoreItem;