fontdue-js 3.6.0 → 3.7.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/CHANGELOG.md +8 -0
- package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
- package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
- package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
- package/dist/__generated__/PriceBar_node.graphql.js +23 -16
- package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
- package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
- package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
- package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
- package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
- package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
- package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
- package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
- package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
- package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
- package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductQuery.graphql.js +78 -77
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +78 -77
- package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
- package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
- package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
- package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
- package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
- package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
- package/dist/__tests__/infiniteScroll.test.js +180 -0
- package/dist/__tests__/licenseExclusions.test.js +2 -1
- package/dist/__tests__/storefrontEvents.test.js +167 -0
- package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
- package/dist/components/Cart/Checkout.js +11 -1
- package/dist/components/Cart/storefrontEvents.d.ts +34 -0
- package/dist/components/Cart/storefrontEvents.js +138 -0
- package/dist/components/FontFamilies/index.js +4 -3
- package/dist/components/Precart/index.js +6 -0
- package/dist/components/PriceBar/index.js +5 -3
- package/dist/components/Root/productState.d.ts +2 -8
- package/dist/components/Root/productState.js +46 -57
- package/dist/components/SKUPrice/index.js +7 -8
- package/dist/components/StoreModal/StoreModalFamily.js +4 -2
- package/dist/components/StoreModal/StoreModalIndex.js +48 -7
- package/dist/components/StoreModal/StoreModalProduct.js +16 -9
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
- package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
- package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
- package/dist/components/TypeTester/useTypeTesterStyler.js +1 -0
- package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
- package/dist/components/elements/StoreModalContainer/index.js +9 -4
- package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
- package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
- package/dist/components/elements/StoreModalUnifiedCheckout.js +12 -0
- package/dist/components/useInfiniteScroll.d.ts +28 -0
- package/dist/components/useInfiniteScroll.js +50 -0
- package/dist/fontdue.css +33 -0
- package/dist/hooks/useAutofit.d.ts +2 -1
- package/dist/hooks/useAutofit.js +10 -4
- package/dist/reducer.d.ts +5 -5
- package/dist/reducer.js +9 -87
- package/dist/selection.d.ts +40 -0
- package/dist/selection.js +494 -0
- package/dist/utils.d.ts +1 -12
- package/dist/utils.js +1 -36
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef, useCallback } from 'react';
|
|
1
|
+
import React, { useRef, useCallback, useMemo } from 'react';
|
|
2
2
|
import { ArrowLeft, CartNew, X } from '../../Icons/index.js';
|
|
3
3
|
import StoreModalContainerContext from './StoreModalContainerContext.js';
|
|
4
4
|
const StoreModalContainer = _ref => {
|
|
@@ -25,10 +25,15 @@ const StoreModalContainer = _ref => {
|
|
|
25
25
|
left: 0
|
|
26
26
|
});
|
|
27
27
|
}, [container]);
|
|
28
|
+
|
|
29
|
+
// Stable, so an effect that depends on the context value (the index's
|
|
30
|
+
// load-more observer) isn't rebuilt on every render of the modal.
|
|
31
|
+
const context = useMemo(() => ({
|
|
32
|
+
scrollToTop: handleScrollToTop,
|
|
33
|
+
scrollContainer: container
|
|
34
|
+
}), [handleScrollToTop]);
|
|
28
35
|
return /*#__PURE__*/React.createElement(StoreModalContainerContext.Provider, {
|
|
29
|
-
value:
|
|
30
|
-
scrollToTop: handleScrollToTop
|
|
31
|
-
}
|
|
36
|
+
value: context
|
|
32
37
|
}, /*#__PURE__*/React.createElement("div", {
|
|
33
38
|
className: "store-modal__container__overlay",
|
|
34
39
|
"data-route": routeName
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface StoreModalIndexLoadMoreProps {
|
|
3
|
+
sentinelRef: (node: Element | null) => void;
|
|
4
|
+
onClick: () => void;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
}
|
|
7
|
+
declare const StoreModalIndexLoadMore: React.FunctionComponent<StoreModalIndexLoadMoreProps>;
|
|
8
|
+
export default StoreModalIndexLoadMore;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
// Sits after the last collection while more remain. The button is what a
|
|
3
|
+
// visitor sees while a page is on its way, and the way to go on without
|
|
4
|
+
// scrolling — with a keyboard, or if the observer never fires.
|
|
5
|
+
const StoreModalIndexLoadMore = _ref => {
|
|
6
|
+
let {
|
|
7
|
+
sentinelRef,
|
|
8
|
+
onClick,
|
|
9
|
+
loading
|
|
10
|
+
} = _ref;
|
|
11
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
12
|
+
className: "store-modal__index__load-more",
|
|
13
|
+
ref: sentinelRef
|
|
14
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
15
|
+
type: "button",
|
|
16
|
+
className: "store-modal__index__load-more__button",
|
|
17
|
+
onClick: onClick,
|
|
18
|
+
disabled: loading
|
|
19
|
+
}, loading ? 'Loading…' : 'Load more'));
|
|
20
|
+
};
|
|
21
|
+
export default StoreModalIndexLoadMore;
|
|
@@ -79,6 +79,7 @@ import { Check, X } from '../Icons/index.js';
|
|
|
79
79
|
import ConfigContext from '../ConfigContext.js';
|
|
80
80
|
import { Price } from '../Price/index.js';
|
|
81
81
|
import { sendOrderTracking } from '../Cart/orderTracking.js';
|
|
82
|
+
import { trackPaymentInfoEntered } from '../Cart/storefrontEvents.js';
|
|
82
83
|
import { validateAddressElements, validateElements } from '../../stripeElements.js';
|
|
83
84
|
import { useRecaptchaTimeout, isRecaptchaScriptLoaded, RECAPTCHA_UNAVAILABLE_MESSAGE } from '../../hooks/useRecaptchaTimeout.js';
|
|
84
85
|
|
|
@@ -287,6 +288,16 @@ export default function StoreModalUnifiedCheckout(_ref2) {
|
|
|
287
288
|
useEffect(() => {
|
|
288
289
|
sendOrderTracking(environment);
|
|
289
290
|
}, [environment]);
|
|
291
|
+
|
|
292
|
+
// The Payment Element reports every keystroke; the first time the details
|
|
293
|
+
// are complete is the "Payment Info Entered" moment. The server keeps it to
|
|
294
|
+
// one per order, so this only saves requests.
|
|
295
|
+
const paymentInfoTracked = useRef(false);
|
|
296
|
+
const handlePaymentChange = useCallback(event => {
|
|
297
|
+
if (!event.complete || paymentInfoTracked.current) return;
|
|
298
|
+
paymentInfoTracked.current = true;
|
|
299
|
+
trackPaymentInfoEntered(environment);
|
|
300
|
+
}, [environment]);
|
|
290
301
|
const [error, setError] = useState(null);
|
|
291
302
|
const [errorsObject, setErrorsObject] = useState(null);
|
|
292
303
|
const [customerErrorsObject, setCustomerErrorsObject] = useState(null);
|
|
@@ -789,6 +800,7 @@ export default function StoreModalUnifiedCheckout(_ref2) {
|
|
|
789
800
|
}, /*#__PURE__*/React.createElement(Sentry.ErrorBoundary, {
|
|
790
801
|
fallback: /*#__PURE__*/React.createElement("div", null, "Something went wrong")
|
|
791
802
|
}, /*#__PURE__*/React.createElement(PaymentElement, {
|
|
803
|
+
onChange: handlePaymentChange,
|
|
792
804
|
options: {
|
|
793
805
|
layout: {
|
|
794
806
|
type: 'tabs',
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface UseInfiniteScrollOptions {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
onLoadMore: () => void;
|
|
5
|
+
rootRef?: React.RefObject<Element | null>;
|
|
6
|
+
rootMargin?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Calls `onLoadMore` when a sentinel element scrolls into view, for lists that
|
|
10
|
+
* page as the visitor reaches the end of them.
|
|
11
|
+
*
|
|
12
|
+
* Returns a ref callback to put on that sentinel — render it after the last
|
|
13
|
+
* item, and only while there is more to load:
|
|
14
|
+
*
|
|
15
|
+
* const sentinelRef = useInfiniteScroll({
|
|
16
|
+
* enabled: hasNext && !isLoadingNext,
|
|
17
|
+
* onLoadMore: () => loadNext(25),
|
|
18
|
+
* rootRef: scrollContainer,
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* {hasNext && <div ref={sentinelRef} />}
|
|
22
|
+
*
|
|
23
|
+
* The sentinel is tracked as state rather than a ref so that mounting and
|
|
24
|
+
* unmounting it re-runs the effect; `onLoadMore` is read through a ref, so a
|
|
25
|
+
* fresh callback on every render doesn't tear the observer down and rebuild it.
|
|
26
|
+
*/
|
|
27
|
+
declare const useInfiniteScroll: ({ enabled, onLoadMore, rootRef, rootMargin, }: UseInfiniteScrollOptions) => ((node: Element | null) => void);
|
|
28
|
+
export default useInfiniteScroll;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Calls `onLoadMore` when a sentinel element scrolls into view, for lists that
|
|
6
|
+
* page as the visitor reaches the end of them.
|
|
7
|
+
*
|
|
8
|
+
* Returns a ref callback to put on that sentinel — render it after the last
|
|
9
|
+
* item, and only while there is more to load:
|
|
10
|
+
*
|
|
11
|
+
* const sentinelRef = useInfiniteScroll({
|
|
12
|
+
* enabled: hasNext && !isLoadingNext,
|
|
13
|
+
* onLoadMore: () => loadNext(25),
|
|
14
|
+
* rootRef: scrollContainer,
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* {hasNext && <div ref={sentinelRef} />}
|
|
18
|
+
*
|
|
19
|
+
* The sentinel is tracked as state rather than a ref so that mounting and
|
|
20
|
+
* unmounting it re-runs the effect; `onLoadMore` is read through a ref, so a
|
|
21
|
+
* fresh callback on every render doesn't tear the observer down and rebuild it.
|
|
22
|
+
*/
|
|
23
|
+
const useInfiniteScroll = _ref => {
|
|
24
|
+
let {
|
|
25
|
+
enabled,
|
|
26
|
+
onLoadMore,
|
|
27
|
+
rootRef,
|
|
28
|
+
rootMargin
|
|
29
|
+
} = _ref;
|
|
30
|
+
const [sentinel, setSentinel] = useState(null);
|
|
31
|
+
const savedOnLoadMore = useRef(onLoadMore);
|
|
32
|
+
savedOnLoadMore.current = onLoadMore;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!sentinel || !enabled) return;
|
|
35
|
+
// Absent while server-rendering, and in test environments without one.
|
|
36
|
+
if (typeof IntersectionObserver === 'undefined') return;
|
|
37
|
+
const observer = new IntersectionObserver(entries => {
|
|
38
|
+
if (entries.some(entry => entry.isIntersecting)) {
|
|
39
|
+
savedOnLoadMore.current();
|
|
40
|
+
}
|
|
41
|
+
}, {
|
|
42
|
+
root: (rootRef === null || rootRef === void 0 ? void 0 : rootRef.current) ?? null,
|
|
43
|
+
rootMargin
|
|
44
|
+
});
|
|
45
|
+
observer.observe(sentinel);
|
|
46
|
+
return () => observer.disconnect();
|
|
47
|
+
}, [sentinel, enabled, rootRef, rootMargin]);
|
|
48
|
+
return setSentinel;
|
|
49
|
+
};
|
|
50
|
+
export default useInfiniteScroll;
|
package/dist/fontdue.css
CHANGED
|
@@ -2038,6 +2038,39 @@ input:checked + .checkbox__icon {
|
|
|
2038
2038
|
font-size: 20px;
|
|
2039
2039
|
}
|
|
2040
2040
|
|
|
2041
|
+
.store-modal__index__load-more {
|
|
2042
|
+
grid-column: 1/-1;
|
|
2043
|
+
padding-bottom: 20px;
|
|
2044
|
+
text-align: center;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
.store-modal__index__load-more__button {
|
|
2048
|
+
background: none;
|
|
2049
|
+
color: inherit;
|
|
2050
|
+
font: inherit;
|
|
2051
|
+
text-decoration: inherit;
|
|
2052
|
+
text-align: inherit;
|
|
2053
|
+
border: 0;
|
|
2054
|
+
border-radius: 0;
|
|
2055
|
+
padding: 0;
|
|
2056
|
+
margin: 0;
|
|
2057
|
+
appearance: none;
|
|
2058
|
+
text-transform: inherit;
|
|
2059
|
+
}
|
|
2060
|
+
.store-modal__index__load-more__button:active, .store-modal__index__load-more__button:focus {
|
|
2061
|
+
outline: none;
|
|
2062
|
+
}
|
|
2063
|
+
.store-modal__index__load-more__button:not(:disabled) {
|
|
2064
|
+
cursor: pointer;
|
|
2065
|
+
}
|
|
2066
|
+
.store-modal__index__load-more__button {
|
|
2067
|
+
color: var(--secondary_text_color);
|
|
2068
|
+
text-decoration: underline;
|
|
2069
|
+
}
|
|
2070
|
+
.store-modal__index__load-more__button:disabled {
|
|
2071
|
+
text-decoration: none;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2041
2074
|
.store-modal__page__body:has(> .store-modal__index-item__button) {
|
|
2042
2075
|
display: grid;
|
|
2043
2076
|
grid-template-columns: auto 1fr auto;
|
|
@@ -8,6 +8,7 @@ export interface UseAutofitProps {
|
|
|
8
8
|
fontWeight?: React.CSSProperties['fontWeight'];
|
|
9
9
|
fontStyle?: string;
|
|
10
10
|
fontVariationSettings?: string;
|
|
11
|
+
fontFeatureSettings?: string;
|
|
11
12
|
enabled?: boolean;
|
|
12
13
|
padding?: number;
|
|
13
14
|
}
|
|
@@ -16,5 +17,5 @@ export interface UseAutofitResult {
|
|
|
16
17
|
fontSize: number;
|
|
17
18
|
ready: boolean;
|
|
18
19
|
}
|
|
19
|
-
declare const useAutofit: ({ text, fontFamily, fontSize: externalFontSize, min, max, letterSpacing, fontWeight, fontStyle, fontVariationSettings, enabled, padding, }: UseAutofitProps) => UseAutofitResult;
|
|
20
|
+
declare const useAutofit: ({ text, fontFamily, fontSize: externalFontSize, min, max, letterSpacing, fontWeight, fontStyle, fontVariationSettings, fontFeatureSettings, enabled, padding, }: UseAutofitProps) => UseAutofitResult;
|
|
20
21
|
export default useAutofit;
|
package/dist/hooks/useAutofit.js
CHANGED
|
@@ -9,7 +9,8 @@ function getCanvasContext() {
|
|
|
9
9
|
return sharedCanvas.getContext('2d');
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// DOM measurement (fallback for variable fonts
|
|
12
|
+
// DOM measurement (fallback for variable fonts and OpenType features, which
|
|
13
|
+
// canvas can't express: `ctx.font` takes neither variation nor feature settings)
|
|
13
14
|
let measureEl = null;
|
|
14
15
|
function getMeasureElement() {
|
|
15
16
|
if (!measureEl) {
|
|
@@ -31,7 +32,7 @@ function measureWithCanvas(text, fontFamily, fontWeight, fontStyleValue, letterS
|
|
|
31
32
|
ctx.letterSpacing = '0px';
|
|
32
33
|
return width;
|
|
33
34
|
}
|
|
34
|
-
function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpacing, fontVariationSettings) {
|
|
35
|
+
function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpacing, fontVariationSettings, fontFeatureSettings) {
|
|
35
36
|
const el = getMeasureElement();
|
|
36
37
|
el.style.fontFamily = fontFamily;
|
|
37
38
|
el.style.fontWeight = `${fontWeight}`;
|
|
@@ -39,6 +40,7 @@ function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpac
|
|
|
39
40
|
el.style.fontSize = `${REF_SIZE}px`;
|
|
40
41
|
el.style.letterSpacing = `${letterSpacing}em`;
|
|
41
42
|
el.style.fontVariationSettings = fontVariationSettings;
|
|
43
|
+
el.style.fontFeatureSettings = fontFeatureSettings;
|
|
42
44
|
el.textContent = text;
|
|
43
45
|
return el.getBoundingClientRect().width;
|
|
44
46
|
}
|
|
@@ -53,6 +55,7 @@ const useAutofit = _ref => {
|
|
|
53
55
|
fontWeight = 400,
|
|
54
56
|
fontStyle = 'normal',
|
|
55
57
|
fontVariationSettings,
|
|
58
|
+
fontFeatureSettings,
|
|
56
59
|
enabled = true,
|
|
57
60
|
padding = 0
|
|
58
61
|
} = _ref;
|
|
@@ -83,7 +86,10 @@ const useAutofit = _ref => {
|
|
|
83
86
|
// Split by newlines and measure the widest line, since the rendered
|
|
84
87
|
// text preserves line breaks (Draft.js uses white-space: pre-wrap).
|
|
85
88
|
const lines = text.split('\n');
|
|
86
|
-
|
|
89
|
+
// Enabled features can change glyph widths (stylistic sets, small caps,
|
|
90
|
+
// ligatures), so the text has to be measured with them applied. The
|
|
91
|
+
// measure element is shared, so both settings are written on every call.
|
|
92
|
+
const measure = fontVariationSettings || fontFeatureSettings ? line => measureWithDOM(line, fontFamily, fontWeight, fontStyle, letterSpacing, fontVariationSettings || 'normal', fontFeatureSettings || 'normal') : line => measureWithCanvas(line, fontFamily, fontWeight, fontStyle, letterSpacing);
|
|
87
93
|
const refWidth = Math.max(...lines.map(measure));
|
|
88
94
|
if (refWidth <= 0) return;
|
|
89
95
|
|
|
@@ -92,7 +98,7 @@ const useAutofit = _ref => {
|
|
|
92
98
|
const newSize = Math.min(Math.max(min, Math.floor(availableWidth / (refWidth / REF_SIZE))), max);
|
|
93
99
|
setFontSize(newSize);
|
|
94
100
|
if (!measured) setMeasured(true);
|
|
95
|
-
}, [text, fontFamily, fontWeight, fontStyle, fontVariationSettings, letterSpacing, containerWidth, enabled, min, max, padding, fontVersion]);
|
|
101
|
+
}, [text, fontFamily, fontWeight, fontStyle, fontVariationSettings, fontFeatureSettings, letterSpacing, containerWidth, enabled, min, max, padding, fontVersion]);
|
|
96
102
|
if (!enabled) return {
|
|
97
103
|
ref,
|
|
98
104
|
fontSize: externalFontSize,
|
package/dist/reducer.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SkuStyleIds } from './components/Root/productState.js';
|
|
2
2
|
import { Reducer } from 'redux';
|
|
3
3
|
import { productState_Query$data } from './__generated__/productState_Query.graphql.js';
|
|
4
4
|
import type { RouteConfig } from './components/StoreModal/routes.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isSelected } from './selection.js';
|
|
6
6
|
import { Config } from './components/ConfigContext.js';
|
|
7
|
-
export {
|
|
7
|
+
export { isSelected };
|
|
8
8
|
interface LicenseOptionSpec {
|
|
9
9
|
licenseId: string;
|
|
10
10
|
licenseOptions: {
|
|
@@ -22,8 +22,9 @@ export interface FontdueState {
|
|
|
22
22
|
selectedSkuIds: {
|
|
23
23
|
[skuId: string]: boolean;
|
|
24
24
|
};
|
|
25
|
+
requestedSkuIds: string[];
|
|
25
26
|
licenseOptions: LicenseOptionSpec[];
|
|
26
|
-
|
|
27
|
+
skuStyleIds: SkuStyleIds;
|
|
27
28
|
collectionSkus: {
|
|
28
29
|
[collectionSkuId: string]: string | null;
|
|
29
30
|
};
|
|
@@ -94,7 +95,6 @@ export type FontdueAction = {
|
|
|
94
95
|
type: 'UPDATE_LICENSEE_IS_BILLING_IDENTITY';
|
|
95
96
|
value: boolean;
|
|
96
97
|
};
|
|
97
|
-
export declare const collectionSkuIdWithDiscount: (state: FontdueState, skuId: string) => [string, number] | [null, null];
|
|
98
98
|
declare const reducer: Reducer<FontdueState, FontdueAction>;
|
|
99
99
|
export declare function createDefaultStore(config: Config | undefined): import("redux").Store<FontdueState, FontdueAction, unknown>;
|
|
100
100
|
export default reducer;
|
package/dist/reducer.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { populateProductStateReducer } from './components/Root/productState.js';
|
|
2
2
|
import { createStore } from 'redux';
|
|
3
3
|
import { shallowEqual } from 'react-redux';
|
|
4
|
-
import {
|
|
4
|
+
import { isSelected, repriceSelection, selectSku } from './selection.js';
|
|
5
5
|
import { makeConfig } from './components/ConfigContext.js';
|
|
6
|
-
export {
|
|
6
|
+
export { isSelected };
|
|
7
7
|
const unselectLicense = (state, action) => {
|
|
8
8
|
if (!(action.type === 'PRECART_UNSELECT_LICENSE' || action.type === 'PRECART_TOGGLE_LICENSE')) return state;
|
|
9
9
|
return {
|
|
@@ -61,96 +61,17 @@ const selectLicenseOption = (state, action) => {
|
|
|
61
61
|
})
|
|
62
62
|
};
|
|
63
63
|
};
|
|
64
|
-
export const collectionSkuIdWithDiscount = (state, skuId) => {
|
|
65
|
-
const differences = collectionSkuIdsDifferences(state, skuId);
|
|
66
|
-
const collectionId = Object.keys(differences).filter(collSkuId => differences[collSkuId] <= state.skuPrices[skuId]).reduce((smallest, key) => {
|
|
67
|
-
if (!smallest || differences[key] < differences[smallest]) return key;
|
|
68
|
-
if (differences[key] === differences[smallest]) {
|
|
69
|
-
// When differences are equal, prefer a collection that is not the
|
|
70
|
-
// SKU being selected — this ensures collection bundles are preferred
|
|
71
|
-
// over selecting the family itself at the same price
|
|
72
|
-
if (smallest === skuId && key !== skuId) return key;
|
|
73
|
-
// Among non-self candidates, prefer the one covering more unique
|
|
74
|
-
// font styles — this ensures the most comprehensive collection wins
|
|
75
|
-
if (smallest !== skuId && key !== skuId) {
|
|
76
|
-
var _state$collectionStyl, _state$collectionStyl2;
|
|
77
|
-
const smallestStyles = new Set((_state$collectionStyl = state.collectionStyleSkus[smallest]) === null || _state$collectionStyl === void 0 ? void 0 : _state$collectionStyl.fontStyleSkuIds).size;
|
|
78
|
-
const keyStyles = new Set((_state$collectionStyl2 = state.collectionStyleSkus[key]) === null || _state$collectionStyl2 === void 0 ? void 0 : _state$collectionStyl2.fontStyleSkuIds).size;
|
|
79
|
-
if (keyStyles > smallestStyles) return key;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return smallest;
|
|
83
|
-
}, null);
|
|
84
|
-
if (collectionId) return [collectionId, differences[collectionId]];
|
|
85
|
-
return [null, null];
|
|
86
|
-
};
|
|
87
|
-
const selectedCollectionSkuIdContainingSkuId = (state, skuId) => Object.keys(state.selectedSkuIds).filter(id => state.selectedSkuIds[id] === true).find(selectedSkuId => collTransitivelyContainsSkuId(state.collectionStyleSkus, selectedSkuId, skuId));
|
|
88
|
-
const setKeysFalse = (acc, key) => {
|
|
89
|
-
acc[key] = false;
|
|
90
|
-
return acc;
|
|
91
|
-
};
|
|
92
|
-
const unselectedMembersSkuIds = (state, action) => {
|
|
93
|
-
if (action.type !== 'SELECT_SKU_ID') throw new Error('Wrong action ${action.type}');
|
|
94
|
-
const {
|
|
95
|
-
skuId,
|
|
96
|
-
selected
|
|
97
|
-
} = action;
|
|
98
|
-
if (!selected) return {}; // only do this in case we are selecting something
|
|
99
|
-
|
|
100
|
-
// Recursively collect all descendant SKU IDs so that transitive
|
|
101
|
-
// selections (e.g., collection bundle → family → bundle) are all
|
|
102
|
-
// marked as false
|
|
103
|
-
const result = {};
|
|
104
|
-
const collectMembers = collSkuId => {
|
|
105
|
-
const members = state.collectionStyleSkus[collSkuId];
|
|
106
|
-
if (!members) return;
|
|
107
|
-
for (const id of members.fontStyleSkuIds) {
|
|
108
|
-
result[id] = false;
|
|
109
|
-
}
|
|
110
|
-
for (const id of members.childrenSkuIds) {
|
|
111
|
-
result[id] = false;
|
|
112
|
-
collectMembers(id);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
collectMembers(skuId);
|
|
116
|
-
return result;
|
|
117
|
-
};
|
|
118
64
|
const selectSkuId = (state, action) => {
|
|
119
65
|
if (action.type !== 'SELECT_SKU_ID') return state;
|
|
120
|
-
const {
|
|
121
|
-
skuId,
|
|
122
|
-
selected
|
|
123
|
-
} = action;
|
|
124
|
-
let actualSkuId = skuId;
|
|
125
|
-
if (selected) {
|
|
126
|
-
// check if selecting any collections instead of this sku
|
|
127
|
-
// would reduce the selection price
|
|
128
|
-
const [collectionSkuId, _amount] = collectionSkuIdWithDiscount(state, skuId);
|
|
129
|
-
actualSkuId = collectionSkuId || actualSkuId;
|
|
130
|
-
} else if (state.selectedSkuIds[skuId] === false) {
|
|
131
|
-
// we're clicking "deselect" on an item that's not actually selected,
|
|
132
|
-
// it's a member of a collection which is selected. in this case
|
|
133
|
-
// we'll find the collection and deselect that collection instead
|
|
134
|
-
const parentId = selectedCollectionSkuIdContainingSkuId(state, skuId);
|
|
135
|
-
if (parentId) actualSkuId = parentId;
|
|
136
|
-
}
|
|
137
66
|
return {
|
|
138
|
-
...state,
|
|
139
|
-
precartOpen: true
|
|
140
|
-
selectedSkuIds: {
|
|
141
|
-
...state.selectedSkuIds,
|
|
142
|
-
[actualSkuId]: selected,
|
|
143
|
-
...unselectedMembersSkuIds(state, {
|
|
144
|
-
...action,
|
|
145
|
-
skuId: actualSkuId,
|
|
146
|
-
selected
|
|
147
|
-
})
|
|
148
|
-
}
|
|
67
|
+
...selectSku(state, action.skuId, action.selected),
|
|
68
|
+
precartOpen: true
|
|
149
69
|
};
|
|
150
70
|
};
|
|
151
71
|
const unselectSkus = state => ({
|
|
152
72
|
...state,
|
|
153
|
-
selectedSkuIds: {}
|
|
73
|
+
selectedSkuIds: {},
|
|
74
|
+
requestedSkuIds: []
|
|
154
75
|
});
|
|
155
76
|
const storeModalNavigate = (state, action) => {
|
|
156
77
|
if (action.type !== 'STORE_MODAL_NAVIGATE') return state;
|
|
@@ -197,7 +118,7 @@ const reducer = (state, action) => {
|
|
|
197
118
|
if (!state) throw 'Reducer called without state';
|
|
198
119
|
switch (action.type) {
|
|
199
120
|
case 'POPULATE_PRODUCT_STATE':
|
|
200
|
-
return populateProductStateReducer(state, action);
|
|
121
|
+
return repriceSelection(populateProductStateReducer(state, action));
|
|
201
122
|
case 'PRECART_TOGGLE_LICENSE':
|
|
202
123
|
return toggleLicense(state, action);
|
|
203
124
|
case 'PRECART_UNSELECT_LICENSE':
|
|
@@ -260,10 +181,11 @@ export function createDefaultStore(config) {
|
|
|
260
181
|
return createStore(reducer, {
|
|
261
182
|
selectedSkuIds: {},
|
|
262
183
|
// { [skuId]: Boolean }
|
|
184
|
+
requestedSkuIds: [],
|
|
263
185
|
licenseOptions: [],
|
|
264
186
|
cartOpen: false,
|
|
265
187
|
precartOpen: false,
|
|
266
|
-
|
|
188
|
+
skuStyleIds: {},
|
|
267
189
|
skuPrices: {},
|
|
268
190
|
collectionSkus: {},
|
|
269
191
|
fetchedCollectionIds: [],
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FontdueState } from './reducer.js';
|
|
2
|
+
type Catalog = Pick<FontdueState, 'skuStyleIds' | 'skuPrices'>;
|
|
3
|
+
/**
|
|
4
|
+
* The cheapest set of SKUs in which each of `picks` sits inside one SKU
|
|
5
|
+
* granting all of its styles, sorted. Picks whose styles or price aren't known
|
|
6
|
+
* are left out.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* cheapestSkuIds(['sku:bold', 'sku:regular', 'sku:light'], catalog)
|
|
10
|
+
* // => ['sku:light+regular+bold'] rather than a pair plus a single
|
|
11
|
+
*/
|
|
12
|
+
export declare const cheapestSkuIds: (picks: readonly string[], catalog: Catalog) => string[];
|
|
13
|
+
/**
|
|
14
|
+
* Re-derives `selectedSkuIds` from the picks. A pick whose styles or price
|
|
15
|
+
* aren't known yet – selected from the host page before its product data
|
|
16
|
+
* arrived – is bought as it is until they are.
|
|
17
|
+
*/
|
|
18
|
+
export declare const repriceSelection: (state: FontdueState) => FontdueState;
|
|
19
|
+
/**
|
|
20
|
+
* Adds a pick, or takes out every pick sharing a style with the clicked SKU.
|
|
21
|
+
*
|
|
22
|
+
* If what's left is still cheapest bought with a SKU granting the clicked one's
|
|
23
|
+
* styles anyway – it's part of a bundle that costs no more without it – the
|
|
24
|
+
* click would change nothing, so the picks inside the selected SKUs granting
|
|
25
|
+
* them go too, as deselecting a member of a selected bundle always has.
|
|
26
|
+
*/
|
|
27
|
+
export declare const selectSku: (state: FontdueState, skuId: string, selected: boolean) => FontdueState;
|
|
28
|
+
/**
|
|
29
|
+
* Whether a SKU – or a style with no SKU of its own – is in the selection:
|
|
30
|
+
* selected itself, or every style it grants is granted by what is selected.
|
|
31
|
+
*/
|
|
32
|
+
export declare const isSelected: (skuIdOrStyleId: string | null) => (state: FontdueState) => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* What picking a SKU would add to the selection's price: the cheapest
|
|
35
|
+
* combination with it, less the current one. Never more than the SKU's own
|
|
36
|
+
* price, and less when it completes a cheaper bundle – the Store Modal's
|
|
37
|
+
* “+$X”. Null when the SKU's styles or price aren't known.
|
|
38
|
+
*/
|
|
39
|
+
export declare const additionalCost: (state: FontdueState, skuId: string) => number | null;
|
|
40
|
+
export {};
|