Skip to content

Commit 63f9dd9

Browse files
committed
feat(common): add order page for modal
1 parent 1c7e1a5 commit 63f9dd9

File tree

10 files changed

+236
-10
lines changed

10 files changed

+236
-10
lines changed

components/error.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
import { H3, Panel } from '@bigcommerce/big-design';
2-
import { ErrorMessageProps } from '../types';
2+
import { ErrorMessageProps, ErrorProps } from '../types';
33

4-
const ErrorMessage = ({ error }: ErrorMessageProps) => (
5-
<Panel>
4+
const ErrorContent = ({ message }: Pick<ErrorProps, 'message'>) => (
5+
<>
66
<H3>Failed to load</H3>
7-
{error && error.message}
8-
</Panel>
9-
);
7+
{message}
8+
</>
9+
)
10+
11+
const ErrorMessage = ({ error, renderPanel = true }: ErrorMessageProps) => {
12+
if (renderPanel) {
13+
return (
14+
<Panel>
15+
<ErrorContent message={error.message} />
16+
</Panel>
17+
)
18+
}
19+
20+
return <ErrorContent message={error.message} />
21+
};
1022

1123
export default ErrorMessage;

components/header.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@ export const TabRoutes = {
1313
[TabIds.PRODUCTS]: '/products',
1414
};
1515

16+
const HeaderlessRoutes = [
17+
'/orders/[orderId]/labels',
18+
'/orders/[orderId]/modal',
19+
]
20+
1621
const InnerRoutes = [
1722
'/products/[pid]',
1823
];
1924

2025
const HeaderTypes = {
2126
GLOBAL: 'global',
2227
INNER: 'inner',
28+
HEADERLESS: 'headerless',
2329
};
2430

2531
const Header = () => {
@@ -32,6 +38,8 @@ const Header = () => {
3238
if (InnerRoutes.includes(pathname)) {
3339
// Use InnerHeader if route matches inner routes
3440
setHeaderType(HeaderTypes.INNER);
41+
} else if (HeaderlessRoutes.includes(pathname)) {
42+
setHeaderType(HeaderTypes.HEADERLESS);
3543
} else {
3644
// Check if new route matches TabRoutes
3745
const tabKey = Object.keys(TabRoutes).find(key => TabRoutes[key] === pathname);
@@ -59,6 +67,7 @@ const Header = () => {
5967
return router.push(TabRoutes[tabId]);
6068
};
6169

70+
if (headerType === HeaderTypes.HEADERLESS) return null;
6271
if (headerType === HeaderTypes.INNER) return <InnerHeader />;
6372

6473
return (

lib/auth.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@ const bigcommerceSigned = new BigCommerce({
2323
responseType: 'json',
2424
});
2525

26-
export function bigcommerceClient(accessToken: string, storeHash: string) {
26+
export function bigcommerceClient(accessToken: string, storeHash: string, apiVersion = 'v3') {
2727
return new BigCommerce({
2828
clientId: CLIENT_ID,
2929
accessToken,
3030
storeHash,
3131
responseType: 'json',
32-
apiVersion: 'v3',
32+
apiVersion,
3333
});
3434
}
35+
3536
// Authorizes app on install
3637
export function getBCAuth(query: QueryParams) {
3738
return bigcommerce.authorize(query);

lib/hooks.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import useSWR from 'swr';
22
import { useSession } from '../context/session';
3-
import { ErrorProps, ListItem, QueryParams } from '../types';
3+
import { ErrorProps, ListItem, Order, QueryParams } from '../types';
44

55
async function fetcher(url: string, query: string) {
66
const res = await fetch(`${url}?${query}`);
@@ -60,3 +60,18 @@ export function useProductInfo(pid: number, list: ListItem[]) {
6060
error,
6161
};
6262
}
63+
64+
export const useOrder = (orderId: number) => {
65+
const { context } = useSession();
66+
const params = new URLSearchParams({ context }).toString();
67+
const shouldFetch = context && orderId !== undefined;
68+
69+
// Conditionally fetch orderId is defined
70+
const { data, error } = useSWR<Order, ErrorProps>(shouldFetch ? [`/api/orders/${orderId}`, params] : null, fetcher);
71+
72+
return {
73+
order: data,
74+
isLoading: !data && !error,
75+
error,
76+
};
77+
}

pages/_app.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ const MyApp = ({ Component, pageProps }: AppProps) => {
99
return (
1010
<ThemeProvider theme={defaultTheme}>
1111
<GlobalStyles />
12-
<Box marginHorizontal="xxxLarge" marginVertical="xxLarge">
12+
<Box
13+
marginHorizontal={{ mobile: 'none', tablet: 'xxxLarge' }}
14+
marginVertical={{ mobile: 'none', tablet: "xxLarge" }}
15+
>
1316
<Header />
1417
<SessionProvider>
1518
<Component {...pageProps} />

pages/api/orders/[orderId]/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { NextApiRequest, NextApiResponse } from 'next';
2+
import { bigcommerceClient, getSession } from '../../../../lib/auth';
3+
4+
export default async function orderId(req: NextApiRequest, res: NextApiResponse) {
5+
const {
6+
query: { orderId },
7+
method,
8+
} = req;
9+
10+
try {
11+
const { accessToken, storeHash } = await getSession(req);
12+
const bigcommerce = bigcommerceClient(accessToken, storeHash , 'v2');
13+
14+
switch (method) {
15+
case 'GET': {
16+
const data = await bigcommerce.get(`/orders/${orderId}`);
17+
18+
res.status(200).json(data);
19+
20+
break;
21+
}
22+
23+
default: {
24+
res.setHeader('Allow', ['GET']);
25+
res.status(405).end(`Method ${method} Not Allowed`);
26+
}
27+
}
28+
29+
} catch (error) {
30+
const { message, response } = error;
31+
res.status(response?.status || 500).json({ message });
32+
}
33+
}

pages/orders/[orderId]/modal.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { Badge, Flex, Grid, GridItem, H3, Text } from '@bigcommerce/big-design';
2+
import { useRouter } from 'next/router';
3+
import React from 'react';
4+
import styled from 'styled-components';
5+
6+
import ErrorMessage from '../../../components/error';
7+
import { useOrder } from '../../../lib/hooks';
8+
import { Order } from '../../../types';
9+
10+
const StyledAddress = styled.address`
11+
color: ${({ theme }) => theme.colors.secondary70};
12+
font-style: normal;
13+
line-height: ${({ theme }) => theme.lineHeight.medium};
14+
`;
15+
16+
const StyledDl = styled.dl`
17+
color: ${({ theme }) => theme.colors.secondary70};
18+
display: grid;
19+
grid-template: 'dt dd' auto / 1fr auto;
20+
grid-auto-rows: auto;
21+
line-height: ${({ theme }) => theme.lineHeight.medium};
22+
margin: 0;
23+
24+
dt {
25+
grid-area: 'dt';
26+
}
27+
28+
dd {
29+
grid-area: 'dd';
30+
text-align: right;
31+
}
32+
`;
33+
34+
const InternalOrderModalPage = (order: Order) => {
35+
const { billing_address } = order;
36+
37+
const formatCurrency = (amount: string) =>
38+
new Intl.NumberFormat(order.customer_locale, { style: 'currency', currency: order.currency_code }).format(parseFloat(amount));
39+
40+
return (
41+
<Grid gridColumns="repeat(auto-fill, minmax(16rem, 1fr))" gridGap="3rem">
42+
<GridItem>
43+
<H3>Billing information</H3>
44+
<StyledAddress>
45+
<div>
46+
{billing_address.first_name} {billing_address.last_name}
47+
</div>
48+
<div>{billing_address.street_1}</div>
49+
{billing_address.street_2 && <div>{billing_address.street_2}</div>}
50+
<div>
51+
{billing_address.city}, {billing_address.state}, {billing_address.zip}
52+
</div>
53+
<div>{billing_address.country}</div>
54+
</StyledAddress>
55+
</GridItem>
56+
<GridItem>
57+
<Flex
58+
alignItems="center"
59+
flexDirection="row"
60+
flexWrap="nowrap"
61+
justifyContent="space-between"
62+
marginBottom="small"
63+
>
64+
<H3 marginBottom="none">Payment details</H3>
65+
<Badge label={order.payment_status} variant="success" />
66+
</Flex>
67+
<StyledDl>
68+
<dt>Subtotal</dt>
69+
<dd>{formatCurrency(order.subtotal_ex_tax)}</dd>
70+
<dt>Discount</dt>
71+
<dd>-{formatCurrency(order.discount_amount)}</dd>
72+
<dt>Shipping</dt>
73+
<dd>{formatCurrency(order.shipping_cost_ex_tax)}</dd>
74+
<dt>Tax</dt>
75+
<dd>{formatCurrency(order.total_tax)}</dd>
76+
<dt>
77+
<Text as="span" bold marginBottom="none">Grand total</Text>
78+
</dt>
79+
<dd>
80+
<Text as="span" bold marginBottom="none">{formatCurrency(order.total_inc_tax)}</Text>
81+
</dd>
82+
</StyledDl>
83+
</GridItem>
84+
<GridItem>
85+
<H3>Order information</H3>
86+
<StyledDl>
87+
<dt>ID</dt>
88+
<dd>{order.id}</dd>
89+
<dt>Type</dt>
90+
<dd>
91+
<span style={{ textTransform: 'capitalize' }}>{order.order_source}</span>
92+
</dd>
93+
<dt>Status</dt>
94+
<dd>{order.status}</dd>
95+
<dt>Total items</dt>
96+
<dd>
97+
{order.items_total > 1 ? `${order.items_total} items` : `${order.items_total} item`}
98+
</dd>
99+
</StyledDl>
100+
</GridItem>
101+
</Grid>
102+
);
103+
};
104+
105+
const OrderModalPage = () => {
106+
const router = useRouter();
107+
const { orderId } = router.query;
108+
const { isLoading, order, error } = useOrder(parseInt(`${orderId}`, 10));
109+
110+
if (isLoading) {
111+
return null;
112+
}
113+
114+
if (error) {
115+
return <ErrorMessage error={error} renderPanel={false} />
116+
}
117+
118+
return <InternalOrderModalPage {...order} />;
119+
};
120+
121+
export default OrderModalPage;

types/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export interface ErrorProps extends Error {
44

55
export interface ErrorMessageProps {
66
error?: ErrorProps;
7+
renderPanel?: boolean;
78
}

types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './auth';
22
export * from './data';
33
export * from './db';
44
export * from './error';
5+
export * from './order';

types/order.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export interface BillingAddress {
2+
// Additional fields exist, type as needed
3+
[key: string]: unknown;
4+
first_name: string;
5+
last_name: string;
6+
street_1: string;
7+
street_2: string;
8+
city: string;
9+
state: string;
10+
zip: string;
11+
country: string;
12+
}
13+
14+
export interface Order {
15+
// Additional fields exist, type as needed
16+
[key: string]: unknown;
17+
billing_address: BillingAddress;
18+
currency_code: string;
19+
customer_locale: string;
20+
discount_amount: string;
21+
id: number;
22+
items_total: number;
23+
order_source: string;
24+
payment_status: string;
25+
status: string;
26+
subtotal_ex_tax: string;
27+
shipping_cost_ex_tax: string;
28+
total_inc_tax: string;
29+
total_tax: string;
30+
}

0 commit comments

Comments
 (0)