@justeattakeaway/pie-cookie-banner 0.1.0 → 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.d.ts CHANGED
@@ -1,13 +1,115 @@
1
1
  import type { CSSResult } from 'lit';
2
2
  import type { LitElement } from 'lit';
3
- import type { TemplateResult } from 'lit-html';
3
+ import { PieToggleSwitch } from '@justeattakeaway/pie-toggle-switch';
4
+ import type { TemplateResult } from 'lit';
4
5
 
5
6
  export declare interface CookieBannerProps {
6
7
  }
7
8
 
9
+ /**
10
+ * Event name for when all cookies are accepted.
11
+ *
12
+ * @constant
13
+ */
14
+ export declare const ON_COOKIE_BANNER_ACCEPT_ALL = "pie-cookie-banner-accept-all";
15
+
16
+ /**
17
+ * Event name for when a user clicks manage preferences.
18
+ *
19
+ * @constant
20
+ */
21
+ export declare const ON_COOKIE_BANNER_MANAGE_PREFS = "pie-cookie-banner-manage-prefs";
22
+
23
+ /**
24
+ * Event name for when all only necessary cookies are accepted.
25
+ *
26
+ * @constant
27
+ */
28
+ export declare const ON_COOKIE_BANNER_NECESSARY_ONLY = "pie-cookie-banner-necessary-only";
29
+
30
+ /**
31
+ * Event name for when a user clicks save preferences.
32
+ *
33
+ * @constant
34
+ */
35
+ export declare const ON_COOKIE_BANNER_PREFS_SAVED = "pie-cookie-banner-prefs-saved";
36
+
37
+ /**
38
+ * @event {CustomEvent} pie-cookie-banner-accept-all - when all cookies are accepted.
39
+ * @event {CustomEvent} pie-cookie-banner-necessary-only - when all only necessary cookies are accepted.
40
+ * @event {CustomEvent} pie-cookie-banner-manage-prefs - when a user clicks manage preferences.
41
+ * @event {CustomEvent} pie-cookie-banner-prefs-saved - when a user clicks save preferences.
42
+ */
8
43
  export declare class PieCookieBanner extends LitElement implements CookieBannerProps {
44
+ private _isCookieBannerHidden;
45
+ private _isModalOpen;
46
+ _preferencesNodes: NodeListOf<PieToggleSwitch>;
47
+ /**
48
+ * Handles closing the modal and re-displaying the cookie banner
49
+ * and making the cookie banner visible
50
+ */
51
+ private _displayCookieBanner;
52
+ /**
53
+ * Handles saving the user cookie preferences, closing the modal and the cookie banner
54
+ * Creates a state object for the save event, indicating the isChecked status
55
+ * of each preference except for the `all` preference.
56
+ * @example {
57
+ * functional: false,
58
+ * necessary: true
59
+ * }
60
+ */
61
+ private _handlePreferencesSaved;
62
+ /**
63
+ * Note: We should aim to have a shareable event helper system to allow
64
+ * us to share this across components in-future.
65
+ *
66
+ * Dispatch a custom event.
67
+ *
68
+ * To be used whenever we have behavioral events we want to
69
+ * bubble up through the cookie banner.
70
+ *
71
+ * @param {string} eventType
72
+ * @param {any} detail
73
+ */
74
+ private _dispatchCookieBannerCustomEvent;
75
+ /**
76
+ * Opens the manage preferences modal and emits an event letting users know
77
+ */
78
+ private _openManagePreferencesModal;
79
+ /**
80
+ * Handles the logic of the toggle switch nodes (preferences).
81
+ * Clicking the “all” toggle switch should turn on all preferences.
82
+ * When the “all” toggle is checked, and one of the other preferences is clicked,
83
+ * then the “all” toggle should be unchecked.
84
+ * if all toggle switches are checked, the `all` toggle switch should
85
+ * be turned on automatically
86
+ */
87
+ private _handleToggleStates;
88
+ /**
89
+ * Renders the content of the preference item.
90
+ * @private
91
+ */
92
+ private renderPreference;
93
+ /**
94
+ * Renders the modal content.
95
+ * @private
96
+ */
97
+ private renderModalContent;
9
98
  render(): TemplateResult<1>;
10
99
  static styles: CSSResult;
11
100
  }
12
101
 
102
+ export declare interface Preference {
103
+ id: PreferenceIds;
104
+ title: string;
105
+ description?: string;
106
+ isDisabled?: boolean;
107
+ isChecked?: boolean;
108
+ hasDivider?: boolean;
109
+ }
110
+
111
+ export declare type PreferenceIds = 'all' | 'necessary' | 'functional' | 'analytical' | 'personalized';
112
+
113
+ export declare const preferences: Preference[];
114
+
13
115
  export { }
package/dist/index.js CHANGED
@@ -1,10 +1,147 @@
1
- import { unsafeCSS as i, LitElement as n, html as t } from "lit";
2
- const a = `*{margin:0}.c-cookieBanner{--cb-font-family: var(--dt-font-interactive-m-family);--cb-font-size: calc(var(--dt-font-body-l-size) * 1px);--cb-line-height: calc(var(--dt-font-body-l-line-height) * 1px);--cb-font-weight: var(--dt-font-body-l-weight);--cb-padding-inline: var(--dt-spacing-d);--cb-padding-block: var(--dt-spacing-d);--cb-offset: 0;color-scheme:only dark;background-color:var(--dt-color-background-dark);color:var(--dt-color-content-inverse);font-family:var(--cb-font-family);font-size:var(--cb-font-size);line-height:var(--cb-line-height);font-weight:var(--cb-font-weight);padding-block-start:var(--cb-padding-block);padding-block-end:var(--cb-padding-block);max-height:432px;position:fixed;bottom:var(--cb-offset);left:var(--cb-offset);right:var(--cb-offset);border-top-left-radius:var(--dt-radius-rounded-c);border-top-right-radius:var(--dt-radius-rounded-c)}@media (min-width: 700px) and (orientation: landscape){.c-cookieBanner{--cb-padding-inline: var(--dt-spacing-f);--cb-offset: var(--dt-spacing-d);max-height:90%;border-bottom-left-radius:var(--dt-radius-rounded-c);border-bottom-right-radius:var(--dt-radius-rounded-c)}}.c-cookieBanner-title,.c-cookieBanner-body,.c-cookieBanner-actions{padding-inline-start:var(--cb-padding-inline);padding-inline-end:var(--cb-padding-inline)}.c-cookieBanner-title{--cb-title-font-size: var(--dt-font-heading-s-size--narrow);--cb-title-line-height: var(--dt-font-heading-s-line-height--narrow);font-size:calc(var(--cb-title-font-size) * 1px);font-weight:var(--dt-font-heading-s-weight);line-height:calc(var(--cb-title-line-height) * 1px)}@media (min-width: 700px) and (orientation: landscape){.c-cookieBanner-title{--cb-title-font-size: var(--dt-font-heading-s-size--wide);--cb-title-line-height: var(--dt-font-heading-s-line-height--wide)}}.c-cookieBanner-body{--cb-scroll-shadow-color: var(--dt-color-black);margin-block-start:var(--dt-spacing-a);max-height:200px;overflow-y:auto;background:linear-gradient(to bottom,transparent,var(--dt-color-background-dark) 75%) center bottom,linear-gradient(transparent,var(--cb-scroll-shadow-color)) center bottom;background-repeat:no-repeat;background-size:100% 48px,100% 8px;background-attachment:local,scroll}@media (min-width: 700px) and (orientation: landscape){.c-cookieBanner-body{max-height:150px}}.c-cookieBanner-actions{--cb-actions-flex-dir: column;margin-block-start:var(--dt-spacing-d);display:flex;gap:var(--dt-spacing-d);flex-direction:var(--cb-actions-flex-dir)}.c-cookieBanner-actions>pie-link{width:max-content;margin-inline-start:auto;margin-inline-end:auto}@media (min-width: 700px) and (orientation: landscape){.c-cookieBanner-actions{--cb-actions-flex-dir: row-reverse;justify-content:flex-start;align-items:center}.c-cookieBanner-actions>pie-link{margin-inline-start:0;margin-inline-end:0}}
3
- `, o = "pie-cookie-banner";
4
- class e extends n {
1
+ import { unsafeCSS as p, LitElement as g, html as r, nothing as l } from "lit";
2
+ import { state as h, queryAll as f } from "lit/decorators.js";
3
+ import { repeat as b } from "lit/directives/repeat.js";
4
+ const k = `*{margin:0}.c-cookieBanner{--cb-font-family: var(--dt-font-interactive-m-family);--cb-font-size: calc(var(--dt-font-body-l-size) * 1px);--cb-line-height: calc(var(--dt-font-body-l-line-height) * 1px);--cb-font-weight: var(--dt-font-body-l-weight);--cb-padding-inline: var(--dt-spacing-d);--cb-padding-block: var(--dt-spacing-d);--cb-offset: 0;color-scheme:only dark;background-color:var(--dt-color-background-dark);color:var(--dt-color-content-inverse);font-family:var(--cb-font-family);font-size:var(--cb-font-size);line-height:var(--cb-line-height);font-weight:var(--cb-font-weight);padding-block-start:var(--cb-padding-block);padding-block-end:var(--cb-padding-block);max-height:432px;position:fixed;bottom:var(--cb-offset);left:var(--cb-offset);right:var(--cb-offset);border-top-left-radius:var(--dt-radius-rounded-c);border-top-right-radius:var(--dt-radius-rounded-c)}@media (min-width: 700px) and (orientation: landscape){.c-cookieBanner{--cb-padding-inline: var(--dt-spacing-f);--cb-offset: var(--dt-spacing-d);max-height:90%;border-bottom-left-radius:var(--dt-radius-rounded-c);border-bottom-right-radius:var(--dt-radius-rounded-c)}}.c-cookieBanner[isCookieBannerHidden]{display:none}.c-cookieBanner-title,.c-cookieBanner-body,.c-cookieBanner-actions{padding-inline-start:var(--cb-padding-inline);padding-inline-end:var(--cb-padding-inline)}.c-cookieBanner-title{--cb-title-font-size: var(--dt-font-heading-s-size--narrow);--cb-title-line-height: var(--dt-font-heading-s-line-height--narrow);font-size:calc(var(--cb-title-font-size) * 1px);font-weight:var(--dt-font-heading-s-weight);line-height:calc(var(--cb-title-line-height) * 1px)}@media (min-width: 700px) and (orientation: landscape){.c-cookieBanner-title{--cb-title-font-size: var(--dt-font-heading-s-size--wide);--cb-title-line-height: var(--dt-font-heading-s-line-height--wide)}}.c-cookieBanner-body{--cb-scroll-shadow-color: var(--dt-color-black);margin-block-start:var(--dt-spacing-a);max-height:200px;overflow-y:auto;background:linear-gradient(to bottom,transparent,var(--dt-color-background-dark) 75%) center bottom,linear-gradient(transparent,var(--cb-scroll-shadow-color)) center bottom;background-repeat:no-repeat;background-size:100% 48px,100% 8px;background-attachment:local,scroll}@media (min-width: 700px) and (orientation: landscape){.c-cookieBanner-body{max-height:150px}}.c-cookieBanner-actions{--cb-actions-flex-dir: column;margin-block-start:var(--dt-spacing-d);display:flex;gap:var(--dt-spacing-d);flex-direction:var(--cb-actions-flex-dir)}.c-cookieBanner-actions>pie-link{text-align:center;align-self:center}@media (min-width: 700px) and (orientation: landscape){.c-cookieBanner-actions{--cb-actions-flex-dir: row-reverse;justify-content:flex-start;align-items:center}}.c-cookieBanner-subheading{--cb-subheading-font-size: var(--dt-font-heading-s-size--narrow);--cb-subheading-line-height: var(--dt-font-heading-s-line-height--narrow);font-size:calc(var(--cb-subheading-font-size) * 1px);font-weight:var(--dt-font-heading-s-weight);line-height:calc(var(--cb-subheading-line-height) * 1px)}@media (min-width: 700px) and (orientation: landscape){.c-cookieBanner-subheading{--cb-subheading-font-size: var(--dt-font-heading-s-size--wide);--cb-subheading-line-height: var(--dt-font-heading-s-line-height--wide)}}.c-cookieBanner-description{font-size:calc(var(--dt-font-body-s-size) * 1px);line-height:calc(var(--dt-font-body-s-line-height) * 1px)}.c-cookieBanner-preference{display:flex;gap:var(--dt-spacing-d);justify-content:space-between;margin-block:var(--dt-spacing-e)}.c-cookieBanner-preference p{margin-block-start:var(--dt-spacing-b)}.c-cookieBanner-preference:last-child{margin-block-end:0}
5
+ `, v = "pie-cookie-banner-accept-all", u = "pie-cookie-banner-necessary-only", m = "pie-cookie-banner-manage-prefs", y = "pie-cookie-banner-prefs-saved", _ = [
6
+ {
7
+ id: "all",
8
+ title: "Turn on all",
9
+ hasDivider: !0
10
+ },
11
+ {
12
+ id: "necessary",
13
+ title: "Necessary",
14
+ description: "These cookies are necessary to ensure that the website and its features function properly. Services you have asked for cannot be provided without these cookies.",
15
+ isDisabled: !0,
16
+ isChecked: !0
17
+ },
18
+ {
19
+ id: "functional",
20
+ title: "Functional",
21
+ description: "These cookies allow the website to remember the choices you make to give you better functionality and personal features."
22
+ },
23
+ {
24
+ id: "analytical",
25
+ title: "Analytical",
26
+ description: "These analytical cookies, including statistics, are used to understand how visitors interact with the website and we can measure and improve the performance of our website."
27
+ },
28
+ {
29
+ id: "personalized",
30
+ title: "Personalized (targeting and advertising)",
31
+ description: "These marketing cookies are used to tailor the delivery of information to you based upon your interest and to measure the effectiveness of such advertisements, both on our website and our advertising partners' websites."
32
+ }
33
+ ];
34
+ var B = Object.defineProperty, w = Object.getOwnPropertyDescriptor, d = (c, e, t, n) => {
35
+ for (var i = n > 1 ? void 0 : n ? w(e, t) : e, a = c.length - 1, o; a >= 0; a--)
36
+ (o = c[a]) && (i = (n ? o(e, t, i) : o(i)) || i);
37
+ return n && i && B(e, t, i), i;
38
+ };
39
+ const C = "pie-cookie-banner";
40
+ class s extends g {
41
+ constructor() {
42
+ super(...arguments), this._isCookieBannerHidden = !1, this._isModalOpen = !1, this._dispatchCookieBannerCustomEvent = (e, t) => {
43
+ const n = new CustomEvent(e, {
44
+ bubbles: !0,
45
+ composed: !0,
46
+ detail: t
47
+ });
48
+ this.dispatchEvent(n);
49
+ }, this._openManagePreferencesModal = () => {
50
+ this._isCookieBannerHidden = !0, this._dispatchCookieBannerCustomEvent(m), this._isModalOpen = !0;
51
+ }, this._handleToggleStates = (e) => {
52
+ const { id: t } = e == null ? void 0 : e.currentTarget, n = [...this._preferencesNodes].find(({ id: i }) => i === "all");
53
+ if (t === n.id) {
54
+ const i = e.detail;
55
+ this._preferencesNodes.forEach((a) => {
56
+ a.isChecked = a.isDisabled ? a.isChecked : i;
57
+ });
58
+ } else
59
+ n.isChecked = [...this._preferencesNodes].filter(({ id: i }) => i !== "all").every(({ isChecked: i }) => i);
60
+ };
61
+ }
62
+ /**
63
+ * Handles closing the modal and re-displaying the cookie banner
64
+ * and making the cookie banner visible
65
+ */
66
+ _displayCookieBanner() {
67
+ this._isModalOpen = !1, this._isCookieBannerHidden = !1;
68
+ }
69
+ /**
70
+ * Handles saving the user cookie preferences, closing the modal and the cookie banner
71
+ * Creates a state object for the save event, indicating the isChecked status
72
+ * of each preference except for the `all` preference.
73
+ * @example {
74
+ * functional: false,
75
+ * necessary: true
76
+ * }
77
+ */
78
+ _handlePreferencesSaved() {
79
+ let e = {};
80
+ [...this._preferencesNodes].filter(({ id: t }) => t !== "all").forEach(({ id: t, isChecked: n }) => {
81
+ e = { ...e, [t]: n };
82
+ }), this._dispatchCookieBannerCustomEvent(y, e), this._isModalOpen = !1, this._isCookieBannerHidden = !0;
83
+ }
84
+ /**
85
+ * Renders the content of the preference item.
86
+ * @private
87
+ */
88
+ renderPreference({
89
+ id: e,
90
+ title: t,
91
+ description: n,
92
+ isChecked: i,
93
+ isDisabled: a,
94
+ hasDivider: o
95
+ }) {
96
+ return r`
97
+ <div class="c-cookieBanner-preference">
98
+ <div>
99
+ <h3 class="c-cookieBanner-subheading">${t}</h3>
100
+ ${n ? r`<p class="c-cookieBanner-description">${n}</p>` : l}
101
+ </div>
102
+ <pie-toggle-switch
103
+ id="${e}"
104
+ ?isChecked="${i}"
105
+ ?isDisabled="${a}"
106
+ @pie-toggle-switch-changed="${this._handleToggleStates}"/>
107
+ </div>
108
+ ${o ? r`<pie-divider></pie-divider>` : l}`;
109
+ }
110
+ /**
111
+ * Renders the modal content.
112
+ * @private
113
+ */
114
+ renderModalContent() {
115
+ return r`
116
+ <p class="c-cookieBanner-description">You can find all the information in the
117
+ <pie-link href="#" size="small" target="_blank">Cookie Statement</pie-link> and
118
+ <pie-link href="#" size="small" target="_blank">Cookie technology list</pie-link>.
119
+ </p>
120
+ ${b(
121
+ _,
122
+ ({ id: e }) => e,
123
+ (e) => this.renderPreference(e)
124
+ )}`;
125
+ }
5
126
  render() {
6
- return t`
7
- <aside data-test-id="pie-cookie-banner" class="c-cookieBanner">
127
+ const e = {
128
+ text: "Save",
129
+ variant: "primary",
130
+ ariaLabel: "Save your cookie preferences"
131
+ };
132
+ return r`
133
+ <pie-modal
134
+ .isOpen="${this._isModalOpen}"
135
+ hasBackButton
136
+ hasStackedActions
137
+ isFullWidthBelowMid
138
+ heading="Manage your preferences"
139
+ .leadingAction="${e}"
140
+ @pie-modal-leading-action-click="${this._handlePreferencesSaved}"
141
+ @pie-modal-back="${this._displayCookieBanner}">
142
+ ${this.renderModalContent()}
143
+ </pie-modal>
144
+ <aside data-test-id="pie-cookie-banner" class="c-cookieBanner" ?isCookieBannerHidden=${this._isCookieBannerHidden}>
8
145
  <h2 class="c-cookieBanner-title">Cookies</h2>
9
146
  <div class="c-cookieBanner-body">
10
147
  <p>We use our own and third party cookies and other tech to enhance and personalise your user experience,
@@ -16,21 +153,54 @@ class e extends n {
16
153
  </div>
17
154
 
18
155
  <div class="c-cookieBanner-actions">
19
- <pie-button variant="primary" isFullWidth="true" size="small-expressive">
156
+ <pie-button
157
+ data-test-id="accept-all"
158
+ @click="${() => {
159
+ this._dispatchCookieBannerCustomEvent(v), this._isCookieBannerHidden = !0;
160
+ }}"
161
+ variant="primary"
162
+ isFullWidth
163
+ size="small-expressive">
20
164
  Accept all
21
165
  </pie-button>
22
- <pie-button variant="outline" isFullWidth="true" size="small-expressive">
166
+ <pie-button
167
+ data-test-id="necessary-only"
168
+ @click="${() => {
169
+ this._dispatchCookieBannerCustomEvent(u), this._isCookieBannerHidden = !0;
170
+ }}"
171
+ variant="outline-inverse"
172
+ isFullWidth
173
+ size="small-expressive">
23
174
  Necessary only
24
175
  </pie-button>
25
- <pie-link variant="inverse" size="medium" isBold="true">
176
+ <pie-link
177
+ data-test-id="manage-prefs"
178
+ @click="${this._openManagePreferencesModal}"
179
+ tag="button"
180
+ variant="inverse"
181
+ isBold="true">
26
182
  Manage preferences
27
183
  </pie-link>
28
184
  </div>
29
185
  </aside>`;
30
186
  }
31
187
  }
32
- e.styles = i(a);
33
- customElements.define(o, e);
188
+ s.styles = p(k);
189
+ d([
190
+ h()
191
+ ], s.prototype, "_isCookieBannerHidden", 2);
192
+ d([
193
+ h()
194
+ ], s.prototype, "_isModalOpen", 2);
195
+ d([
196
+ f("pie-toggle-switch")
197
+ ], s.prototype, "_preferencesNodes", 2);
198
+ customElements.define(C, s);
34
199
  export {
35
- e as PieCookieBanner
200
+ v as ON_COOKIE_BANNER_ACCEPT_ALL,
201
+ m as ON_COOKIE_BANNER_MANAGE_PREFS,
202
+ u as ON_COOKIE_BANNER_NECESSARY_ONLY,
203
+ y as ON_COOKIE_BANNER_PREFS_SAVED,
204
+ s as PieCookieBanner,
205
+ _ as preferences
36
206
  };
package/dist/react.d.ts CHANGED
@@ -1,16 +1,124 @@
1
1
  import type { CSSResult } from 'lit';
2
+ import type { EventName } from '@lit-labs/react';
2
3
  import type { LitElement } from 'lit';
4
+ import { PieToggleSwitch } from '@justeattakeaway/pie-toggle-switch';
3
5
  import type { ReactWebComponent } from '@lit-labs/react';
4
- import type { TemplateResult } from 'lit-html';
6
+ import type { TemplateResult } from 'lit';
5
7
 
6
8
  export declare interface CookieBannerProps {
7
9
  }
8
10
 
9
- export declare const PieCookieBanner: ReactWebComponent<PieCookieBanner_2, {}>;
11
+ /**
12
+ * Event name for when all cookies are accepted.
13
+ *
14
+ * @constant
15
+ */
16
+ export declare const ON_COOKIE_BANNER_ACCEPT_ALL = "pie-cookie-banner-accept-all";
10
17
 
18
+ /**
19
+ * Event name for when a user clicks manage preferences.
20
+ *
21
+ * @constant
22
+ */
23
+ export declare const ON_COOKIE_BANNER_MANAGE_PREFS = "pie-cookie-banner-manage-prefs";
24
+
25
+ /**
26
+ * Event name for when all only necessary cookies are accepted.
27
+ *
28
+ * @constant
29
+ */
30
+ export declare const ON_COOKIE_BANNER_NECESSARY_ONLY = "pie-cookie-banner-necessary-only";
31
+
32
+ /**
33
+ * Event name for when a user clicks save preferences.
34
+ *
35
+ * @constant
36
+ */
37
+ export declare const ON_COOKIE_BANNER_PREFS_SAVED = "pie-cookie-banner-prefs-saved";
38
+
39
+ export declare const PieCookieBanner: ReactWebComponent<PieCookieBanner_2, {
40
+ onPieCookieBannerAcceptAll: EventName<CustomEvent<any>>;
41
+ onPieCookieBannerNecessaryOnly: EventName<CustomEvent<any>>;
42
+ onPieCookieBannerManagePrefs: EventName<CustomEvent<any>>;
43
+ onPieCookieBannerPrefsSaved: EventName<CustomEvent<any>>;
44
+ }>;
45
+
46
+ /**
47
+ * @event {CustomEvent} pie-cookie-banner-accept-all - when all cookies are accepted.
48
+ * @event {CustomEvent} pie-cookie-banner-necessary-only - when all only necessary cookies are accepted.
49
+ * @event {CustomEvent} pie-cookie-banner-manage-prefs - when a user clicks manage preferences.
50
+ * @event {CustomEvent} pie-cookie-banner-prefs-saved - when a user clicks save preferences.
51
+ */
11
52
  declare class PieCookieBanner_2 extends LitElement implements CookieBannerProps {
53
+ private _isCookieBannerHidden;
54
+ private _isModalOpen;
55
+ _preferencesNodes: NodeListOf<PieToggleSwitch>;
56
+ /**
57
+ * Handles closing the modal and re-displaying the cookie banner
58
+ * and making the cookie banner visible
59
+ */
60
+ private _displayCookieBanner;
61
+ /**
62
+ * Handles saving the user cookie preferences, closing the modal and the cookie banner
63
+ * Creates a state object for the save event, indicating the isChecked status
64
+ * of each preference except for the `all` preference.
65
+ * @example {
66
+ * functional: false,
67
+ * necessary: true
68
+ * }
69
+ */
70
+ private _handlePreferencesSaved;
71
+ /**
72
+ * Note: We should aim to have a shareable event helper system to allow
73
+ * us to share this across components in-future.
74
+ *
75
+ * Dispatch a custom event.
76
+ *
77
+ * To be used whenever we have behavioral events we want to
78
+ * bubble up through the cookie banner.
79
+ *
80
+ * @param {string} eventType
81
+ * @param {any} detail
82
+ */
83
+ private _dispatchCookieBannerCustomEvent;
84
+ /**
85
+ * Opens the manage preferences modal and emits an event letting users know
86
+ */
87
+ private _openManagePreferencesModal;
88
+ /**
89
+ * Handles the logic of the toggle switch nodes (preferences).
90
+ * Clicking the “all” toggle switch should turn on all preferences.
91
+ * When the “all” toggle is checked, and one of the other preferences is clicked,
92
+ * then the “all” toggle should be unchecked.
93
+ * if all toggle switches are checked, the `all` toggle switch should
94
+ * be turned on automatically
95
+ */
96
+ private _handleToggleStates;
97
+ /**
98
+ * Renders the content of the preference item.
99
+ * @private
100
+ */
101
+ private renderPreference;
102
+ /**
103
+ * Renders the modal content.
104
+ * @private
105
+ */
106
+ private renderModalContent;
12
107
  render(): TemplateResult<1>;
13
108
  static styles: CSSResult;
14
109
  }
15
110
 
111
+ export declare interface Preference {
112
+ id: PreferenceIds;
113
+ title: string;
114
+ description?: string;
115
+ isDisabled?: boolean;
116
+ isChecked?: boolean;
117
+ hasDivider?: boolean;
118
+ }
119
+
120
+ export declare type PreferenceIds = 'all' | 'necessary' | 'functional' | 'analytical' | 'personalized';
121
+
122
+ export declare const preferences: Preference[];
123
+
16
124
  export { }
package/dist/react.js CHANGED
@@ -1,36 +1,39 @@
1
- import * as C from "react";
2
- import { PieCookieBanner as E } from "./index.js";
1
+ import * as O from "react";
2
+ import { PieCookieBanner as k } from "./index.js";
3
+ import { ON_COOKIE_BANNER_ACCEPT_ALL as M, ON_COOKIE_BANNER_MANAGE_PREFS as I, ON_COOKIE_BANNER_NECESSARY_ONLY as K, ON_COOKIE_BANNER_PREFS_SAVED as x, preferences as D } from "./index.js";
3
4
  import "lit";
5
+ import "lit/decorators.js";
6
+ import "lit/directives/repeat.js";
4
7
  /**
5
8
  * @license
6
9
  * Copyright 2018 Google LLC
7
10
  * SPDX-License-Identifier: BSD-3-Clause
8
11
  */
9
- const g = /* @__PURE__ */ new Set(["children", "localName", "ref", "style", "className"]), w = /* @__PURE__ */ new WeakMap(), b = (d, r, m, p, h) => {
10
- const i = h == null ? void 0 : h[r];
11
- i === void 0 || m === p ? m == null && r in HTMLElement.prototype ? d.removeAttribute(r) : d[r] = m : ((o, t, u) => {
12
- let s = w.get(o);
13
- s === void 0 && w.set(o, s = /* @__PURE__ */ new Map());
14
- let a = s.get(t);
15
- u !== void 0 ? a === void 0 ? (s.set(t, a = { handleEvent: u }), o.addEventListener(t, a)) : a.handleEvent = u : a !== void 0 && (s.delete(t), o.removeEventListener(t, a));
16
- })(d, i, m);
12
+ const y = /* @__PURE__ */ new Set(["children", "localName", "ref", "style", "className"]), _ = /* @__PURE__ */ new WeakMap(), A = (p, r, d, m, N) => {
13
+ const t = N == null ? void 0 : N[r];
14
+ t === void 0 || d === m ? d == null && r in HTMLElement.prototype ? p.removeAttribute(r) : p[r] = d : ((i, n, h) => {
15
+ let s = _.get(i);
16
+ s === void 0 && _.set(i, s = /* @__PURE__ */ new Map());
17
+ let a = s.get(n);
18
+ h !== void 0 ? a === void 0 ? (s.set(n, a = { handleEvent: h }), i.addEventListener(n, a)) : a.handleEvent = h : a !== void 0 && (s.delete(n), i.removeEventListener(n, a));
19
+ })(p, t, d);
17
20
  };
18
- function B(d = window.React, r, m, p, h) {
19
- let i, o, t;
21
+ function P(p = window.React, r, d, m, N) {
22
+ let t, i, n;
20
23
  if (r === void 0) {
21
- const l = d;
22
- ({ tagName: o, elementClass: t, events: p, displayName: h } = l), i = l.react;
24
+ const l = p;
25
+ ({ tagName: i, elementClass: n, events: m, displayName: N } = l), t = l.react;
23
26
  } else
24
- i = d, t = m, o = r;
25
- const u = i.Component, s = i.createElement, a = new Set(Object.keys(p ?? {}));
26
- class f extends u {
27
+ t = p, n = d, i = r;
28
+ const h = t.Component, s = t.createElement, a = new Set(Object.keys(m ?? {}));
29
+ class u extends h {
27
30
  constructor() {
28
31
  super(...arguments), this.o = null;
29
32
  }
30
33
  t(e) {
31
34
  if (this.o !== null)
32
- for (const v in this.i)
33
- b(this.o, v, this.props[v], e ? e[v] : void 0, p);
35
+ for (const f in this.i)
36
+ A(this.o, f, this.props[f], e ? e[f] : void 0, m);
34
37
  }
35
38
  componentDidMount() {
36
39
  this.t();
@@ -39,29 +42,43 @@ function B(d = window.React, r, m, p, h) {
39
42
  this.t(e);
40
43
  }
41
44
  render() {
42
- const { _$Gl: e, ...v } = this.props;
43
- this.h !== e && (this.u = (n) => {
44
- e !== null && ((c, k) => {
45
- typeof c == "function" ? c(k) : c.current = k;
46
- })(e, n), this.o = n, this.h = e;
45
+ const { _$Gl: e, ...f } = this.props;
46
+ this.h !== e && (this.u = (o) => {
47
+ e !== null && ((c, C) => {
48
+ typeof c == "function" ? c(C) : c.current = C;
49
+ })(e, o), this.o = o, this.h = e;
47
50
  }), this.i = {};
48
- const y = { ref: this.u };
49
- for (const [n, c] of Object.entries(v))
50
- g.has(n) ? y[n === "className" ? "class" : n] = c : a.has(n) || n in t.prototype ? this.i[n] = c : y[n] = c;
51
- return s(o, y);
51
+ const E = { ref: this.u };
52
+ for (const [o, c] of Object.entries(f))
53
+ y.has(o) ? E[o === "className" ? "class" : o] = c : a.has(o) || o in n.prototype ? this.i[o] = c : E[o] = c;
54
+ return s(i, E);
52
55
  }
53
56
  }
54
- f.displayName = h ?? t.name;
55
- const N = i.forwardRef((l, e) => s(f, { ...l, _$Gl: e }, l == null ? void 0 : l.children));
56
- return N.displayName = f.displayName, N;
57
+ u.displayName = N ?? n.name;
58
+ const v = t.forwardRef((l, e) => s(u, { ...l, _$Gl: e }, l == null ? void 0 : l.children));
59
+ return v.displayName = u.displayName, v;
57
60
  }
58
- const L = B({
61
+ const g = P({
59
62
  displayName: "PieCookieBanner",
60
- elementClass: E,
61
- react: C,
63
+ elementClass: k,
64
+ react: O,
62
65
  tagName: "pie-cookie-banner",
63
- events: {}
66
+ events: {
67
+ onPieCookieBannerAcceptAll: "pie-cookie-banner-accept-all",
68
+ // when all cookies are accepted.
69
+ onPieCookieBannerNecessaryOnly: "pie-cookie-banner-necessary-only",
70
+ // when all only necessary cookies are accepted.
71
+ onPieCookieBannerManagePrefs: "pie-cookie-banner-manage-prefs",
72
+ // when a user clicks manage preferences.
73
+ onPieCookieBannerPrefsSaved: "pie-cookie-banner-prefs-saved"
74
+ // when a user clicks save preferences.
75
+ }
64
76
  });
65
77
  export {
66
- L as PieCookieBanner
78
+ M as ON_COOKIE_BANNER_ACCEPT_ALL,
79
+ I as ON_COOKIE_BANNER_MANAGE_PREFS,
80
+ K as ON_COOKIE_BANNER_NECESSARY_ONLY,
81
+ x as ON_COOKIE_BANNER_PREFS_SAVED,
82
+ g as PieCookieBanner,
83
+ D as preferences
67
84
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@justeattakeaway/pie-cookie-banner",
3
3
  "description": "PIE Design System Cookie Banner built using Web Components",
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
@@ -28,9 +28,12 @@
28
28
  "author": "JustEatTakeaway.com - Design System Web Team",
29
29
  "license": "Apache-2.0",
30
30
  "devDependencies": {
31
- "@justeattakeaway/pie-button": "0.26.0",
31
+ "@justeattakeaway/pie-button": "0.28.0",
32
32
  "@justeattakeaway/pie-components-config": "0.4.0",
33
- "@justeattakeaway/pie-link": "0.3.0"
33
+ "@justeattakeaway/pie-icon-button": "0.14.0",
34
+ "@justeattakeaway/pie-link": "0.3.0",
35
+ "@justeattakeaway/pie-modal": "0.19.0",
36
+ "@justeattakeaway/pie-toggle-switch": "0.6.0"
34
37
  },
35
38
  "dependencies": {
36
39
  "@justeattakeaway/pie-webc-core": "0.8.0"
@@ -41,6 +41,10 @@ $breakpoint-wide: 700px;
41
41
  border-bottom-left-radius: var(--dt-radius-rounded-c);
42
42
  border-bottom-right-radius: var(--dt-radius-rounded-c);
43
43
  }
44
+
45
+ &[isCookieBannerHidden] {
46
+ display: none;
47
+ }
44
48
  }
45
49
 
46
50
  .c-cookieBanner-title,
@@ -101,9 +105,8 @@ $breakpoint-wide: 700px;
101
105
  flex-direction: var(--cb-actions-flex-dir);
102
106
 
103
107
  > pie-link {
104
- width: max-content;
105
- margin-inline-start: auto;
106
- margin-inline-end: auto;
108
+ text-align: center;
109
+ align-self: center;
107
110
  }
108
111
 
109
112
  @media (min-width: $breakpoint-wide) and (orientation: landscape) {
@@ -111,10 +114,39 @@ $breakpoint-wide: 700px;
111
114
 
112
115
  justify-content: flex-start;
113
116
  align-items: center;
117
+ }
118
+ }
119
+
120
+ .c-cookieBanner-subheading {
121
+ --cb-subheading-font-size: var(--dt-font-heading-s-size--narrow);
122
+ --cb-subheading-line-height: var(--dt-font-heading-s-line-height--narrow);
123
+
124
+ @include p.font-size(--cb-subheading-font-size);
125
+ font-weight: var(--dt-font-heading-s-weight);
126
+ line-height: calc(var(--cb-subheading-line-height) * 1px);
127
+
128
+ @media (min-width: $breakpoint-wide) and (orientation: landscape) {
129
+ --cb-subheading-font-size: var(--dt-font-heading-s-size--wide);
130
+ --cb-subheading-line-height: var(--dt-font-heading-s-line-height--wide);
131
+ }
132
+ }
133
+
134
+ .c-cookieBanner-description {
135
+ @include p.font-size(--dt-font-body-s-size);
136
+ line-height: calc(var(--dt-font-body-s-line-height) * 1px);
137
+ }
138
+
139
+ .c-cookieBanner-preference {
140
+ display: flex;
141
+ gap: var(--dt-spacing-d);
142
+ justify-content: space-between;
143
+ margin-block: var(--dt-spacing-e);
144
+
145
+ p {
146
+ margin-block-start: var(--dt-spacing-b);
147
+ }
114
148
 
115
- > pie-link {
116
- margin-inline-start: 0;
117
- margin-inline-end: 0;
118
- }
149
+ &:last-child {
150
+ margin-block-end: 0;
119
151
  }
120
152
  }
package/src/defs.ts CHANGED
@@ -1,3 +1,72 @@
1
1
  // TODO - please remove the eslint disable comment below when you add props to this interface
2
2
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
3
3
  export interface CookieBannerProps {}
4
+
5
+ /**
6
+ * Event name for when all cookies are accepted.
7
+ *
8
+ * @constant
9
+ */
10
+ export const ON_COOKIE_BANNER_ACCEPT_ALL = 'pie-cookie-banner-accept-all';
11
+
12
+ /**
13
+ * Event name for when all only necessary cookies are accepted.
14
+ *
15
+ * @constant
16
+ */
17
+ export const ON_COOKIE_BANNER_NECESSARY_ONLY = 'pie-cookie-banner-necessary-only';
18
+
19
+ /**
20
+ * Event name for when a user clicks manage preferences.
21
+ *
22
+ * @constant
23
+ */
24
+ export const ON_COOKIE_BANNER_MANAGE_PREFS = 'pie-cookie-banner-manage-prefs';
25
+
26
+ /**
27
+ * Event name for when a user clicks save preferences.
28
+ *
29
+ * @constant
30
+ */
31
+ export const ON_COOKIE_BANNER_PREFS_SAVED = 'pie-cookie-banner-prefs-saved';
32
+
33
+ export type PreferenceIds = 'all' | 'necessary' | 'functional' | 'analytical' | 'personalized';
34
+
35
+ export interface Preference {
36
+ id: PreferenceIds;
37
+ title: string;
38
+ description?: string;
39
+ isDisabled?: boolean,
40
+ isChecked?: boolean,
41
+ hasDivider?: boolean
42
+ }
43
+
44
+ export const preferences: Preference[] = [
45
+ {
46
+ id: 'all',
47
+ title: 'Turn on all',
48
+ hasDivider: true,
49
+ },
50
+ {
51
+ id: 'necessary',
52
+ title: 'Necessary',
53
+ description: 'These cookies are necessary to ensure that the website and its features function properly. Services you have asked for cannot be provided without these cookies.',
54
+ isDisabled: true,
55
+ isChecked: true,
56
+ },
57
+ {
58
+ id: 'functional',
59
+ title: 'Functional',
60
+ description: 'These cookies allow the website to remember the choices you make to give you better functionality and personal features.',
61
+ },
62
+ {
63
+ id: 'analytical',
64
+ title: 'Analytical',
65
+ description: 'These analytical cookies, including statistics, are used to understand how visitors interact with the website and we can measure and improve the performance of our website.',
66
+ },
67
+ {
68
+ id: 'personalized',
69
+ title: 'Personalized (targeting and advertising)',
70
+ description: 'These marketing cookies are used to tailor the delivery of information to you based upon your interest and to measure the effectiveness of such advertisements, both on our website and our advertising partners\' websites.',
71
+ }
72
+ ];
package/src/index.ts CHANGED
@@ -1,17 +1,188 @@
1
- import { LitElement, html, unsafeCSS } from 'lit';
2
-
1
+ import {
2
+ LitElement, html, unsafeCSS, TemplateResult, nothing,
3
+ } from 'lit';
4
+ import { state, queryAll } from 'lit/decorators.js';
5
+ import { repeat } from 'lit/directives/repeat.js';
6
+ import { PieToggleSwitch } from '@justeattakeaway/pie-toggle-switch';
3
7
  import styles from './cookie-banner.scss?inline';
4
- import { CookieBannerProps } from './defs';
8
+ import {
9
+ CookieBannerProps,
10
+ ON_COOKIE_BANNER_ACCEPT_ALL,
11
+ ON_COOKIE_BANNER_NECESSARY_ONLY,
12
+ ON_COOKIE_BANNER_MANAGE_PREFS,
13
+ ON_COOKIE_BANNER_PREFS_SAVED,
14
+ preferences,
15
+ type Preference,
16
+ type PreferenceIds,
17
+ } from './defs';
5
18
 
6
19
  // Valid values available to consumers
7
20
  export * from './defs';
8
21
 
9
22
  const componentSelector = 'pie-cookie-banner';
10
23
 
24
+ /**
25
+ * @event {CustomEvent} pie-cookie-banner-accept-all - when all cookies are accepted.
26
+ * @event {CustomEvent} pie-cookie-banner-necessary-only - when all only necessary cookies are accepted.
27
+ * @event {CustomEvent} pie-cookie-banner-manage-prefs - when a user clicks manage preferences.
28
+ * @event {CustomEvent} pie-cookie-banner-prefs-saved - when a user clicks save preferences.
29
+ */
11
30
  export class PieCookieBanner extends LitElement implements CookieBannerProps {
31
+ @state()
32
+ private _isCookieBannerHidden = false;
33
+
34
+ @state()
35
+ private _isModalOpen = false;
36
+
37
+ @queryAll('pie-toggle-switch')
38
+ _preferencesNodes!: NodeListOf<PieToggleSwitch>;
39
+
40
+ /**
41
+ * Handles closing the modal and re-displaying the cookie banner
42
+ * and making the cookie banner visible
43
+ */
44
+ private _displayCookieBanner () : void {
45
+ this._isModalOpen = false;
46
+ this._isCookieBannerHidden = false;
47
+ }
48
+
49
+ /**
50
+ * Handles saving the user cookie preferences, closing the modal and the cookie banner
51
+ * Creates a state object for the save event, indicating the isChecked status
52
+ * of each preference except for the `all` preference.
53
+ * @example {
54
+ * functional: false,
55
+ * necessary: true
56
+ * }
57
+ */
58
+ private _handlePreferencesSaved () : void {
59
+ let state: Partial<{ [x in PreferenceIds]: boolean }> = {};
60
+
61
+ [...this._preferencesNodes]
62
+ .filter(({ id }) => id !== 'all')
63
+ .forEach(({ id, isChecked }) => {
64
+ state = { ...state, [id]: isChecked };
65
+ });
66
+
67
+ this._dispatchCookieBannerCustomEvent(ON_COOKIE_BANNER_PREFS_SAVED, state);
68
+ this._isModalOpen = false;
69
+ this._isCookieBannerHidden = true;
70
+ }
71
+
72
+ /**
73
+ * Note: We should aim to have a shareable event helper system to allow
74
+ * us to share this across components in-future.
75
+ *
76
+ * Dispatch a custom event.
77
+ *
78
+ * To be used whenever we have behavioral events we want to
79
+ * bubble up through the cookie banner.
80
+ *
81
+ * @param {string} eventType
82
+ * @param {any} detail
83
+ */
84
+ private _dispatchCookieBannerCustomEvent = (eventType: string, detail?: CustomEventInit['detail']) : void => {
85
+ const event = new CustomEvent(eventType, {
86
+ bubbles: true,
87
+ composed: true,
88
+ detail,
89
+ });
90
+
91
+ this.dispatchEvent(event);
92
+ };
93
+
94
+ /**
95
+ * Opens the manage preferences modal and emits an event letting users know
96
+ */
97
+ private _openManagePreferencesModal = () : void => {
98
+ this._isCookieBannerHidden = true;
99
+ this._dispatchCookieBannerCustomEvent(ON_COOKIE_BANNER_MANAGE_PREFS);
100
+ this._isModalOpen = true;
101
+ };
102
+
103
+ /**
104
+ * Handles the logic of the toggle switch nodes (preferences).
105
+ * Clicking the “all” toggle switch should turn on all preferences.
106
+ * When the “all” toggle is checked, and one of the other preferences is clicked,
107
+ * then the “all” toggle should be unchecked.
108
+ * if all toggle switches are checked, the `all` toggle switch should
109
+ * be turned on automatically
110
+ */
111
+ private _handleToggleStates = (e: CustomEvent) : void => {
112
+ const { id } = e?.currentTarget as HTMLInputElement;
113
+ const toggleAllNode = [...this._preferencesNodes].find(({ id }) => id === 'all') as PieToggleSwitch;
114
+
115
+ if (id === toggleAllNode.id) {
116
+ const isChecked = e.detail;
117
+ this._preferencesNodes.forEach((node) => {
118
+ node.isChecked = node.isDisabled ? node.isChecked : isChecked;
119
+ });
120
+ } else {
121
+ toggleAllNode.isChecked = [...this._preferencesNodes]
122
+ .filter(({ id }) => id !== 'all')
123
+ .every(({ isChecked }) => isChecked);
124
+ }
125
+ };
126
+
127
+ /**
128
+ * Renders the content of the preference item.
129
+ * @private
130
+ */
131
+ private renderPreference ({
132
+ id, title, description, isChecked, isDisabled, hasDivider,
133
+ }: Preference): TemplateResult {
134
+ return html`
135
+ <div class="c-cookieBanner-preference">
136
+ <div>
137
+ <h3 class="c-cookieBanner-subheading">${title}</h3>
138
+ ${description ? html`<p class="c-cookieBanner-description">${description}</p>` : nothing}
139
+ </div>
140
+ <pie-toggle-switch
141
+ id="${id}"
142
+ ?isChecked="${isChecked}"
143
+ ?isDisabled="${isDisabled}"
144
+ @pie-toggle-switch-changed="${this._handleToggleStates}"/>
145
+ </div>
146
+ ${hasDivider ? html`<pie-divider></pie-divider>` : nothing}`;
147
+ }
148
+
149
+ /**
150
+ * Renders the modal content.
151
+ * @private
152
+ */
153
+ private renderModalContent (): TemplateResult {
154
+ return html`
155
+ <p class="c-cookieBanner-description">You can find all the information in the
156
+ <pie-link href="#" size="small" target="_blank">Cookie Statement</pie-link> and
157
+ <pie-link href="#" size="small" target="_blank">Cookie technology list</pie-link>.
158
+ </p>
159
+ ${repeat(
160
+ preferences,
161
+ ({ id }) => id,
162
+ (preference) => this.renderPreference(preference),
163
+ )}`;
164
+ }
165
+
12
166
  render () {
167
+ const modalActionProps = {
168
+ text: 'Save',
169
+ variant: 'primary',
170
+ ariaLabel: 'Save your cookie preferences',
171
+ };
172
+
13
173
  return html`
14
- <aside data-test-id="pie-cookie-banner" class="c-cookieBanner">
174
+ <pie-modal
175
+ .isOpen="${this._isModalOpen}"
176
+ hasBackButton
177
+ hasStackedActions
178
+ isFullWidthBelowMid
179
+ heading="Manage your preferences"
180
+ .leadingAction="${modalActionProps}"
181
+ @pie-modal-leading-action-click="${this._handlePreferencesSaved}"
182
+ @pie-modal-back="${this._displayCookieBanner}">
183
+ ${this.renderModalContent()}
184
+ </pie-modal>
185
+ <aside data-test-id="pie-cookie-banner" class="c-cookieBanner" ?isCookieBannerHidden=${this._isCookieBannerHidden}>
15
186
  <h2 class="c-cookieBanner-title">Cookies</h2>
16
187
  <div class="c-cookieBanner-body">
17
188
  <p>We use our own and third party cookies and other tech to enhance and personalise your user experience,
@@ -23,13 +194,28 @@ export class PieCookieBanner extends LitElement implements CookieBannerProps {
23
194
  </div>
24
195
 
25
196
  <div class="c-cookieBanner-actions">
26
- <pie-button variant="primary" isFullWidth="true" size="small-expressive">
197
+ <pie-button
198
+ data-test-id="accept-all"
199
+ @click="${() => { this._dispatchCookieBannerCustomEvent(ON_COOKIE_BANNER_ACCEPT_ALL); this._isCookieBannerHidden = true; }}"
200
+ variant="primary"
201
+ isFullWidth
202
+ size="small-expressive">
27
203
  Accept all
28
204
  </pie-button>
29
- <pie-button variant="outline" isFullWidth="true" size="small-expressive">
205
+ <pie-button
206
+ data-test-id="necessary-only"
207
+ @click="${() => { this._dispatchCookieBannerCustomEvent(ON_COOKIE_BANNER_NECESSARY_ONLY); this._isCookieBannerHidden = true; }}"
208
+ variant="outline-inverse"
209
+ isFullWidth
210
+ size="small-expressive">
30
211
  Necessary only
31
212
  </pie-button>
32
- <pie-link variant="inverse" size="medium" isBold="true">
213
+ <pie-link
214
+ data-test-id="manage-prefs"
215
+ @click="${this._openManagePreferencesModal}"
216
+ tag="button"
217
+ variant="inverse"
218
+ isBold="true">
33
219
  Manage preferences
34
220
  </pie-link>
35
221
  </div>