@springmicro/cart 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +5838 -5413
- package/dist/index.umd.cjs +67 -67
- package/package.json +3 -3
- package/springmicro-cart-0.2.3.tgz +0 -0
- package/src/AddToCartForm.tsx +2 -1
- package/src/CartButton.tsx +119 -48
- package/src/ProductCard.css +106 -0
- package/src/ProductCard.tsx +165 -0
- package/src/checkout/CheckoutList.css +93 -0
- package/src/checkout/CheckoutList.tsx +264 -0
- package/src/checkout/{Address.tsx → components/Address.tsx} +2 -2
- package/src/checkout/{Billing.tsx → components/Billing.tsx} +19 -12
- package/src/checkout/components/CartProductCard.css +67 -0
- package/src/checkout/components/CartProductCard.tsx +80 -0
- package/src/checkout/{index.tsx → components/index.tsx} +10 -3
- package/src/index.ts +11 -5
- package/src/types.d.ts +29 -4
- package/src/utils/api.ts +0 -1
- package/src/utils/cartAuthHandler.ts +2 -2
- package/src/utils/index.ts +1 -1
- package/src/utils/storage.ts +44 -9
- /package/src/checkout/{Order.tsx → components/Order.tsx} +0 -0
- /package/src/checkout/{ProviderLogos.tsx → components/ProviderLogos.tsx} +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springmicro/cart",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@nanostores/persistent": "^0.10.1",
|
|
24
24
|
"@nanostores/query": "^0.3.3",
|
|
25
25
|
"@nanostores/react": "^0.7.2",
|
|
26
|
-
"@springmicro/utils": "0.
|
|
26
|
+
"@springmicro/utils": "0.3.0",
|
|
27
27
|
"dotenv": "^16.4.5",
|
|
28
28
|
"nanostores": "^0.10.3",
|
|
29
29
|
"react": "^18.2.0",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"vite-plugin-css-injected-by-js": "^3.5.1",
|
|
48
48
|
"yup": "^1.4.0"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "a50179e6b459af707d7ba19db175073207ce28c0"
|
|
51
51
|
}
|
|
Binary file
|
package/src/AddToCartForm.tsx
CHANGED
package/src/CartButton.tsx
CHANGED
|
@@ -23,11 +23,15 @@ export default function CartButton({
|
|
|
23
23
|
apiBaseUrl,
|
|
24
24
|
color,
|
|
25
25
|
modalYOffset,
|
|
26
|
+
checkout,
|
|
27
|
+
float,
|
|
26
28
|
}: {
|
|
27
|
-
user
|
|
29
|
+
user?: { id: string | number } & any;
|
|
28
30
|
apiBaseUrl: string;
|
|
29
|
-
color
|
|
30
|
-
modalYOffset
|
|
31
|
+
color?: string;
|
|
32
|
+
modalYOffset?: string | number;
|
|
33
|
+
checkout: () => void;
|
|
34
|
+
float?: boolean;
|
|
31
35
|
}) {
|
|
32
36
|
const [modalOpen, setModalOpen] = React.useState(false);
|
|
33
37
|
|
|
@@ -39,13 +43,17 @@ export default function CartButton({
|
|
|
39
43
|
}, [user]);
|
|
40
44
|
|
|
41
45
|
const cartStorage = useStore(cartStore);
|
|
46
|
+
const cartData = JSON.parse(cartStorage);
|
|
42
47
|
|
|
43
48
|
return (
|
|
44
49
|
<Cart
|
|
45
|
-
cart={
|
|
50
|
+
cart={cartData}
|
|
46
51
|
modalState={[modalOpen, setModalOpen]}
|
|
47
52
|
color={color}
|
|
48
53
|
modalYOffset={modalYOffset}
|
|
54
|
+
checkout={checkout}
|
|
55
|
+
apiBaseUrl={apiBaseUrl}
|
|
56
|
+
float={float}
|
|
49
57
|
/>
|
|
50
58
|
);
|
|
51
59
|
}
|
|
@@ -55,11 +63,17 @@ function Cart({
|
|
|
55
63
|
cart,
|
|
56
64
|
color,
|
|
57
65
|
modalYOffset,
|
|
66
|
+
checkout,
|
|
67
|
+
apiBaseUrl,
|
|
68
|
+
float,
|
|
58
69
|
}: {
|
|
59
70
|
modalState: [boolean, React.Dispatch<React.SetStateAction<boolean>>];
|
|
60
71
|
cart: Cart;
|
|
61
72
|
color?: string;
|
|
62
73
|
modalYOffset?: string | number;
|
|
74
|
+
checkout: () => void;
|
|
75
|
+
apiBaseUrl: string;
|
|
76
|
+
float?: boolean;
|
|
63
77
|
}) {
|
|
64
78
|
const [modalOpen, setModalOpen] = modalState;
|
|
65
79
|
|
|
@@ -77,7 +91,7 @@ function Cart({
|
|
|
77
91
|
setModalOpen((t) => !t);
|
|
78
92
|
}}
|
|
79
93
|
>
|
|
80
|
-
{cart.
|
|
94
|
+
{cart.items.length > 0 ? (
|
|
81
95
|
<ShoppingCartIcon />
|
|
82
96
|
) : (
|
|
83
97
|
<ShoppingCartOutlinedIcon />
|
|
@@ -87,8 +101,10 @@ function Cart({
|
|
|
87
101
|
open={modalOpen}
|
|
88
102
|
cart={cart}
|
|
89
103
|
clearCart={clearCart}
|
|
90
|
-
removeFromCart={removeFromCart}
|
|
91
104
|
modalYOffset={modalYOffset}
|
|
105
|
+
checkout={checkout}
|
|
106
|
+
apiBaseUrl={apiBaseUrl}
|
|
107
|
+
float={float}
|
|
92
108
|
/>
|
|
93
109
|
</Box>
|
|
94
110
|
</ClickAwayListener>
|
|
@@ -99,31 +115,66 @@ function Cart({
|
|
|
99
115
|
function CartModal({
|
|
100
116
|
open,
|
|
101
117
|
cart,
|
|
102
|
-
removeFromCart,
|
|
103
118
|
clearCart,
|
|
104
119
|
modalYOffset,
|
|
120
|
+
checkout,
|
|
121
|
+
apiBaseUrl,
|
|
122
|
+
float,
|
|
105
123
|
}: {
|
|
106
124
|
open?: boolean;
|
|
107
125
|
cart: Cart;
|
|
108
|
-
removeFromCart: (i: number) => void;
|
|
109
126
|
clearCart: () => void;
|
|
110
127
|
modalYOffset?: string | number;
|
|
128
|
+
checkout: () => void;
|
|
129
|
+
apiBaseUrl: string;
|
|
130
|
+
float?: boolean;
|
|
111
131
|
}) {
|
|
132
|
+
const [prices, setPrices] = React.useState({});
|
|
133
|
+
// build pricing list
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
// filter out prices that have already been queried
|
|
136
|
+
const pricesToGet = cart.items
|
|
137
|
+
.map((c) => c.price_id)
|
|
138
|
+
.filter(
|
|
139
|
+
(pId) => Object.keys(prices).findIndex((pKey) => pKey == pId) === -1
|
|
140
|
+
);
|
|
141
|
+
if (pricesToGet.length === 0) return;
|
|
142
|
+
|
|
143
|
+
const url = `${apiBaseUrl}/api/ecommerce/price?filter={'ids':[${pricesToGet.join(",")}]}`;
|
|
144
|
+
fetch(url, {
|
|
145
|
+
method: "GET",
|
|
146
|
+
headers: {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
.then((res) =>
|
|
151
|
+
res.json().then((data) => {
|
|
152
|
+
const pricingData = { ...prices };
|
|
153
|
+
|
|
154
|
+
data.forEach((p) => {
|
|
155
|
+
pricingData[p.id] = p;
|
|
156
|
+
});
|
|
157
|
+
setPrices(pricingData);
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
.catch(() => {
|
|
161
|
+
setPrices(null);
|
|
162
|
+
});
|
|
163
|
+
}, [cart]);
|
|
164
|
+
|
|
112
165
|
if (!open) return null;
|
|
113
166
|
return (
|
|
114
167
|
<Box
|
|
115
168
|
sx={{
|
|
116
169
|
marginTop: modalYOffset,
|
|
117
|
-
position: "fixed",
|
|
118
|
-
width:
|
|
119
|
-
minHeight:
|
|
170
|
+
position: float ? "fixed" : "absolute",
|
|
171
|
+
width: 325 + 16,
|
|
172
|
+
minHeight: 450,
|
|
120
173
|
maxHeight: "calc(90vh - 8rem)",
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
174
|
+
boxShadow: "-4px 8px 5px -7px rgba(0,0,0,0.75)",
|
|
175
|
+
py: 1,
|
|
176
|
+
right: -16,
|
|
124
177
|
pr: 2,
|
|
125
|
-
py: 2,
|
|
126
|
-
right: 0,
|
|
127
178
|
display: "grid",
|
|
128
179
|
gridTemplateRows: "1fr 40px",
|
|
129
180
|
color: "black",
|
|
@@ -133,45 +184,65 @@ function CartModal({
|
|
|
133
184
|
zIndex: 1,
|
|
134
185
|
}}
|
|
135
186
|
>
|
|
136
|
-
<Box sx={{ overflow: "auto" }}>
|
|
137
|
-
{cart.
|
|
138
|
-
{cart.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
187
|
+
<Box sx={{ overflow: "auto", pl: 2 }}>
|
|
188
|
+
{cart.items.length === 0 ? <Box>Your cart is empty.</Box> : null}
|
|
189
|
+
{cart.items.map((product, i) => {
|
|
190
|
+
const price = prices[product.price_id];
|
|
191
|
+
return (
|
|
192
|
+
<Box
|
|
193
|
+
key={product.product_id}
|
|
194
|
+
sx={{
|
|
195
|
+
display: "flex",
|
|
196
|
+
width: "100%",
|
|
197
|
+
borderBottom: "2px solid #ddd",
|
|
198
|
+
boxSizing: "border-box",
|
|
199
|
+
pr: 1,
|
|
200
|
+
py: 1.5,
|
|
201
|
+
">*": {
|
|
202
|
+
m: 0,
|
|
203
|
+
},
|
|
204
|
+
justifyContent: "space-between",
|
|
205
|
+
alignItems: "center",
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
|
209
|
+
<h2>{product.name}</h2>
|
|
210
|
+
|
|
211
|
+
<h3 style={{ fontSize: 13 }}>
|
|
212
|
+
{prices === null
|
|
213
|
+
? "Price not found."
|
|
214
|
+
: price
|
|
215
|
+
? `$${(price.unit_amount / 100).toFixed(2)}${
|
|
216
|
+
price.recurring
|
|
217
|
+
? `/${price.recurring.interval_count > 1 ? `${price.recurring.interval_count} ` : ""}${price.recurring.interval}${price.recurring.interval_count > 1 ? "s" : ""}`
|
|
218
|
+
: ""
|
|
219
|
+
}`
|
|
220
|
+
: "Loading price..."}
|
|
221
|
+
</h3>
|
|
222
|
+
</Box>
|
|
223
|
+
<Tooltip title="Remove from cart">
|
|
224
|
+
<IconButton
|
|
225
|
+
sx={{ color: "red" }}
|
|
226
|
+
onClick={() => {
|
|
227
|
+
removeFromCart(i);
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<CloseIcon />
|
|
231
|
+
</IconButton>
|
|
232
|
+
</Tooltip>
|
|
233
|
+
</Box>
|
|
234
|
+
);
|
|
235
|
+
})}
|
|
165
236
|
</Box>
|
|
166
|
-
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
|
237
|
+
<Box sx={{ display: "flex", justifyContent: "space-between", mx: 2 }}>
|
|
167
238
|
<Button
|
|
168
239
|
sx={{ color: "red" }}
|
|
169
240
|
onClick={clearCart}
|
|
170
|
-
disabled={cart.
|
|
241
|
+
disabled={cart.items.length === 0}
|
|
171
242
|
>
|
|
172
243
|
Clear cart
|
|
173
244
|
</Button>
|
|
174
|
-
<Button>Checkout →</Button>
|
|
245
|
+
<Button onClick={checkout}>Checkout →</Button>
|
|
175
246
|
</Box>
|
|
176
247
|
</Box>
|
|
177
248
|
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
display: flex;
|
|
3
|
+
gap: 0.5rem;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
border-radius: 8px;
|
|
6
|
+
padding: 1rem;
|
|
7
|
+
box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0);
|
|
8
|
+
transition: all 0.3s 0.1s;
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.card:hover {
|
|
13
|
+
box-shadow: 0px 2px 5px 2px #dfdfdfff;
|
|
14
|
+
transition: all 0.3s 0s;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.card-section {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
align-items: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.image-container {
|
|
25
|
+
width: 100%;
|
|
26
|
+
background-color: #ccc;
|
|
27
|
+
border-radius: 4px;
|
|
28
|
+
aspect-ratio: 1.5;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.button-section {
|
|
32
|
+
display: flex;
|
|
33
|
+
justify-content: space-around;
|
|
34
|
+
flex-direction: row;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
margin: 4px 2rem 0px;
|
|
37
|
+
gap: 0.5rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.addtocartbutton {
|
|
41
|
+
color: white;
|
|
42
|
+
background-color: rgb(39, 138, 230);
|
|
43
|
+
padding: 4px 16px 6px;
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
outline: none;
|
|
46
|
+
transition: all 0.3s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.addtocartbutton.red {
|
|
50
|
+
background-color: rgb(230, 39, 39);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.addtocartbutton.disabled {
|
|
54
|
+
background-color: rgb(170, 170, 170);
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
}
|
|
57
|
+
.addtocartbutton.disabled:hover {
|
|
58
|
+
background-color: rgb(170, 170, 170);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.addtocartbutton:hover {
|
|
62
|
+
background-color: rgb(36, 124, 207);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.hover-button > .hover-section {
|
|
66
|
+
display: none;
|
|
67
|
+
position: absolute;
|
|
68
|
+
width: 325px;
|
|
69
|
+
margin-left: calc(-113.75px);
|
|
70
|
+
padding: 1rem;
|
|
71
|
+
color: black;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.hover-button:hover > .hover-section {
|
|
75
|
+
display: flex;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.hover-data {
|
|
80
|
+
border-radius: 8px;
|
|
81
|
+
padding: 1rem;
|
|
82
|
+
box-shadow: 0px 2px 5px 2px rgb(173, 173, 173);
|
|
83
|
+
background-color: white;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.hover-data > div {
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
background-color: rgb(203, 203, 203);
|
|
90
|
+
gap: 1px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tieredpricingbutton {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
justify-content: flex-start;
|
|
97
|
+
align-items: flex-start;
|
|
98
|
+
text-align: left;
|
|
99
|
+
padding: 4px 0.5rem 8px;
|
|
100
|
+
background-color: white;
|
|
101
|
+
transition: all 0.3s;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.tieredpricingbutton:hover {
|
|
105
|
+
background-color: rgb(233, 233, 233);
|
|
106
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import "./ProductCard.css";
|
|
2
|
+
import { useStore } from "@nanostores/react";
|
|
3
|
+
import type React from "react";
|
|
4
|
+
import { Product, ProductPricing } from "./types";
|
|
5
|
+
import { addToCart, cartStore, removeFromCart } from "./utils/storage";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A card meant for displaying product data, allowing for purchases with managed functions.
|
|
9
|
+
*
|
|
10
|
+
* By default it displays a card component from the ecommerce-example repo but you can use a different component by providing it in the properties.
|
|
11
|
+
*
|
|
12
|
+
* A new component should be declared using this format: ```function ExampleComponent(props: @type {ProductCardComponentProps} { return <></>; })```
|
|
13
|
+
* Pass this new component as ```component={ExampleComponent}```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type ProductCardComponentProps = {
|
|
17
|
+
product: Product;
|
|
18
|
+
foundInCart: {
|
|
19
|
+
product: boolean;
|
|
20
|
+
price: boolean;
|
|
21
|
+
};
|
|
22
|
+
pricing;
|
|
23
|
+
addToCart: (price_index?: number) => void;
|
|
24
|
+
removeFromCart: (price?: boolean) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default function ProductCard({
|
|
28
|
+
product,
|
|
29
|
+
component,
|
|
30
|
+
priceTierName,
|
|
31
|
+
}: {
|
|
32
|
+
product: Product;
|
|
33
|
+
component?: React.FC<ProductCardComponentProps>;
|
|
34
|
+
priceTierName?: string; // Might have incomplete code for cases where the tier name doesn't match.
|
|
35
|
+
}) {
|
|
36
|
+
const cart = JSON.parse(useStore(cartStore));
|
|
37
|
+
|
|
38
|
+
const productInCartIndex: number = (cart.items as any[]).findIndex(
|
|
39
|
+
(p) => p.product_id === product.id
|
|
40
|
+
);
|
|
41
|
+
const productFoundInCart = !!~productInCartIndex;
|
|
42
|
+
|
|
43
|
+
const pricing: ProductPricing[] = JSON.parse(product.pricing);
|
|
44
|
+
|
|
45
|
+
const pi = pricing.findIndex((price) => price.tier_label === priceTierName); // TODO add fail-case for missing price
|
|
46
|
+
|
|
47
|
+
const priceInCartIndex: number = (cart.items as any[]).findIndex(
|
|
48
|
+
(p) => p.price_id === pricing[pi].id
|
|
49
|
+
);
|
|
50
|
+
const priceFoundInCart = !!~priceInCartIndex;
|
|
51
|
+
|
|
52
|
+
function addToCartSafe(priceIndex: number = pi) {
|
|
53
|
+
// Removes the ability to add improper info from the addToCart
|
|
54
|
+
addToCart({
|
|
55
|
+
product_id: product.id,
|
|
56
|
+
name: product.name,
|
|
57
|
+
price_id: pricing[priceIndex].id,
|
|
58
|
+
image: undefined,
|
|
59
|
+
quantity: undefined,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const props: ProductCardComponentProps = {
|
|
64
|
+
product,
|
|
65
|
+
foundInCart: {
|
|
66
|
+
product: productFoundInCart,
|
|
67
|
+
price: priceFoundInCart,
|
|
68
|
+
},
|
|
69
|
+
pricing,
|
|
70
|
+
addToCart: addToCartSafe,
|
|
71
|
+
removeFromCart: (price?: boolean) => {
|
|
72
|
+
removeFromCart(price ? priceInCartIndex : productInCartIndex);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (component !== undefined) {
|
|
77
|
+
return component!(props);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return <DefaultProductCard {...props} />;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function DefaultProductCard({
|
|
84
|
+
product,
|
|
85
|
+
foundInCart,
|
|
86
|
+
pricing,
|
|
87
|
+
addToCart,
|
|
88
|
+
removeFromCart,
|
|
89
|
+
}: ProductCardComponentProps) {
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
className="card"
|
|
93
|
+
onClick={(e) =>
|
|
94
|
+
// @ts-expect-error it works
|
|
95
|
+
!e.target.closest("button") &&
|
|
96
|
+
(window.location.href = `/product/${product.id}`)
|
|
97
|
+
}
|
|
98
|
+
>
|
|
99
|
+
<div className="card-section">
|
|
100
|
+
<b>{product.name}</b>
|
|
101
|
+
<div className="image-container"></div>
|
|
102
|
+
</div>
|
|
103
|
+
{product.description && (
|
|
104
|
+
<p style={{ margin: "0px 1rem", flexGrow: 1 }}>{product.description}</p>
|
|
105
|
+
)}
|
|
106
|
+
<div className="button-section">
|
|
107
|
+
{foundInCart.product ? (
|
|
108
|
+
<button
|
|
109
|
+
className="addtocartbutton"
|
|
110
|
+
onClick={() => {
|
|
111
|
+
removeFromCart();
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
Remove from cart
|
|
115
|
+
</button>
|
|
116
|
+
) : pricing.length === 1 ? (
|
|
117
|
+
<button
|
|
118
|
+
className="addtocartbutton"
|
|
119
|
+
onClick={() => {
|
|
120
|
+
addToCart();
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
Add {formatPricing(pricing[0])}
|
|
124
|
+
</button>
|
|
125
|
+
) : pricing.length === 0 ? (
|
|
126
|
+
<button className="addtocartbutton disabled">
|
|
127
|
+
Currently unavailable
|
|
128
|
+
</button>
|
|
129
|
+
) : (
|
|
130
|
+
<button className="addtocartbutton hover-button">
|
|
131
|
+
Add to cart ▼
|
|
132
|
+
<div className="hover-section">
|
|
133
|
+
<div className="hover-data">
|
|
134
|
+
<div>
|
|
135
|
+
{pricing.map((p, i) => (
|
|
136
|
+
<button
|
|
137
|
+
className="tieredpricingbutton"
|
|
138
|
+
onClick={() => {
|
|
139
|
+
addToCart(i);
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
Tier {i + 1}:
|
|
143
|
+
<br />
|
|
144
|
+
{formatPricing(p)}
|
|
145
|
+
</button>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</button>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatPricing({ unit_amount, recurring }: any) {
|
|
158
|
+
if (recurring) {
|
|
159
|
+
const { interval, interval_count } = recurring;
|
|
160
|
+
return `$${(unit_amount / 100).toFixed(2)}/${
|
|
161
|
+
interval_count === 1 ? interval : `${interval_count} ${interval}s`
|
|
162
|
+
}`;
|
|
163
|
+
}
|
|
164
|
+
return `$${(unit_amount / 100).toFixed(2)}`;
|
|
165
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
.two-col {
|
|
2
|
+
display: flex;
|
|
3
|
+
/* grid-template-columns: 1fr 1fr; */
|
|
4
|
+
margin-top: 2rem;
|
|
5
|
+
align-items: flex-start;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.two-col > * {
|
|
9
|
+
width: 50%;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.checkout-list {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 1rem;
|
|
17
|
+
padding: 8px 4px;
|
|
18
|
+
margin-top: 1rem;
|
|
19
|
+
max-height: 50vh;
|
|
20
|
+
overflow: auto;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@keyframes cardanimate {
|
|
24
|
+
0% {
|
|
25
|
+
box-shadow: 0px 2px 5px 2px #dfdfdfff;
|
|
26
|
+
}
|
|
27
|
+
50% {
|
|
28
|
+
box-shadow: 0px 0px 0px 0px #ffffff00;
|
|
29
|
+
}
|
|
30
|
+
100% {
|
|
31
|
+
box-shadow: 0px 2px 5px 2px #dfdfdfff;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.skeleton-card {
|
|
36
|
+
min-height: 318px;
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
box-shadow: 0px 2px 5px 2px #dfdfdfff;
|
|
39
|
+
animation: cardanimate 1.25s linear infinite;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#status-bar {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: row;
|
|
45
|
+
gap: 1rem;
|
|
46
|
+
align-items: center;
|
|
47
|
+
margin-left: 56px;
|
|
48
|
+
margin-bottom: 1rem;
|
|
49
|
+
margin-top: 2rem;
|
|
50
|
+
font-size: 18px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.status-bar {
|
|
55
|
+
width: 200px;
|
|
56
|
+
height: 6px;
|
|
57
|
+
background: linear-gradient(
|
|
58
|
+
90deg,
|
|
59
|
+
rgb(40, 130, 213) 0%,
|
|
60
|
+
rgb(40, 130, 213) 50%,
|
|
61
|
+
lightgrey 50%,
|
|
62
|
+
lightgrey 100%
|
|
63
|
+
);
|
|
64
|
+
background-position: 100% 0;
|
|
65
|
+
transition: color 0.3s 0s; /* active -> inactive */
|
|
66
|
+
background-size: 200% 100%;
|
|
67
|
+
border-radius: 16px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.status-bar.active {
|
|
71
|
+
background-position: 0 0;
|
|
72
|
+
transition: background-position 0.75s; /* inactive -> active */
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.status-text {
|
|
76
|
+
color: lightgrey;
|
|
77
|
+
transition: color 0.3s 0s; /* active -> inactive */
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.status-text.active {
|
|
81
|
+
color: rgb(40, 130, 213);
|
|
82
|
+
transition: color 0.5s 0.65s; /* inactive -> active */
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.shopping-button {
|
|
86
|
+
color: white;
|
|
87
|
+
background-color: rgb(39, 138, 230);
|
|
88
|
+
padding: 4px 16px 6px;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
outline: none;
|
|
91
|
+
transition: all 0.3s;
|
|
92
|
+
text-decoration: none;
|
|
93
|
+
}
|