Skip to content

Commit a29fd96

Browse files
authored
Merge pull request #10 from bc-chaz/step-5-big-design-part-2
Step 5 big design part 2
2 parents fc95bdd + cef62c1 commit a29fd96

File tree

12 files changed

+320
-31
lines changed

12 files changed

+320
-31
lines changed

components/error.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { H3, Panel } from '@bigcommerce/big-design';
2+
3+
interface ErrorMessageProps {
4+
error?: Error;
5+
}
6+
7+
const ErrorMessage = ({ error }: ErrorMessageProps) => (
8+
<Panel>
9+
<H3>Failed to load</H3>
10+
{error && error.message}
11+
</Panel>
12+
);
13+
14+
export default ErrorMessage;

components/form.tsx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { Button, Checkbox, Flex, FormGroup, Input, Panel, Select, Form as StyledForm, Textarea } from '@bigcommerce/big-design';
2+
import { ChangeEvent, FormEvent, useState } from 'react';
3+
import { FormData, StringKeyValue } from '../types';
4+
5+
interface FormProps {
6+
formData: FormData;
7+
onCancel(): void;
8+
onSubmit(form: FormData): void;
9+
}
10+
11+
const FormErrors = {
12+
name: 'Product name is required',
13+
price: 'Default price is required',
14+
};
15+
16+
const Form = ({ formData, onCancel, onSubmit }: FormProps) => {
17+
const { description, isVisible, name, price, type } = formData;
18+
const [form, setForm] = useState<FormData>({ description, isVisible, name, price, type });
19+
const [errors, setErrors] = useState<StringKeyValue>({});
20+
21+
const handleChange = (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
22+
const { name: formName, value } = event?.target;
23+
setForm(prevForm => ({ ...prevForm, [formName]: value }));
24+
25+
// Add error if it exists in FormErrors and the input is empty, otherwise remove from errors
26+
!value && FormErrors[formName]
27+
? setErrors(prevErrors => ({ ...prevErrors, [formName]: FormErrors[formName] }))
28+
: setErrors(({ [formName]: removed, ...prevErrors }) => ({ ...prevErrors }));
29+
};
30+
31+
const handleSelectChange = (value: string) => {
32+
setForm(prevForm => ({ ...prevForm, type: value }));
33+
};
34+
35+
const handleCheckboxChange = (event: ChangeEvent<HTMLInputElement>) => {
36+
const { checked, name: formName } = event?.target;
37+
setForm(prevForm => ({ ...prevForm, [formName]: checked }));
38+
};
39+
40+
const handleSubmit = (event: FormEvent<EventTarget>) => {
41+
event.preventDefault();
42+
43+
// If there are errors, do not submit the form
44+
const hasErrors = Object.keys(errors).length > 0;
45+
if (hasErrors) return;
46+
47+
onSubmit(form);
48+
};
49+
50+
return (
51+
<StyledForm onSubmit={handleSubmit}>
52+
<Panel header="Basic Information">
53+
<FormGroup>
54+
<Input
55+
error={errors?.name}
56+
label="Product name"
57+
name="name"
58+
required
59+
value={form.name}
60+
onChange={handleChange}
61+
/>
62+
</FormGroup>
63+
<FormGroup>
64+
<Select
65+
label="Product type"
66+
name="type"
67+
options={[
68+
{ value: 'physical', content: 'Physical' },
69+
{ value: 'digital', content: 'Digital' }
70+
]}
71+
required
72+
value={form.type}
73+
onOptionChange={handleSelectChange}
74+
/>
75+
</FormGroup>
76+
<FormGroup>
77+
<Input
78+
error={errors?.price}
79+
iconLeft={'$'}
80+
label="Default price (excluding tax)"
81+
name="price"
82+
placeholder="10.00"
83+
required
84+
type="number"
85+
step="0.01"
86+
value={form.price}
87+
onChange={handleChange}
88+
/>
89+
</FormGroup>
90+
<FormGroup>
91+
<Checkbox
92+
name="isVisible"
93+
checked={form.isVisible}
94+
onChange={handleCheckboxChange}
95+
label="Visible on storefront"
96+
/>
97+
</FormGroup>
98+
</Panel>
99+
<Panel header="Description">
100+
<FormGroup>
101+
{/* Using description for demo purposes. Consider using a wysiwig instead (e.g. TinyMCE) */}
102+
<Textarea
103+
label="Description"
104+
name="description"
105+
placeholder="Product info"
106+
value={form.description}
107+
onChange={handleChange}
108+
/>
109+
</FormGroup>
110+
</Panel>
111+
<Flex justifyContent="flex-end">
112+
<Button
113+
marginRight="medium"
114+
type="button"
115+
variant="subtle"
116+
onClick={onCancel}
117+
>
118+
Cancel
119+
</Button>
120+
<Button type="submit">Save</Button>
121+
</Flex>
122+
</StyledForm>
123+
);
124+
};
125+
126+
export default Form;

components/header.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Box, Tabs } from '@bigcommerce/big-design';
22
import { useRouter } from 'next/router';
33
import { useEffect, useState } from 'react';
4+
import InnerHeader from './innerHeader';
45

56
const TabIds = {
67
HOME: 'home',
@@ -12,17 +13,33 @@ const TabRoutes = {
1213
[TabIds.PRODUCTS]: '/products',
1314
};
1415

16+
const InnerRoutes = [
17+
'/products/[pid]',
18+
];
19+
20+
const HeaderTypes = {
21+
GLOBAL: 'global',
22+
INNER: 'inner',
23+
};
24+
1525
const Header = () => {
16-
const [activeTab, setActiveTab] = useState(TabIds.HOME);
26+
const [activeTab, setActiveTab] = useState<string>('');
27+
const [headerType, setHeaderType] = useState<string>(HeaderTypes.GLOBAL);
1728
const router = useRouter();
1829
const { pathname } = router;
1930

2031
useEffect(() => {
21-
// Check if new route matches TabRoutes
22-
const tabKey = Object.keys(TabRoutes).find(key => TabRoutes[key] === pathname);
32+
if (InnerRoutes.includes(pathname)) {
33+
// Use InnerHeader if route matches inner routes
34+
setHeaderType(HeaderTypes.INNER);
35+
} else {
36+
// Check if new route matches TabRoutes
37+
const tabKey = Object.keys(TabRoutes).find(key => TabRoutes[key] === pathname);
2338

24-
// Set the active tab to tabKey or set no active tab if route doesn't match (404)
25-
setActiveTab(tabKey ?? '');
39+
// Set the active tab to tabKey or set no active tab if route doesn't match (404)
40+
setActiveTab(tabKey ?? '');
41+
setHeaderType(HeaderTypes.GLOBAL);
42+
}
2643

2744
}, [pathname]);
2845

@@ -42,6 +59,8 @@ const Header = () => {
4259
return router.push(TabRoutes[tabId]);
4360
};
4461

62+
if (headerType === HeaderTypes.INNER) return <InnerHeader />;
63+
4564
return (
4665
<Box marginBottom="xxLarge">
4766
<Tabs

components/innerHeader.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Box, Button, H1, HR, Text } from '@bigcommerce/big-design';
2+
import { ArrowBackIcon } from '@bigcommerce/big-design-icons';
3+
import { useRouter } from 'next/router';
4+
import { useProductList } from '../lib/hooks';
5+
6+
const InnerHeader = () => {
7+
const router = useRouter();
8+
const { pid } = router.query;
9+
const { list = [] } = useProductList();
10+
const { name } = list.find(item => item.id === Number(pid)) ?? {};
11+
12+
const handleBackClick = () => router.back();
13+
14+
return (
15+
<Box marginBottom="xxLarge">
16+
<Button iconLeft={<ArrowBackIcon color="secondary50" />} variant="subtle" onClick={handleBackClick}>
17+
<Text bold color="secondary50">Products</Text>
18+
</Button>
19+
{name &&
20+
<H1>{name}</H1>
21+
}
22+
<HR color="secondary30" />
23+
</Box>
24+
);
25+
};
26+
27+
export default InnerHeader;

components/loading.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Flex, H3, Panel, ProgressCircle } from '@bigcommerce/big-design';
2+
3+
const Loading = () => (
4+
<Panel>
5+
<H3>Loading...</H3>
6+
<Flex justifyContent="center" alignItems="center">
7+
<ProgressCircle size="large" />
8+
</Flex>
9+
</Panel>
10+
);
11+
12+
export default Loading;

lib/hooks.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@ export function useProducts() {
1212

1313
return {
1414
summary: data,
15+
isLoading: !data && !error,
1516
isError: error,
1617
};
1718
}
1819

1920
export function useProductList() {
20-
const { data, error } = useSWR('/api/products/list', fetcher);
21+
const { data, error, mutate: mutateList } = useSWR('/api/products/list', fetcher);
2122

2223
return {
2324
list: data,
25+
isLoading: !data && !error,
2426
isError: error,
27+
mutateList,
2528
};
2629
}

pages/api/products/[pid].ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { NextApiRequest, NextApiResponse } from 'next';
2+
import { bigcommerceClient, getSession } from '../../../lib/auth';
3+
4+
export default async function products(req: NextApiRequest, res: NextApiResponse) {
5+
const {
6+
body,
7+
query: { pid },
8+
} = req;
9+
10+
try {
11+
const { accessToken, storeId } = await getSession(req);
12+
const bigcommerce = bigcommerceClient(accessToken, storeId);
13+
14+
const { data } = await bigcommerce.put(`/catalog/products/${pid}`, body);
15+
res.status(200).json(data);
16+
} catch (error) {
17+
const { message, response } = error;
18+
res.status(response?.status || 500).end(message || 'Authentication failed, please re-install');
19+
}
20+
}

pages/index.tsx

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
1-
import { Box, Flex, H1, H2, H4, Link, Panel } from '@bigcommerce/big-design';
1+
import { Box, Flex, H1, H4, Panel } from '@bigcommerce/big-design';
22
import styled from 'styled-components';
3+
import Loading from '../components/loading';
34
import { useProducts } from '../lib/hooks';
45

56
const Index = () => {
6-
const { summary } = useProducts();
7+
const { isLoading, summary } = useProducts();
8+
9+
if (isLoading) return <Loading />;
710

811
return (
912
<Panel header="Homepage">
10-
{summary &&
11-
<Flex>
12-
<StyledBox border="box" borderRadius="normal" marginRight="xLarge" padding="medium">
13-
<H4>Inventory count</H4>
14-
<H1 marginBottom="none">{summary.inventory_count}</H1>
15-
</StyledBox>
16-
<StyledBox border="box" borderRadius="normal" marginRight="xLarge" padding="medium">
17-
<H4>Variant count</H4>
18-
<H1 marginBottom="none">{summary.variant_count}</H1>
19-
</StyledBox>
20-
<StyledBox border="box" borderRadius="normal" padding="medium">
21-
<H4>Primary category</H4>
22-
<H1 marginBottom="none">{summary.primary_category_name}</H1>
23-
</StyledBox>
24-
</Flex>
25-
}
13+
<Flex>
14+
<StyledBox border="box" borderRadius="normal" marginRight="xLarge" padding="medium">
15+
<H4>Inventory count</H4>
16+
<H1 marginBottom="none">{summary.inventory_count}</H1>
17+
</StyledBox>
18+
<StyledBox border="box" borderRadius="normal" marginRight="xLarge" padding="medium">
19+
<H4>Variant count</H4>
20+
<H1 marginBottom="none">{summary.variant_count}</H1>
21+
</StyledBox>
22+
<StyledBox border="box" borderRadius="normal" padding="medium">
23+
<H4>Primary category</H4>
24+
<H1 marginBottom="none">{summary.primary_category_name}</H1>
25+
</StyledBox>
26+
</Flex>
2627
</Panel>
2728
);
2829
};

pages/products/[pid].tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useRouter } from 'next/router';
2+
import ErrorMessage from '../../components/error';
3+
import Form from '../../components/form';
4+
import Loading from '../../components/loading';
5+
import { useProductList } from '../../lib/hooks';
6+
import { FormData } from '../../types';
7+
8+
const ProductInfo = () => {
9+
const router = useRouter();
10+
const { pid } = router.query;
11+
const { isError, isLoading, list = [], mutateList } = useProductList();
12+
const product = list.find(item => item.id === Number(pid));
13+
const { description, is_visible: isVisible, name, price, type } = product ?? {};
14+
const formData = { description, isVisible, name, price, type };
15+
16+
const handleCancel = () => router.push('/products');
17+
18+
const handleSubmit = async (data: FormData) => {
19+
try {
20+
const filteredList = list.filter(item => item.id !== Number(pid));
21+
// Update local data immediately (reduce latency to user)
22+
mutateList([...filteredList, { ...product, ...data }], false);
23+
24+
// Update product details
25+
await fetch(`/api/products/${pid}`, {
26+
method: 'PUT',
27+
headers: { 'Content-Type': 'application/json' },
28+
body: JSON.stringify(data),
29+
});
30+
31+
// Refetch to validate local data
32+
mutateList();
33+
34+
router.push('/products');
35+
} catch (error) {
36+
console.error('Error updating the product: ', error);
37+
}
38+
};
39+
40+
if (isLoading) return <Loading />;
41+
if (isError) return <ErrorMessage />;
42+
43+
return (
44+
<Form formData={formData} onCancel={handleCancel} onSubmit={handleSubmit} />
45+
);
46+
};
47+
48+
export default ProductInfo;

0 commit comments

Comments
 (0)