@tapcart/mobile-components 0.12.21 → 0.13.1
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/components/hooks/use-block-conditional-rendering.d.ts +2 -0
- package/dist/components/hooks/use-block-conditional-rendering.d.ts.map +1 -1
- package/dist/components/hooks/use-block-conditional-rendering.js +10 -9
- package/dist/components/hooks/use-block-conditional-rendering.test.d.ts +2 -0
- package/dist/components/hooks/use-block-conditional-rendering.test.d.ts.map +1 -0
- package/dist/components/hooks/use-block-conditional-rendering.test.js +112 -0
- package/dist/components/hooks/use-personalized-cluster-feed-core.d.ts +74 -0
- package/dist/components/hooks/use-personalized-cluster-feed-core.d.ts.map +1 -0
- package/dist/components/hooks/use-personalized-cluster-feed-core.js +163 -0
- package/dist/components/hooks/use-personalized-cluster-feed-core.test.d.ts +2 -0
- package/dist/components/hooks/use-personalized-cluster-feed-core.test.d.ts.map +1 -0
- package/dist/components/hooks/use-personalized-cluster-feed-core.test.js +106 -0
- package/dist/components/hooks/use-personalized-cluster-feed.d.ts +5 -0
- package/dist/components/hooks/use-personalized-cluster-feed.d.ts.map +1 -0
- package/dist/components/hooks/use-personalized-cluster-feed.js +191 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/lib/conditional-rendering.util.test.d.ts +2 -0
- package/dist/lib/conditional-rendering.util.test.d.ts.map +1 -0
- package/dist/lib/conditional-rendering.util.test.js +116 -0
- package/dist/styles.css +106 -3
- package/package.json +1 -1
|
@@ -4,6 +4,8 @@ export declare const useBlockConditionalRendering: (_props: {
|
|
|
4
4
|
apiUrl: string;
|
|
5
5
|
deviceVariables: any;
|
|
6
6
|
customerVariables: any;
|
|
7
|
+
sessionVariables?: any;
|
|
8
|
+
storageVariables?: any;
|
|
7
9
|
}, _block: PhoenixBlock, mobileComponentOverrides?: any) => {
|
|
8
10
|
shouldShow: boolean;
|
|
9
11
|
isLoading: any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-block-conditional-rendering.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-block-conditional-rendering.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,YAAY,EAGb,MAAM,kBAAkB,CAAA;AAezB,eAAO,MAAM,4BAA4B,WAC/B;IACN,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,GAAG,CAAA;IACpB,iBAAiB,EAAE,GAAG,CAAA;CACvB,UACO,YAAY,6BACO,GAAG;;;
|
|
1
|
+
{"version":3,"file":"use-block-conditional-rendering.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-block-conditional-rendering.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,YAAY,EAGb,MAAM,kBAAkB,CAAA;AAezB,eAAO,MAAM,4BAA4B,WAC/B;IACN,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,GAAG,CAAA;IACpB,iBAAiB,EAAE,GAAG,CAAA;IACtB,gBAAgB,CAAC,EAAE,GAAG,CAAA;IACtB,gBAAgB,CAAC,EAAE,GAAG,CAAA;CACvB,UACO,YAAY,6BACO,GAAG;;;CAwT/B,CAAA"}
|
|
@@ -16,12 +16,12 @@ import { useCollection as mclUseCollection } from "./use-collection";
|
|
|
16
16
|
import { useSearchParams } from "next/navigation";
|
|
17
17
|
import { gidFromId, countNumberOfTagsInState, getEnvState, shouldShowBlock, evaluateConditions, parseBCP47Locale, } from "../../lib/utils";
|
|
18
18
|
export const useBlockConditionalRendering = (_props, _block, mobileComponentOverrides) => {
|
|
19
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j
|
|
20
|
-
const
|
|
19
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
20
|
+
const _k = ((_a = _block === null || _block === void 0 ? void 0 : _block.visibilityConditions) === null || _a === void 0 ? void 0 : _a.conditions) || {}, { enabled: conditionalsV1IsEnabled = false, exclude: isInverse = false } = _k, restOfProps = __rest(_k, ["enabled", "exclude"]);
|
|
21
21
|
const { _version = 1, conditions, enabled: conditionalsV2IsEnabled = false, } = (_block === null || _block === void 0 ? void 0 : _block.visibilityConditions) || {};
|
|
22
22
|
const isConditionalsEnabled = conditionalsV1IsEnabled || conditionalsV2IsEnabled;
|
|
23
23
|
const blockState = restOfProps;
|
|
24
|
-
const { appId = "", apiUrl = "", deviceVariables, customerVariables } = _props;
|
|
24
|
+
const { appId = "", apiUrl = "", deviceVariables, customerVariables, sessionVariables, storageVariables, } = _props;
|
|
25
25
|
const searchParams = useSearchParams();
|
|
26
26
|
const [deviceLanguage = "en", deviceCountry = "US"] = parseBCP47Locale(deviceVariables === null || deviceVariables === void 0 ? void 0 : deviceVariables.locale);
|
|
27
27
|
const country = (_c = (_b = deviceVariables === null || deviceVariables === void 0 ? void 0 : deviceVariables.countryCode) !== null && _b !== void 0 ? _b : searchParams === null || searchParams === void 0 ? void 0 : searchParams.get("country")) !== null && _c !== void 0 ? _c : deviceCountry;
|
|
@@ -87,9 +87,7 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
|
|
|
87
87
|
country,
|
|
88
88
|
productMetafieldsQuery,
|
|
89
89
|
]);
|
|
90
|
-
const useProductsSkipArguments = useMemo(() => (mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts)
|
|
91
|
-
? { skip: true }
|
|
92
|
-
: null, [mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts]);
|
|
90
|
+
const useProductsSkipArguments = useMemo(() => ((mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts) ? { skip: true } : null), [mobileComponentOverrides === null || mobileComponentOverrides === void 0 ? void 0 : mobileComponentOverrides.useProducts]);
|
|
93
91
|
const useProductsInput = useMemo(() => shouldFetchProduct ? useProductsArguments : useProductsSkipArguments, [shouldFetchProduct, useProductsArguments, useProductsSkipArguments]);
|
|
94
92
|
const { products, error: useProductsError, isLoading: isProductsLoading, } = useProducts(useProductsInput);
|
|
95
93
|
const blockCollectionMetafields = useMemo(() => {
|
|
@@ -180,8 +178,11 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
|
|
|
180
178
|
product,
|
|
181
179
|
collection: specificCollection,
|
|
182
180
|
customer: customerVariables,
|
|
183
|
-
userGroup
|
|
184
|
-
|
|
181
|
+
// userGroup/storage live at the top level of the SDK variables object
|
|
182
|
+
// (variables.session / variables.storage), as siblings of device and
|
|
183
|
+
// customer — not nested under device.
|
|
184
|
+
userGroup: sessionVariables === null || sessionVariables === void 0 ? void 0 : sessionVariables.userGroup,
|
|
185
|
+
storage: storageVariables,
|
|
185
186
|
searchParams,
|
|
186
187
|
});
|
|
187
188
|
const evaluateV1Conditions = () => {
|
|
@@ -216,5 +217,5 @@ export const useBlockConditionalRendering = (_props, _block, mobileComponentOver
|
|
|
216
217
|
console.error("Error evaluating block visibility conditions:", e);
|
|
217
218
|
shouldShow = true; // Fail-safe to show block
|
|
218
219
|
}
|
|
219
|
-
return
|
|
220
|
+
return { shouldShow, isLoading };
|
|
220
221
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-block-conditional-rendering.test.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-block-conditional-rendering.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
// Controlled by tests; the next/navigation mock reads it on each call.
|
|
3
|
+
let mockSearchParams = new URLSearchParams();
|
|
4
|
+
jest.mock("next/navigation", () => ({
|
|
5
|
+
useSearchParams: () => mockSearchParams,
|
|
6
|
+
}));
|
|
7
|
+
// The hook fetches products/collections via SWR-backed hooks; stub them so no
|
|
8
|
+
// network/fetch happens. userGroup/storage/queryParam paths don't need them.
|
|
9
|
+
jest.mock("./use-products", () => ({
|
|
10
|
+
useProducts: () => ({ products: [], error: undefined, isLoading: false }),
|
|
11
|
+
}));
|
|
12
|
+
jest.mock("./use-collection", () => ({
|
|
13
|
+
useCollection: () => ({
|
|
14
|
+
specificCollection: {},
|
|
15
|
+
error: undefined,
|
|
16
|
+
loading: false,
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
import { useBlockConditionalRendering } from "./use-block-conditional-rendering";
|
|
20
|
+
const v2Block = (conditions, enabled = true) => ({
|
|
21
|
+
_id: "block-1",
|
|
22
|
+
visibilityConditions: { _version: 2, enabled, conditions },
|
|
23
|
+
});
|
|
24
|
+
const baseProps = {
|
|
25
|
+
appId: "app-1",
|
|
26
|
+
apiUrl: "https://example.test",
|
|
27
|
+
deviceVariables: { locale: "en-US" },
|
|
28
|
+
customerVariables: {},
|
|
29
|
+
sessionVariables: { userGroup: 1 },
|
|
30
|
+
storageVariables: {},
|
|
31
|
+
};
|
|
32
|
+
const run = (props, block) => renderHook(() => useBlockConditionalRendering(props, block, undefined)).result
|
|
33
|
+
.current;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mockSearchParams = new URLSearchParams();
|
|
36
|
+
});
|
|
37
|
+
describe("useBlockConditionalRendering - userGroup", () => {
|
|
38
|
+
const block = v2Block({
|
|
39
|
+
logic: "OR",
|
|
40
|
+
conditions: [{ type: "userGroup", operator: "equals", value: "1" }],
|
|
41
|
+
});
|
|
42
|
+
it("shows the block when the top-level session.userGroup matches", () => {
|
|
43
|
+
// Regression guard: userGroup must be read from sessionVariables
|
|
44
|
+
// (variables.session.userGroup), NOT variables.device.session.userGroup.
|
|
45
|
+
const { shouldShow } = run(Object.assign(Object.assign({}, baseProps), { sessionVariables: { userGroup: 1 } }), block);
|
|
46
|
+
expect(shouldShow).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it("hides the block when the user is in a different group", () => {
|
|
49
|
+
const { shouldShow } = run(Object.assign(Object.assign({}, baseProps), { sessionVariables: { userGroup: 2 } }), block);
|
|
50
|
+
expect(shouldShow).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it("hides the block (fails closed) when no session is present", () => {
|
|
53
|
+
const { shouldShow } = run(Object.assign(Object.assign({}, baseProps), { sessionVariables: undefined }), block);
|
|
54
|
+
expect(shouldShow).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
it("does NOT read userGroup from deviceVariables.session", () => {
|
|
57
|
+
// If the wiring regresses to deviceVariables.session.userGroup, this
|
|
58
|
+
// (group on device, not session) would incorrectly show the block.
|
|
59
|
+
const { shouldShow } = run(Object.assign(Object.assign({}, baseProps), { sessionVariables: undefined, deviceVariables: { locale: "en-US", session: { userGroup: 1 } } }), block);
|
|
60
|
+
expect(shouldShow).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("useBlockConditionalRendering - queryParam", () => {
|
|
64
|
+
const block = v2Block({
|
|
65
|
+
logic: "OR",
|
|
66
|
+
conditions: [
|
|
67
|
+
{
|
|
68
|
+
type: "queryParam",
|
|
69
|
+
operator: "equals",
|
|
70
|
+
value: { key: "utm", value: "spring" },
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
it("shows when the query param matches", () => {
|
|
75
|
+
mockSearchParams = new URLSearchParams("utm=spring");
|
|
76
|
+
expect(run(baseProps, block).shouldShow).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it("hides when the query param does not match", () => {
|
|
79
|
+
mockSearchParams = new URLSearchParams("utm=fall");
|
|
80
|
+
expect(run(baseProps, block).shouldShow).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("useBlockConditionalRendering - storageVariable", () => {
|
|
84
|
+
const block = v2Block({
|
|
85
|
+
logic: "OR",
|
|
86
|
+
conditions: [
|
|
87
|
+
{
|
|
88
|
+
type: "storageVariable",
|
|
89
|
+
operator: "equals",
|
|
90
|
+
value: { key: "promo", value: "active" },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
it("shows when the storage value matches", () => {
|
|
95
|
+
expect(run(Object.assign(Object.assign({}, baseProps), { storageVariables: { promo: "active" } }), block)
|
|
96
|
+
.shouldShow).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it("hides when the storage value does not match", () => {
|
|
99
|
+
expect(run(Object.assign(Object.assign({}, baseProps), { storageVariables: { promo: "inactive" } }), block)
|
|
100
|
+
.shouldShow).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("useBlockConditionalRendering - disabled", () => {
|
|
104
|
+
it("shows the block when visibility conditions are disabled", () => {
|
|
105
|
+
const block = v2Block({
|
|
106
|
+
logic: "OR",
|
|
107
|
+
conditions: [{ type: "userGroup", operator: "equals", value: "999" }],
|
|
108
|
+
}, false // enabled: false
|
|
109
|
+
);
|
|
110
|
+
expect(run(baseProps, block).shouldShow).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type PersonalizedClusterFeedFrame = {
|
|
2
|
+
bundleId?: string | number | null;
|
|
3
|
+
layoutType?: string | null;
|
|
4
|
+
collectionId?: string | number | null;
|
|
5
|
+
anchorProductId?: string | number | null;
|
|
6
|
+
complementProductIds?: Array<string | number>;
|
|
7
|
+
title?: string | null;
|
|
8
|
+
hook?: string | null;
|
|
9
|
+
theme?: string | null;
|
|
10
|
+
atcScore?: number | null;
|
|
11
|
+
content?: {
|
|
12
|
+
contentType?: string | null;
|
|
13
|
+
contentUrl?: string | null;
|
|
14
|
+
} | null;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
export type PersonalizedClusterFeedData = {
|
|
18
|
+
collectionIds: string[];
|
|
19
|
+
explanations: Record<string, unknown>;
|
|
20
|
+
frames: PersonalizedClusterFeedFrame[];
|
|
21
|
+
};
|
|
22
|
+
export type PersonalizedClusterFeedStatus = "idle" | "loading" | "ready" | "empty" | "error";
|
|
23
|
+
export type UsePersonalizedClusterFeedProps = {
|
|
24
|
+
appId?: string | null;
|
|
25
|
+
deviceId?: string | null;
|
|
26
|
+
customerProfilesBaseUrl?: string | null;
|
|
27
|
+
customerProfilesBaseURL?: string | null;
|
|
28
|
+
dashboardPreview?: boolean;
|
|
29
|
+
skip?: boolean;
|
|
30
|
+
};
|
|
31
|
+
export type UsePersonalizedClusterFeedResult = {
|
|
32
|
+
data: PersonalizedClusterFeedData | null;
|
|
33
|
+
status: PersonalizedClusterFeedStatus;
|
|
34
|
+
error: unknown;
|
|
35
|
+
isLoading: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare const normalizePersonalizedClusterFeedBaseUrl: (value?: string | null) => string | null;
|
|
38
|
+
export declare const createPersonalizedClusterFeedRequestKey: ({ appId, deviceId, customerProfilesBaseUrl, dashboardPreview, }: {
|
|
39
|
+
appId: string;
|
|
40
|
+
deviceId?: string | null | undefined;
|
|
41
|
+
customerProfilesBaseUrl: string;
|
|
42
|
+
dashboardPreview?: boolean | undefined;
|
|
43
|
+
}) => string;
|
|
44
|
+
export declare const getPersonalizedClusterFeedSessionCacheKey: (appId: string, deviceId: string) => string;
|
|
45
|
+
export declare const normalizePersonalizedClusterFeed: (data: unknown) => PersonalizedClusterFeedData;
|
|
46
|
+
export declare const isEmptyPersonalizedClusterFeed: (data: PersonalizedClusterFeedData | null) => boolean;
|
|
47
|
+
export declare const readPersonalizedClusterFeedSessionCache: ({ appId, deviceId, freshMs, swrMs, }: {
|
|
48
|
+
appId: string;
|
|
49
|
+
deviceId: string;
|
|
50
|
+
freshMs: number;
|
|
51
|
+
swrMs: number;
|
|
52
|
+
}) => {
|
|
53
|
+
data: PersonalizedClusterFeedData;
|
|
54
|
+
fresh: boolean;
|
|
55
|
+
} | null;
|
|
56
|
+
export declare const writePersonalizedClusterFeedSessionCache: ({ appId, deviceId, data, }: {
|
|
57
|
+
appId: string;
|
|
58
|
+
deviceId: string;
|
|
59
|
+
data: PersonalizedClusterFeedData;
|
|
60
|
+
}) => void;
|
|
61
|
+
export declare function fetchPersonalizedClusterFeed({ appId, deviceId, customerProfilesBaseUrl, dashboardPreview, signal, }: {
|
|
62
|
+
appId: string;
|
|
63
|
+
deviceId?: string | null;
|
|
64
|
+
customerProfilesBaseUrl: string;
|
|
65
|
+
dashboardPreview?: boolean;
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
}): Promise<PersonalizedClusterFeedData>;
|
|
68
|
+
export declare const fetchPersonalizedClusterFeedOnce: ({ requests, key, fetcher, retentionMs, }: {
|
|
69
|
+
requests: Map<string, Promise<PersonalizedClusterFeedData>>;
|
|
70
|
+
key: string;
|
|
71
|
+
fetcher: () => Promise<PersonalizedClusterFeedData>;
|
|
72
|
+
retentionMs?: number | undefined;
|
|
73
|
+
}) => Promise<PersonalizedClusterFeedData>;
|
|
74
|
+
//# sourceMappingURL=use-personalized-cluster-feed-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-personalized-cluster-feed-core.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-personalized-cluster-feed-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
const normalizeStringIds = (ids) => Array.isArray(ids)
|
|
13
|
+
? ids.filter((id) => id != null).map((id) => String(id))
|
|
14
|
+
: [];
|
|
15
|
+
const normalizeFrame = (frame) => {
|
|
16
|
+
if (!isRecord(frame))
|
|
17
|
+
return null;
|
|
18
|
+
return Object.assign(Object.assign(Object.assign({}, frame), (frame.layoutType != null
|
|
19
|
+
? { layoutType: String(frame.layoutType) }
|
|
20
|
+
: {})), (Array.isArray(frame.complementProductIds)
|
|
21
|
+
? {
|
|
22
|
+
complementProductIds: frame.complementProductIds
|
|
23
|
+
.filter((id) => id != null)
|
|
24
|
+
.map((id) => String(id)),
|
|
25
|
+
}
|
|
26
|
+
: {}));
|
|
27
|
+
};
|
|
28
|
+
export const normalizePersonalizedClusterFeedBaseUrl = (value) => {
|
|
29
|
+
if (!value)
|
|
30
|
+
return null;
|
|
31
|
+
return String(value).replace(/\/+$/, "");
|
|
32
|
+
};
|
|
33
|
+
export const createPersonalizedClusterFeedRequestKey = ({ appId, deviceId, customerProfilesBaseUrl, dashboardPreview, }) => {
|
|
34
|
+
const mode = dashboardPreview ? "preview" : "cluster-feed";
|
|
35
|
+
return [
|
|
36
|
+
"personalized-cluster-feed",
|
|
37
|
+
mode,
|
|
38
|
+
normalizePersonalizedClusterFeedBaseUrl(customerProfilesBaseUrl),
|
|
39
|
+
appId,
|
|
40
|
+
dashboardPreview ? "preview" : deviceId,
|
|
41
|
+
].join(":");
|
|
42
|
+
};
|
|
43
|
+
export const getPersonalizedClusterFeedSessionCacheKey = (appId, deviceId) => `foryou-cluster-${appId}-${deviceId}`;
|
|
44
|
+
export const normalizePersonalizedClusterFeed = (data) => {
|
|
45
|
+
const source = isRecord(data) ? data : {};
|
|
46
|
+
return {
|
|
47
|
+
collectionIds: normalizeStringIds(source.collectionIds),
|
|
48
|
+
explanations: isRecord(source.explanations) ? source.explanations : {},
|
|
49
|
+
frames: Array.isArray(source.frames)
|
|
50
|
+
? source.frames
|
|
51
|
+
.map(normalizeFrame)
|
|
52
|
+
.filter((frame) => Boolean(frame))
|
|
53
|
+
: [],
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export const isEmptyPersonalizedClusterFeed = (data) => Boolean(data) && !data.collectionIds.length && !data.frames.length;
|
|
57
|
+
export const readPersonalizedClusterFeedSessionCache = ({ appId, deviceId, freshMs, swrMs, }) => {
|
|
58
|
+
try {
|
|
59
|
+
const cached = sessionStorage.getItem(getPersonalizedClusterFeedSessionCacheKey(appId, deviceId));
|
|
60
|
+
if (!cached)
|
|
61
|
+
return null;
|
|
62
|
+
const { data, ts } = JSON.parse(cached);
|
|
63
|
+
const age = Date.now() - Number(ts || 0);
|
|
64
|
+
if (age >= swrMs)
|
|
65
|
+
return null;
|
|
66
|
+
return {
|
|
67
|
+
data: normalizePersonalizedClusterFeed(data),
|
|
68
|
+
fresh: age < freshMs,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (_a) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
export const writePersonalizedClusterFeedSessionCache = ({ appId, deviceId, data, }) => {
|
|
76
|
+
try {
|
|
77
|
+
sessionStorage.setItem(getPersonalizedClusterFeedSessionCacheKey(appId, deviceId), JSON.stringify({ data, ts: Date.now() }));
|
|
78
|
+
}
|
|
79
|
+
catch (_a) { }
|
|
80
|
+
};
|
|
81
|
+
function fetchWithTimeout(url, options = {}, timeoutMs = 15000) {
|
|
82
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const upstreamSignal = options === null || options === void 0 ? void 0 : options.signal;
|
|
85
|
+
const abortFromUpstream = () => controller.abort(upstreamSignal === null || upstreamSignal === void 0 ? void 0 : upstreamSignal.reason);
|
|
86
|
+
if (upstreamSignal === null || upstreamSignal === void 0 ? void 0 : upstreamSignal.aborted) {
|
|
87
|
+
controller.abort(upstreamSignal.reason);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
upstreamSignal === null || upstreamSignal === void 0 ? void 0 : upstreamSignal.addEventListener("abort", abortFromUpstream, { once: true });
|
|
91
|
+
}
|
|
92
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
93
|
+
try {
|
|
94
|
+
return yield fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal }));
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
upstreamSignal === null || upstreamSignal === void 0 ? void 0 : upstreamSignal.removeEventListener("abort", abortFromUpstream);
|
|
98
|
+
clearTimeout(timeoutId);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function readJson(response) {
|
|
103
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
try {
|
|
105
|
+
return yield response.json();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const err = new Error((error === null || error === void 0 ? void 0 : error.message) || "Malformed JSON");
|
|
109
|
+
err.reason = "cluster-feed-parse";
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export function fetchPersonalizedClusterFeed({ appId, deviceId, customerProfilesBaseUrl, dashboardPreview, signal, }) {
|
|
115
|
+
var _a, _b, _c;
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
const baseUrl = normalizePersonalizedClusterFeedBaseUrl(customerProfilesBaseUrl);
|
|
118
|
+
const path = dashboardPreview
|
|
119
|
+
? `/api/v1/personalized-feed/${encodeURIComponent(appId)}/clusters/preview`
|
|
120
|
+
: `/api/v1/personalized-feed/${encodeURIComponent(appId)}/${encodeURIComponent(String(deviceId))}/cluster-feed`;
|
|
121
|
+
const url = `${baseUrl}${path}`;
|
|
122
|
+
let response;
|
|
123
|
+
try {
|
|
124
|
+
response = yield fetchWithTimeout(url, { signal });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const err = new Error((error === null || error === void 0 ? void 0 : error.message) || "Cluster feed request failed");
|
|
128
|
+
err.reason = "cluster-feed-network";
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const err = new Error(`Cluster feed HTTP ${response.status}`);
|
|
133
|
+
err.reason = "cluster-feed-http";
|
|
134
|
+
err.status = response.status;
|
|
135
|
+
err.retryAfter = (_c = (_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, "Retry-After")) !== null && _c !== void 0 ? _c : null;
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
return normalizePersonalizedClusterFeed(yield readJson(response));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export const fetchPersonalizedClusterFeedOnce = ({ requests, key, fetcher, retentionMs = 0, }) => {
|
|
142
|
+
const existing = requests.get(key);
|
|
143
|
+
if (existing)
|
|
144
|
+
return existing;
|
|
145
|
+
const promise = fetcher();
|
|
146
|
+
requests.set(key, promise);
|
|
147
|
+
const release = () => {
|
|
148
|
+
if (requests.get(key) === promise) {
|
|
149
|
+
requests.delete(key);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
if (retentionMs <= 0) {
|
|
155
|
+
release();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const timeoutId = setTimeout(release, retentionMs);
|
|
159
|
+
(_b = (_a = timeoutId).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
160
|
+
};
|
|
161
|
+
promise.then(cleanup, cleanup);
|
|
162
|
+
return promise;
|
|
163
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-personalized-cluster-feed-core.test.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-personalized-cluster-feed-core.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { createPersonalizedClusterFeedRequestKey, fetchPersonalizedClusterFeed, normalizePersonalizedClusterFeed, readPersonalizedClusterFeedSessionCache, writePersonalizedClusterFeedSessionCache, } from "./use-personalized-cluster-feed-core";
|
|
11
|
+
describe("personalized cluster feed core", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
sessionStorage.clear();
|
|
14
|
+
jest.restoreAllMocks();
|
|
15
|
+
global.fetch = jest.fn();
|
|
16
|
+
});
|
|
17
|
+
it("creates stable request keys for feed and preview modes", () => {
|
|
18
|
+
expect(createPersonalizedClusterFeedRequestKey({
|
|
19
|
+
appId: "app/id",
|
|
20
|
+
deviceId: "device/id",
|
|
21
|
+
customerProfilesBaseUrl: "https://profiles.test/",
|
|
22
|
+
})).toBe("personalized-cluster-feed:cluster-feed:https://profiles.test:app/id:device/id");
|
|
23
|
+
expect(createPersonalizedClusterFeedRequestKey({
|
|
24
|
+
appId: "app/id",
|
|
25
|
+
customerProfilesBaseUrl: "https://profiles.test/",
|
|
26
|
+
dashboardPreview: true,
|
|
27
|
+
})).toBe("personalized-cluster-feed:preview:https://profiles.test:app/id:preview");
|
|
28
|
+
});
|
|
29
|
+
it("normalizes payloads without adding absent frame fields", () => {
|
|
30
|
+
expect(normalizePersonalizedClusterFeed({
|
|
31
|
+
collectionIds: [123, "456", null],
|
|
32
|
+
explanations: "bad",
|
|
33
|
+
frames: [
|
|
34
|
+
{ bundleId: "bundle-1", layoutType: 42 },
|
|
35
|
+
{
|
|
36
|
+
layoutType: "complete_the_look",
|
|
37
|
+
complementProductIds: [1, "2", null],
|
|
38
|
+
},
|
|
39
|
+
null,
|
|
40
|
+
],
|
|
41
|
+
})).toEqual({
|
|
42
|
+
collectionIds: ["123", "456"],
|
|
43
|
+
explanations: {},
|
|
44
|
+
frames: [
|
|
45
|
+
{ bundleId: "bundle-1", layoutType: "42" },
|
|
46
|
+
{
|
|
47
|
+
layoutType: "complete_the_look",
|
|
48
|
+
complementProductIds: ["1", "2"],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it("round-trips session cache with freshness metadata", () => {
|
|
54
|
+
writePersonalizedClusterFeedSessionCache({
|
|
55
|
+
appId: "app-id",
|
|
56
|
+
deviceId: "device-id",
|
|
57
|
+
data: { collectionIds: ["1"], explanations: {}, frames: [] },
|
|
58
|
+
});
|
|
59
|
+
expect(readPersonalizedClusterFeedSessionCache({
|
|
60
|
+
appId: "app-id",
|
|
61
|
+
deviceId: "device-id",
|
|
62
|
+
freshMs: 1000,
|
|
63
|
+
swrMs: 2000,
|
|
64
|
+
})).toEqual({
|
|
65
|
+
data: { collectionIds: ["1"], explanations: {}, frames: [] },
|
|
66
|
+
fresh: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it("fetches cluster feed and preserves cluster-feed error reasons", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
var _a;
|
|
71
|
+
const fetchMock = global.fetch
|
|
72
|
+
.mockResolvedValueOnce({
|
|
73
|
+
ok: true,
|
|
74
|
+
status: 200,
|
|
75
|
+
json: () => Promise.resolve({
|
|
76
|
+
collectionIds: ["1"],
|
|
77
|
+
explanations: {},
|
|
78
|
+
frames: [{ layoutType: "ranked_collection" }],
|
|
79
|
+
}),
|
|
80
|
+
})
|
|
81
|
+
.mockResolvedValueOnce({
|
|
82
|
+
ok: false,
|
|
83
|
+
status: 500,
|
|
84
|
+
headers: { get: () => "3" },
|
|
85
|
+
});
|
|
86
|
+
yield expect(fetchPersonalizedClusterFeed({
|
|
87
|
+
appId: "app/id",
|
|
88
|
+
deviceId: "device/id",
|
|
89
|
+
customerProfilesBaseUrl: "https://profiles.test/",
|
|
90
|
+
})).resolves.toEqual({
|
|
91
|
+
collectionIds: ["1"],
|
|
92
|
+
explanations: {},
|
|
93
|
+
frames: [{ layoutType: "ranked_collection" }],
|
|
94
|
+
});
|
|
95
|
+
expect((_a = fetchMock.mock.calls[0]) === null || _a === void 0 ? void 0 : _a[0]).toBe("https://profiles.test/api/v1/personalized-feed/app%2Fid/device%2Fid/cluster-feed");
|
|
96
|
+
yield expect(fetchPersonalizedClusterFeed({
|
|
97
|
+
appId: "app-id",
|
|
98
|
+
deviceId: "device-id",
|
|
99
|
+
customerProfilesBaseUrl: "https://profiles.test",
|
|
100
|
+
})).rejects.toMatchObject({
|
|
101
|
+
reason: "cluster-feed-http",
|
|
102
|
+
status: 500,
|
|
103
|
+
retryAfter: "3",
|
|
104
|
+
});
|
|
105
|
+
}));
|
|
106
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type UsePersonalizedClusterFeedProps, type UsePersonalizedClusterFeedResult } from "./use-personalized-cluster-feed-core";
|
|
2
|
+
export type { PersonalizedClusterFeedData, PersonalizedClusterFeedFrame, PersonalizedClusterFeedStatus, UsePersonalizedClusterFeedProps, UsePersonalizedClusterFeedResult, } from "./use-personalized-cluster-feed-core";
|
|
3
|
+
export { createPersonalizedClusterFeedRequestKey, fetchPersonalizedClusterFeed, fetchPersonalizedClusterFeedOnce, isEmptyPersonalizedClusterFeed, normalizePersonalizedClusterFeed, normalizePersonalizedClusterFeedBaseUrl, readPersonalizedClusterFeedSessionCache, writePersonalizedClusterFeedSessionCache, } from "./use-personalized-cluster-feed-core";
|
|
4
|
+
export declare function usePersonalizedClusterFeed({ appId: rawAppId, deviceId: rawDeviceId, customerProfilesBaseUrl: rawCustomerProfilesBaseUrl, customerProfilesBaseURL, dashboardPreview, skip, }?: UsePersonalizedClusterFeedProps): UsePersonalizedClusterFeedResult;
|
|
5
|
+
//# sourceMappingURL=use-personalized-cluster-feed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-personalized-cluster-feed.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-personalized-cluster-feed.ts"],"names":[],"mappings":"AAIA,OAAO,EAUL,KAAK,+BAA+B,EACpC,KAAK,gCAAgC,EACtC,MAAM,sCAAsC,CAAA;AAE7C,YAAY,EACV,2BAA2B,EAC3B,4BAA4B,EAC5B,6BAA6B,EAC7B,+BAA+B,EAC/B,gCAAgC,GACjC,MAAM,sCAAsC,CAAA;AAE7C,OAAO,EACL,uCAAuC,EACvC,4BAA4B,EAC5B,gCAAgC,EAChC,8BAA8B,EAC9B,gCAAgC,EAChC,uCAAuC,EACvC,uCAAuC,EACvC,wCAAwC,GACzC,MAAM,sCAAsC,CAAA;AAkE7C,wBAAgB,0BAA0B,CAAC,EACzC,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,WAAW,EACrB,uBAAuB,EAAE,0BAA0B,EACnD,uBAAuB,EACvB,gBAAwB,EACxB,IAAY,GACb,GAAE,+BAAoC,GAAG,gCAAgC,CA2JzE"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { createPersonalizedClusterFeedRequestKey, fetchPersonalizedClusterFeed, fetchPersonalizedClusterFeedOnce, isEmptyPersonalizedClusterFeed, normalizePersonalizedClusterFeedBaseUrl, readPersonalizedClusterFeedSessionCache, writePersonalizedClusterFeedSessionCache, } from "./use-personalized-cluster-feed-core";
|
|
4
|
+
export { createPersonalizedClusterFeedRequestKey, fetchPersonalizedClusterFeed, fetchPersonalizedClusterFeedOnce, isEmptyPersonalizedClusterFeed, normalizePersonalizedClusterFeed, normalizePersonalizedClusterFeedBaseUrl, readPersonalizedClusterFeedSessionCache, writePersonalizedClusterFeedSessionCache, } from "./use-personalized-cluster-feed-core";
|
|
5
|
+
const FRESH_MS = 2 * 60 * 1000;
|
|
6
|
+
const SWR_MS = 30 * 60 * 1000;
|
|
7
|
+
const SINGLE_FLIGHT_RETENTION_MS = 5000;
|
|
8
|
+
const clusterFeedRequests = new Map();
|
|
9
|
+
const isEnabledDebugValue = (value) => ["1", "true", "yes", "on"].includes(String(value || "").toLowerCase());
|
|
10
|
+
const isPersonalizationDebugEnabled = () => {
|
|
11
|
+
var _a, _b;
|
|
12
|
+
if (typeof window === "undefined")
|
|
13
|
+
return false;
|
|
14
|
+
try {
|
|
15
|
+
const params = new URLSearchParams(((_a = window.location) === null || _a === void 0 ? void 0 : _a.search) || "");
|
|
16
|
+
if (isEnabledDebugValue(params.get("personalizationDebug")) ||
|
|
17
|
+
isEnabledDebugValue(params.get("debugPersonalization"))) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (_c) { }
|
|
22
|
+
try {
|
|
23
|
+
return isEnabledDebugValue((_b = window.localStorage) === null || _b === void 0 ? void 0 : _b.getItem("personalizationDebug"));
|
|
24
|
+
}
|
|
25
|
+
catch (_d) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const debugPersonalizationFallback = (event, context = {}) => {
|
|
30
|
+
if (!isPersonalizationDebugEnabled())
|
|
31
|
+
return;
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.info(Object.assign({ message: "Personalized cluster feed package fallback", event }, context));
|
|
34
|
+
};
|
|
35
|
+
const summarizeClusterFeed = (data) => {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
return ({
|
|
38
|
+
hasData: Boolean(data),
|
|
39
|
+
collectionCount: ((_a = data === null || data === void 0 ? void 0 : data.collectionIds) === null || _a === void 0 ? void 0 : _a.length) || 0,
|
|
40
|
+
frameCount: ((_b = data === null || data === void 0 ? void 0 : data.frames) === null || _b === void 0 ? void 0 : _b.length) || 0,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
const summarizeClusterFeedError = (error) => error
|
|
44
|
+
? {
|
|
45
|
+
reason: error.reason,
|
|
46
|
+
status: error.status,
|
|
47
|
+
message: error.message,
|
|
48
|
+
}
|
|
49
|
+
: null;
|
|
50
|
+
export function usePersonalizedClusterFeed({ appId: rawAppId, deviceId: rawDeviceId, customerProfilesBaseUrl: rawCustomerProfilesBaseUrl, customerProfilesBaseURL, dashboardPreview = false, skip = false, } = {}) {
|
|
51
|
+
const appId = rawAppId ? String(rawAppId) : "";
|
|
52
|
+
const deviceId = rawDeviceId ? String(rawDeviceId) : "";
|
|
53
|
+
const customerProfilesBaseUrl = normalizePersonalizedClusterFeedBaseUrl(rawCustomerProfilesBaseUrl || customerProfilesBaseURL);
|
|
54
|
+
const shouldSkip = Boolean(skip ||
|
|
55
|
+
!appId ||
|
|
56
|
+
!customerProfilesBaseUrl ||
|
|
57
|
+
(!dashboardPreview && !deviceId));
|
|
58
|
+
const requestKey = React.useMemo(() => {
|
|
59
|
+
if (shouldSkip)
|
|
60
|
+
return null;
|
|
61
|
+
return createPersonalizedClusterFeedRequestKey({
|
|
62
|
+
customerProfilesBaseUrl: customerProfilesBaseUrl,
|
|
63
|
+
appId,
|
|
64
|
+
deviceId,
|
|
65
|
+
dashboardPreview,
|
|
66
|
+
});
|
|
67
|
+
}, [appId, customerProfilesBaseUrl, dashboardPreview, deviceId, shouldSkip]);
|
|
68
|
+
const [data, setData] = React.useState(null);
|
|
69
|
+
const [error, setError] = React.useState(null);
|
|
70
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
debugPersonalizationFallback("effect", {
|
|
73
|
+
appId,
|
|
74
|
+
hasDeviceId: Boolean(deviceId),
|
|
75
|
+
hasCustomerProfilesBaseUrl: Boolean(customerProfilesBaseUrl),
|
|
76
|
+
dashboardPreview,
|
|
77
|
+
skip,
|
|
78
|
+
shouldSkip,
|
|
79
|
+
requestKey,
|
|
80
|
+
});
|
|
81
|
+
if (shouldSkip || !requestKey) {
|
|
82
|
+
setData(null);
|
|
83
|
+
setError(null);
|
|
84
|
+
setIsLoading(false);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let cancelled = false;
|
|
88
|
+
setError(null);
|
|
89
|
+
const cached = !dashboardPreview && deviceId
|
|
90
|
+
? readPersonalizedClusterFeedSessionCache({
|
|
91
|
+
appId,
|
|
92
|
+
deviceId,
|
|
93
|
+
freshMs: FRESH_MS,
|
|
94
|
+
swrMs: SWR_MS,
|
|
95
|
+
})
|
|
96
|
+
: null;
|
|
97
|
+
debugPersonalizationFallback("session-cache-result", {
|
|
98
|
+
appId,
|
|
99
|
+
cacheHit: Boolean(cached),
|
|
100
|
+
fresh: Boolean(cached === null || cached === void 0 ? void 0 : cached.fresh),
|
|
101
|
+
feed: summarizeClusterFeed(cached === null || cached === void 0 ? void 0 : cached.data),
|
|
102
|
+
});
|
|
103
|
+
if (cached) {
|
|
104
|
+
setData(cached.data);
|
|
105
|
+
if (cached.fresh) {
|
|
106
|
+
setIsLoading(false);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
setData(null);
|
|
112
|
+
}
|
|
113
|
+
setIsLoading(!cached);
|
|
114
|
+
debugPersonalizationFallback("fetch-start", {
|
|
115
|
+
appId,
|
|
116
|
+
hasDeviceId: Boolean(deviceId),
|
|
117
|
+
dashboardPreview,
|
|
118
|
+
requestKey,
|
|
119
|
+
});
|
|
120
|
+
fetchPersonalizedClusterFeedOnce({
|
|
121
|
+
requests: clusterFeedRequests,
|
|
122
|
+
key: requestKey,
|
|
123
|
+
retentionMs: SINGLE_FLIGHT_RETENTION_MS,
|
|
124
|
+
fetcher: () => fetchPersonalizedClusterFeed({
|
|
125
|
+
appId,
|
|
126
|
+
deviceId,
|
|
127
|
+
customerProfilesBaseUrl: customerProfilesBaseUrl,
|
|
128
|
+
dashboardPreview,
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
.then((result) => {
|
|
132
|
+
if (cancelled)
|
|
133
|
+
return;
|
|
134
|
+
if (!dashboardPreview && deviceId) {
|
|
135
|
+
writePersonalizedClusterFeedSessionCache({
|
|
136
|
+
appId,
|
|
137
|
+
deviceId,
|
|
138
|
+
data: result,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
debugPersonalizationFallback("fetch-success", {
|
|
142
|
+
appId,
|
|
143
|
+
dashboardPreview,
|
|
144
|
+
feed: summarizeClusterFeed(result),
|
|
145
|
+
});
|
|
146
|
+
setData(result);
|
|
147
|
+
setIsLoading(false);
|
|
148
|
+
})
|
|
149
|
+
.catch((err) => {
|
|
150
|
+
if (cancelled)
|
|
151
|
+
return;
|
|
152
|
+
debugPersonalizationFallback("fetch-error", {
|
|
153
|
+
appId,
|
|
154
|
+
dashboardPreview,
|
|
155
|
+
error: summarizeClusterFeedError(err),
|
|
156
|
+
});
|
|
157
|
+
setError(err);
|
|
158
|
+
setIsLoading(false);
|
|
159
|
+
});
|
|
160
|
+
return () => {
|
|
161
|
+
cancelled = true;
|
|
162
|
+
};
|
|
163
|
+
}, [
|
|
164
|
+
appId,
|
|
165
|
+
customerProfilesBaseUrl,
|
|
166
|
+
dashboardPreview,
|
|
167
|
+
deviceId,
|
|
168
|
+
requestKey,
|
|
169
|
+
shouldSkip,
|
|
170
|
+
skip,
|
|
171
|
+
]);
|
|
172
|
+
let status = "idle";
|
|
173
|
+
if (shouldSkip) {
|
|
174
|
+
status = "idle";
|
|
175
|
+
}
|
|
176
|
+
else if (data) {
|
|
177
|
+
status = isEmptyPersonalizedClusterFeed(data) ? "empty" : "ready";
|
|
178
|
+
}
|
|
179
|
+
else if (error) {
|
|
180
|
+
status = "error";
|
|
181
|
+
}
|
|
182
|
+
else if (isLoading) {
|
|
183
|
+
status = "loading";
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
data,
|
|
187
|
+
status,
|
|
188
|
+
error,
|
|
189
|
+
isLoading,
|
|
190
|
+
};
|
|
191
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./components/hooks/swr-retry";
|
|
|
10
10
|
export * from "./components/hooks/use-infinite-wishlist";
|
|
11
11
|
export * from "./components/hooks/use-recommendations";
|
|
12
12
|
export * from "./components/hooks/use-products";
|
|
13
|
+
export * from "./components/hooks/use-personalized-cluster-feed";
|
|
13
14
|
export * from "./components/hooks/use-order-details";
|
|
14
15
|
export * from "./components/hooks/use-scroll-direction";
|
|
15
16
|
export * from "./components/hooks/use-sort-filter";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./components/hooks/swr-retry";
|
|
|
10
10
|
export * from "./components/hooks/use-infinite-wishlist";
|
|
11
11
|
export * from "./components/hooks/use-recommendations";
|
|
12
12
|
export * from "./components/hooks/use-products";
|
|
13
|
+
export * from "./components/hooks/use-personalized-cluster-feed";
|
|
13
14
|
export * from "./components/hooks/use-order-details";
|
|
14
15
|
export * from "./components/hooks/use-scroll-direction";
|
|
15
16
|
export * from "./components/hooks/use-sort-filter";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditional-rendering.util.test.d.ts","sourceRoot":"","sources":["../../lib/conditional-rendering.util.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { evaluateConditions, getEnvState } from "./utils";
|
|
2
|
+
// Pure-function coverage for the v2 block visibility-condition logic:
|
|
3
|
+
// getEnvState (env assembly) and evaluateConditions/evaluateSingleCondition
|
|
4
|
+
// (matching + AND/OR/NOT). evaluateSingleCondition is not exported, so it is
|
|
5
|
+
// exercised through single-condition groups.
|
|
6
|
+
const baseLocation = { country: "US", language: "en", deviceId: "device-1" };
|
|
7
|
+
describe("getEnvState", () => {
|
|
8
|
+
it("maps a numeric userGroup to a string-valued tag", () => {
|
|
9
|
+
const env = getEnvState({ location: baseLocation, userGroup: 1 });
|
|
10
|
+
expect(env.userGroup).toEqual([{ value: "1" }]);
|
|
11
|
+
});
|
|
12
|
+
it("maps a string userGroup as-is", () => {
|
|
13
|
+
const env = getEnvState({ location: baseLocation, userGroup: "7" });
|
|
14
|
+
expect(env.userGroup).toEqual([{ value: "7" }]);
|
|
15
|
+
});
|
|
16
|
+
it("returns an empty userGroup tag list when userGroup is missing", () => {
|
|
17
|
+
expect(getEnvState({ location: baseLocation }).userGroup).toEqual([]);
|
|
18
|
+
expect(getEnvState({ location: baseLocation, userGroup: undefined }).userGroup).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it("exposes storage and searchParams as the underscored context keys", () => {
|
|
21
|
+
const params = new URLSearchParams("utm=spring");
|
|
22
|
+
const env = getEnvState({
|
|
23
|
+
location: baseLocation,
|
|
24
|
+
storage: { promo: "active" },
|
|
25
|
+
searchParams: params,
|
|
26
|
+
});
|
|
27
|
+
expect(env._storage).toEqual({ promo: "active" });
|
|
28
|
+
expect(env._searchParams).toBe(params);
|
|
29
|
+
});
|
|
30
|
+
it("defaults storage/searchParams to empty/null", () => {
|
|
31
|
+
const env = getEnvState({ location: baseLocation });
|
|
32
|
+
expect(env._storage).toEqual({});
|
|
33
|
+
expect(env._searchParams).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("evaluateConditions - userGroup", () => {
|
|
37
|
+
const ctx = { userGroup: [{ value: "1" }] };
|
|
38
|
+
const group = (operator, value) => ({
|
|
39
|
+
logic: "OR",
|
|
40
|
+
conditions: [{ type: "userGroup", operator, value }],
|
|
41
|
+
});
|
|
42
|
+
it("matches the user's group with equals", () => {
|
|
43
|
+
expect(evaluateConditions(group("equals", "1"), ctx)).toBe(true);
|
|
44
|
+
expect(evaluateConditions(group("equals", "2"), ctx)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("handles does not equal", () => {
|
|
47
|
+
expect(evaluateConditions(group("does not equal", "2"), ctx)).toBe(true);
|
|
48
|
+
expect(evaluateConditions(group("does not equal", "1"), ctx)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it("does not match when no group is present (fails closed)", () => {
|
|
51
|
+
// empty userGroup array (e.g. session.userGroup undefined at runtime)
|
|
52
|
+
expect(evaluateConditions(group("equals", "1"), { userGroup: [] })).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("evaluateConditions - storageVariable", () => {
|
|
56
|
+
const ctx = { _storage: { promo: "active" } };
|
|
57
|
+
const cond = (operator, key, value) => ({
|
|
58
|
+
logic: "OR",
|
|
59
|
+
conditions: [{ type: "storageVariable", operator, value: { key, value } }],
|
|
60
|
+
});
|
|
61
|
+
it("compares the stored value", () => {
|
|
62
|
+
expect(evaluateConditions(cond("equals", "promo", "active"), ctx)).toBe(true);
|
|
63
|
+
expect(evaluateConditions(cond("equals", "promo", "inactive"), ctx)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it("treats a missing key as not-equal", () => {
|
|
66
|
+
expect(evaluateConditions(cond("equals", "missing", "x"), ctx)).toBe(false);
|
|
67
|
+
expect(evaluateConditions(cond("does not equal", "missing", "x"), ctx)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("evaluateConditions - queryParam", () => {
|
|
71
|
+
const ctx = { _searchParams: new URLSearchParams("utm=spring") };
|
|
72
|
+
const cond = (operator, key, value) => ({
|
|
73
|
+
logic: "OR",
|
|
74
|
+
conditions: [{ type: "queryParam", operator, value: { key, value } }],
|
|
75
|
+
});
|
|
76
|
+
it("compares the query param value", () => {
|
|
77
|
+
expect(evaluateConditions(cond("equals", "utm", "spring"), ctx)).toBe(true);
|
|
78
|
+
expect(evaluateConditions(cond("equals", "utm", "fall"), ctx)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
it("treats a missing param as not-equal", () => {
|
|
81
|
+
expect(evaluateConditions(cond("equals", "missing", "x"), ctx)).toBe(false);
|
|
82
|
+
expect(evaluateConditions(cond("does not equal", "missing", "x"), ctx)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("evaluateConditions - logic operators", () => {
|
|
86
|
+
const ctx = {
|
|
87
|
+
userGroup: [{ value: "1" }],
|
|
88
|
+
_storage: { promo: "active" },
|
|
89
|
+
};
|
|
90
|
+
const userGroupIs1 = { type: "userGroup", operator: "equals", value: "1" };
|
|
91
|
+
const userGroupIs2 = { type: "userGroup", operator: "equals", value: "2" };
|
|
92
|
+
it("AND requires every condition", () => {
|
|
93
|
+
expect(evaluateConditions({ logic: "AND", conditions: [userGroupIs1, userGroupIs1] }, ctx)).toBe(true);
|
|
94
|
+
expect(evaluateConditions({ logic: "AND", conditions: [userGroupIs1, userGroupIs2] }, ctx)).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
it("OR requires at least one condition", () => {
|
|
97
|
+
expect(evaluateConditions({ logic: "OR", conditions: [userGroupIs2, userGroupIs1] }, ctx)).toBe(true);
|
|
98
|
+
expect(evaluateConditions({ logic: "OR", conditions: [userGroupIs2, userGroupIs2] }, ctx)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
it("NOT negates an all-true group", () => {
|
|
101
|
+
expect(evaluateConditions({ logic: "NOT", conditions: [userGroupIs1] }, ctx)).toBe(false);
|
|
102
|
+
expect(evaluateConditions({ logic: "NOT", conditions: [userGroupIs2] }, ctx)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
it("evaluates nested groups", () => {
|
|
105
|
+
expect(evaluateConditions({
|
|
106
|
+
logic: "AND",
|
|
107
|
+
conditions: [
|
|
108
|
+
userGroupIs1,
|
|
109
|
+
{ logic: "OR", conditions: [userGroupIs2, userGroupIs1] },
|
|
110
|
+
],
|
|
111
|
+
}, ctx)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it("passes when there are no conditions", () => {
|
|
114
|
+
expect(evaluateConditions({ logic: "AND", conditions: [] }, ctx)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
package/dist/styles.css
CHANGED
|
@@ -717,9 +717,15 @@ video {
|
|
|
717
717
|
left: 0px;
|
|
718
718
|
right: 0px;
|
|
719
719
|
}
|
|
720
|
+
.-bottom-1 {
|
|
721
|
+
bottom: -0.25rem;
|
|
722
|
+
}
|
|
720
723
|
.-bottom-12 {
|
|
721
724
|
bottom: -3rem;
|
|
722
725
|
}
|
|
726
|
+
.-left-1 {
|
|
727
|
+
left: -0.25rem;
|
|
728
|
+
}
|
|
723
729
|
.-left-12 {
|
|
724
730
|
left: -3rem;
|
|
725
731
|
}
|
|
@@ -741,6 +747,9 @@ video {
|
|
|
741
747
|
.bottom-2 {
|
|
742
748
|
bottom: 0.5rem;
|
|
743
749
|
}
|
|
750
|
+
.bottom-3 {
|
|
751
|
+
bottom: 0.75rem;
|
|
752
|
+
}
|
|
744
753
|
.bottom-4 {
|
|
745
754
|
bottom: 1rem;
|
|
746
755
|
}
|
|
@@ -831,6 +840,9 @@ video {
|
|
|
831
840
|
.z-\[1\] {
|
|
832
841
|
z-index: 1;
|
|
833
842
|
}
|
|
843
|
+
.z-\[2\] {
|
|
844
|
+
z-index: 2;
|
|
845
|
+
}
|
|
834
846
|
.z-\[30\] {
|
|
835
847
|
z-index: 30;
|
|
836
848
|
}
|
|
@@ -1015,6 +1027,9 @@ video {
|
|
|
1015
1027
|
.mt-4 {
|
|
1016
1028
|
margin-top: 1rem;
|
|
1017
1029
|
}
|
|
1030
|
+
.mt-6 {
|
|
1031
|
+
margin-top: 1.5rem;
|
|
1032
|
+
}
|
|
1018
1033
|
.mt-auto {
|
|
1019
1034
|
margin-top: auto;
|
|
1020
1035
|
}
|
|
@@ -1072,6 +1087,12 @@ video {
|
|
|
1072
1087
|
.hidden {
|
|
1073
1088
|
display: none;
|
|
1074
1089
|
}
|
|
1090
|
+
.aspect-\[327\/280\] {
|
|
1091
|
+
aspect-ratio: 327/280;
|
|
1092
|
+
}
|
|
1093
|
+
.aspect-\[327\/316\] {
|
|
1094
|
+
aspect-ratio: 327/316;
|
|
1095
|
+
}
|
|
1075
1096
|
.aspect-productImages {
|
|
1076
1097
|
aspect-ratio: var(--productImage-aspectRatio);
|
|
1077
1098
|
}
|
|
@@ -1120,6 +1141,9 @@ video {
|
|
|
1120
1141
|
.h-3 {
|
|
1121
1142
|
height: 0.75rem;
|
|
1122
1143
|
}
|
|
1144
|
+
.h-3\.5 {
|
|
1145
|
+
height: 0.875rem;
|
|
1146
|
+
}
|
|
1123
1147
|
.h-32 {
|
|
1124
1148
|
height: 8rem;
|
|
1125
1149
|
}
|
|
@@ -1241,9 +1265,15 @@ video {
|
|
|
1241
1265
|
.min-h-\[210px\] {
|
|
1242
1266
|
min-height: 210px;
|
|
1243
1267
|
}
|
|
1268
|
+
.min-h-\[230px\] {
|
|
1269
|
+
min-height: 230px;
|
|
1270
|
+
}
|
|
1244
1271
|
.min-h-\[24px\] {
|
|
1245
1272
|
min-height: 24px;
|
|
1246
1273
|
}
|
|
1274
|
+
.min-h-\[260px\] {
|
|
1275
|
+
min-height: 260px;
|
|
1276
|
+
}
|
|
1247
1277
|
.min-h-\[36px\] {
|
|
1248
1278
|
min-height: 36px;
|
|
1249
1279
|
}
|
|
@@ -1289,6 +1319,9 @@ video {
|
|
|
1289
1319
|
.w-11 {
|
|
1290
1320
|
width: 2.75rem;
|
|
1291
1321
|
}
|
|
1322
|
+
.w-11\/12 {
|
|
1323
|
+
width: 91.666667%;
|
|
1324
|
+
}
|
|
1292
1325
|
.w-12 {
|
|
1293
1326
|
width: 3rem;
|
|
1294
1327
|
}
|
|
@@ -1352,9 +1385,18 @@ video {
|
|
|
1352
1385
|
.w-\[120\%\] {
|
|
1353
1386
|
width: 120%;
|
|
1354
1387
|
}
|
|
1388
|
+
.w-\[120px\] {
|
|
1389
|
+
width: 120px;
|
|
1390
|
+
}
|
|
1391
|
+
.w-\[132px\] {
|
|
1392
|
+
width: 132px;
|
|
1393
|
+
}
|
|
1355
1394
|
.w-\[138px\] {
|
|
1356
1395
|
width: 138px;
|
|
1357
1396
|
}
|
|
1397
|
+
.w-\[140px\] {
|
|
1398
|
+
width: 140px;
|
|
1399
|
+
}
|
|
1358
1400
|
.w-\[180px\] {
|
|
1359
1401
|
width: 180px;
|
|
1360
1402
|
}
|
|
@@ -1385,6 +1427,9 @@ video {
|
|
|
1385
1427
|
.w-\[65\%\] {
|
|
1386
1428
|
width: 65%;
|
|
1387
1429
|
}
|
|
1430
|
+
.w-\[85\%\] {
|
|
1431
|
+
width: 85%;
|
|
1432
|
+
}
|
|
1388
1433
|
.w-\[calc\(100\%-2rem\)\] {
|
|
1389
1434
|
width: calc(100% - 2rem);
|
|
1390
1435
|
}
|
|
@@ -1420,6 +1465,12 @@ video {
|
|
|
1420
1465
|
.min-w-8 {
|
|
1421
1466
|
min-width: 2rem;
|
|
1422
1467
|
}
|
|
1468
|
+
.min-w-\[150px\] {
|
|
1469
|
+
min-width: 150px;
|
|
1470
|
+
}
|
|
1471
|
+
.min-w-\[53px\] {
|
|
1472
|
+
min-width: 53px;
|
|
1473
|
+
}
|
|
1423
1474
|
.min-w-\[8rem\] {
|
|
1424
1475
|
min-width: 8rem;
|
|
1425
1476
|
}
|
|
@@ -1436,12 +1487,27 @@ video {
|
|
|
1436
1487
|
.max-w-\[198px\] {
|
|
1437
1488
|
max-width: 198px;
|
|
1438
1489
|
}
|
|
1490
|
+
.max-w-\[220px\] {
|
|
1491
|
+
max-width: 220px;
|
|
1492
|
+
}
|
|
1493
|
+
.max-w-\[360px\] {
|
|
1494
|
+
max-width: 360px;
|
|
1495
|
+
}
|
|
1496
|
+
.max-w-\[480px\] {
|
|
1497
|
+
max-width: 480px;
|
|
1498
|
+
}
|
|
1499
|
+
.max-w-\[50\%\] {
|
|
1500
|
+
max-width: 50%;
|
|
1501
|
+
}
|
|
1439
1502
|
.max-w-\[5rem\] {
|
|
1440
1503
|
max-width: 5rem;
|
|
1441
1504
|
}
|
|
1442
1505
|
.max-w-\[75\%\] {
|
|
1443
1506
|
max-width: 75%;
|
|
1444
1507
|
}
|
|
1508
|
+
.max-w-\[min\(100\%\2c 440px\)\] {
|
|
1509
|
+
max-width: min(100%,440px);
|
|
1510
|
+
}
|
|
1445
1511
|
.max-w-full {
|
|
1446
1512
|
max-width: 100%;
|
|
1447
1513
|
}
|
|
@@ -1484,6 +1550,9 @@ video {
|
|
|
1484
1550
|
.basis-\[300px\] {
|
|
1485
1551
|
flex-basis: 300px;
|
|
1486
1552
|
}
|
|
1553
|
+
.basis-\[88\%\] {
|
|
1554
|
+
flex-basis: 88%;
|
|
1555
|
+
}
|
|
1487
1556
|
.basis-full {
|
|
1488
1557
|
flex-basis: 100%;
|
|
1489
1558
|
}
|
|
@@ -1559,6 +1628,16 @@ video {
|
|
|
1559
1628
|
--tw-scale-y: 1.25;
|
|
1560
1629
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
1561
1630
|
}
|
|
1631
|
+
.scale-150 {
|
|
1632
|
+
--tw-scale-x: 1.5;
|
|
1633
|
+
--tw-scale-y: 1.5;
|
|
1634
|
+
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
1635
|
+
}
|
|
1636
|
+
.scale-\[1\.2\] {
|
|
1637
|
+
--tw-scale-x: 1.2;
|
|
1638
|
+
--tw-scale-y: 1.2;
|
|
1639
|
+
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
1640
|
+
}
|
|
1562
1641
|
.transform {
|
|
1563
1642
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
1564
1643
|
}
|
|
@@ -2105,9 +2184,6 @@ video {
|
|
|
2105
2184
|
--tw-bg-opacity: 1;
|
|
2106
2185
|
background-color: rgb(253 255 218 / var(--tw-bg-opacity, 1));
|
|
2107
2186
|
}
|
|
2108
|
-
.bg-\[var\(--coreColors-pageColor\2c \#ffffff\)\] {
|
|
2109
|
-
background-color: var(--coreColors-pageColor,#ffffff);
|
|
2110
|
-
}
|
|
2111
2187
|
.bg-\[var\(--stateColors-skeleton\)\] {
|
|
2112
2188
|
background-color: var(--stateColors-skeleton);
|
|
2113
2189
|
}
|
|
@@ -2425,6 +2501,9 @@ video {
|
|
|
2425
2501
|
.pr-4 {
|
|
2426
2502
|
padding-right: 1rem;
|
|
2427
2503
|
}
|
|
2504
|
+
.pr-6 {
|
|
2505
|
+
padding-right: 1.5rem;
|
|
2506
|
+
}
|
|
2428
2507
|
.pt-0 {
|
|
2429
2508
|
padding-top: 0px;
|
|
2430
2509
|
}
|
|
@@ -2814,9 +2893,15 @@ video {
|
|
|
2814
2893
|
.opacity-50 {
|
|
2815
2894
|
opacity: 0.5;
|
|
2816
2895
|
}
|
|
2896
|
+
.opacity-60 {
|
|
2897
|
+
opacity: 0.6;
|
|
2898
|
+
}
|
|
2817
2899
|
.opacity-70 {
|
|
2818
2900
|
opacity: 0.7;
|
|
2819
2901
|
}
|
|
2902
|
+
.opacity-\[0\.42\] {
|
|
2903
|
+
opacity: 0.42;
|
|
2904
|
+
}
|
|
2820
2905
|
.shadow {
|
|
2821
2906
|
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
2822
2907
|
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
|
@@ -2827,6 +2912,11 @@ video {
|
|
|
2827
2912
|
--tw-shadow-colored: 0 0 6px 0 var(--tw-shadow-color);
|
|
2828
2913
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
|
2829
2914
|
}
|
|
2915
|
+
.shadow-\[0_2px_8px_rgba\(0\2c 0\2c 0\2c 0\.4\)\] {
|
|
2916
|
+
--tw-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
|
2917
|
+
--tw-shadow-colored: 0 2px 8px var(--tw-shadow-color);
|
|
2918
|
+
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
|
2919
|
+
}
|
|
2830
2920
|
.shadow-lg {
|
|
2831
2921
|
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
2832
2922
|
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
|
@@ -2909,6 +2999,14 @@ video {
|
|
|
2909
2999
|
--tw-blur: blur(8px);
|
|
2910
3000
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
|
2911
3001
|
}
|
|
3002
|
+
.blur-2xl {
|
|
3003
|
+
--tw-blur: blur(40px);
|
|
3004
|
+
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
|
3005
|
+
}
|
|
3006
|
+
.brightness-75 {
|
|
3007
|
+
--tw-brightness: brightness(.75);
|
|
3008
|
+
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
|
3009
|
+
}
|
|
2912
3010
|
.drop-shadow {
|
|
2913
3011
|
--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
|
|
2914
3012
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
|
@@ -2934,6 +3032,11 @@ video {
|
|
|
2934
3032
|
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
|
2935
3033
|
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
|
2936
3034
|
}
|
|
3035
|
+
.backdrop-blur-md {
|
|
3036
|
+
--tw-backdrop-blur: blur(12px);
|
|
3037
|
+
-webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
|
3038
|
+
backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
|
|
3039
|
+
}
|
|
2937
3040
|
.transition {
|
|
2938
3041
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
|
|
2939
3042
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|