@akinon/pz-theme 2.0.37-beta.0 → 2.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -4
- package/package.json +4 -4
- package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
- package/src/__tests__/custom-css.test.ts +181 -0
- package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
- package/src/__tests__/facet-filters-block.test.tsx +707 -0
- package/src/__tests__/item-template.test.tsx +184 -0
- package/src/__tests__/listing-controls.test.tsx +961 -0
- package/src/__tests__/part-style-scope.test.tsx +229 -0
- package/src/__tests__/part-styles.test.ts +311 -0
- package/src/__tests__/registry-parity.test.ts +42 -0
- package/src/__tests__/row-visibility.test.ts +672 -0
- package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
- package/src/__tests__/theme-block-html-id.test.tsx +167 -0
- package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
- package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
- package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
- package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
- package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
- package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
- package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
- package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
- package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
- package/src/blocks/__tests__/share-block.test.tsx +392 -0
- package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
- package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
- package/src/blocks/__tests__/variant-selector-golden.json +20 -0
- package/src/blocks/account-menu-block.tsx +264 -0
- package/src/blocks/add-to-cart-block.tsx +417 -0
- package/src/blocks/address-list-block.tsx +348 -0
- package/src/blocks/applied-filters-block.tsx +288 -0
- package/src/blocks/auth-form-block.tsx +295 -0
- package/src/blocks/basket-items-block.tsx +457 -0
- package/src/blocks/basket-summary-block.tsx +404 -0
- package/src/blocks/block-renderer-registry.tsx +94 -0
- package/src/blocks/change-password-form-block.tsx +240 -0
- package/src/blocks/checkout-steps-block.tsx +288 -0
- package/src/blocks/checkout-summary-block.tsx +449 -0
- package/src/blocks/countdown-block.tsx +182 -0
- package/src/blocks/coupon-list-block.tsx +292 -0
- package/src/blocks/facet-filters-block.tsx +1686 -0
- package/src/blocks/faq-accordion-block.tsx +358 -0
- package/src/blocks/favorite-button-block.tsx +252 -0
- package/src/blocks/favourites-grid-block.tsx +510 -0
- package/src/blocks/forgot-password-block.tsx +155 -0
- package/src/blocks/item-template-utils.ts +131 -0
- package/src/blocks/item-template.tsx +164 -0
- package/src/blocks/listing-breadcrumb-block.tsx +175 -0
- package/src/blocks/listing-context.ts +850 -0
- package/src/blocks/listing-pagination-block.tsx +923 -0
- package/src/blocks/listing-result-count-block.tsx +126 -0
- package/src/blocks/listing-title-block.tsx +115 -0
- package/src/blocks/oauth-buttons-block.tsx +205 -0
- package/src/blocks/order-detail-block.tsx +474 -0
- package/src/blocks/order-list-block.tsx +274 -0
- package/src/blocks/order-success-block.tsx +641 -0
- package/src/blocks/order-tracking-block.tsx +533 -0
- package/src/blocks/product-attributes-block.tsx +307 -0
- package/src/blocks/product-block-utils.tsx +639 -0
- package/src/blocks/product-breadcrumb-block.tsx +215 -0
- package/src/blocks/product-description-block.tsx +169 -0
- package/src/blocks/product-gallery-block.tsx +1523 -0
- package/src/blocks/product-grid-block.tsx +966 -0
- package/src/blocks/product-price-block.tsx +373 -0
- package/src/blocks/product-recommendations-block.tsx +1433 -0
- package/src/blocks/product-title-block.tsx +129 -0
- package/src/blocks/profile-form-block.tsx +297 -0
- package/src/blocks/recently-viewed-store.ts +201 -0
- package/src/blocks/register-form-block.tsx +362 -0
- package/src/blocks/share-block.tsx +667 -0
- package/src/blocks/size-guide-block.tsx +313 -0
- package/src/blocks/sort-select-block.tsx +636 -0
- package/src/blocks/stock-status-block.tsx +168 -0
- package/src/blocks/store-locator-block.tsx +1184 -0
- package/src/blocks/variant-selector-block.tsx +1110 -0
- package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +29 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
7
|
+
import {
|
|
8
|
+
listingPlaceholderStyle,
|
|
9
|
+
readToggle,
|
|
10
|
+
useListingContext
|
|
11
|
+
} from './listing-context';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Functional listing block: the "N products" line. The number is
|
|
15
|
+
* commerce-driven (the pagination count of the category/search response, not
|
|
16
|
+
* the number of cards on the current page); the editor owns the wording,
|
|
17
|
+
* the singular/empty variants and whether the number is its own element.
|
|
18
|
+
*
|
|
19
|
+
* Parts: text, count.
|
|
20
|
+
* Properties: template, oneTemplate, zeroTemplate, groupThousands,
|
|
21
|
+
* highlightCount.
|
|
22
|
+
*/
|
|
23
|
+
const ListingResultCountBlock = ({
|
|
24
|
+
block,
|
|
25
|
+
currentBreakpoint = 'desktop',
|
|
26
|
+
isDesigner = false
|
|
27
|
+
}: BlockRendererProps) => {
|
|
28
|
+
const listing = useListingContext();
|
|
29
|
+
const { locale } = useLocalization();
|
|
30
|
+
|
|
31
|
+
const template = String(
|
|
32
|
+
getResponsiveValue(
|
|
33
|
+
block.properties?.template,
|
|
34
|
+
currentBreakpoint,
|
|
35
|
+
'{count} products'
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
const oneTemplate = String(
|
|
39
|
+
getResponsiveValue(block.properties?.oneTemplate, currentBreakpoint, '')
|
|
40
|
+
);
|
|
41
|
+
const zeroTemplate = String(
|
|
42
|
+
getResponsiveValue(block.properties?.zeroTemplate, currentBreakpoint, '')
|
|
43
|
+
);
|
|
44
|
+
const groupThousands = readToggle(
|
|
45
|
+
block.properties?.groupThousands,
|
|
46
|
+
currentBreakpoint,
|
|
47
|
+
false
|
|
48
|
+
);
|
|
49
|
+
const highlightCount = readToggle(
|
|
50
|
+
block.properties?.highlightCount,
|
|
51
|
+
currentBreakpoint,
|
|
52
|
+
false
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (!listing) {
|
|
56
|
+
return isDesigner ? (
|
|
57
|
+
<div data-listing-result-count="" style={listingPlaceholderStyle}>
|
|
58
|
+
Result count (no listing data)
|
|
59
|
+
</div>
|
|
60
|
+
) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const count = listing.resultCount ?? 0;
|
|
64
|
+
|
|
65
|
+
// The locale is explicit: an implicit Intl locale resolves to the Node
|
|
66
|
+
// default on the server and the browser's on the client, so the grouped
|
|
67
|
+
// number would differ between SSR and hydration.
|
|
68
|
+
let formatted = String(count);
|
|
69
|
+
if (groupThousands) {
|
|
70
|
+
try {
|
|
71
|
+
formatted = new Intl.NumberFormat(locale || 'tr-TR').format(count);
|
|
72
|
+
} catch {
|
|
73
|
+
formatted = String(count);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A zero/one wording only applies when the brand supplied one, so the
|
|
78
|
+
// plural template stays the single source by default.
|
|
79
|
+
const activeTemplate =
|
|
80
|
+
count === 0 && zeroTemplate
|
|
81
|
+
? zeroTemplate
|
|
82
|
+
: count === 1 && oneTemplate
|
|
83
|
+
? oneTemplate
|
|
84
|
+
: template;
|
|
85
|
+
|
|
86
|
+
const hasPlaceholder = activeTemplate.includes('{count}');
|
|
87
|
+
// A template without the placeholder must still show the number, and it
|
|
88
|
+
// reads as a count only in front of the wording ("24 products").
|
|
89
|
+
const text = hasPlaceholder
|
|
90
|
+
? activeTemplate.replace('{count}', formatted)
|
|
91
|
+
: `${formatted} ${activeTemplate}`.trim();
|
|
92
|
+
|
|
93
|
+
const rootProps = {
|
|
94
|
+
'data-listing-result-count': '',
|
|
95
|
+
'data-listing-result-total': count,
|
|
96
|
+
...partAttrs('text', { empty: count === 0 })
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (!highlightCount) {
|
|
100
|
+
return <span {...rootProps}>{text}</span>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const countNode = (
|
|
104
|
+
<span {...partAttrs('count', { empty: count === 0 })}>{formatted}</span>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (!hasPlaceholder) {
|
|
108
|
+
return (
|
|
109
|
+
<span {...rootProps}>
|
|
110
|
+
{countNode} {activeTemplate}
|
|
111
|
+
</span>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const placeholderIndex = activeTemplate.indexOf('{count}');
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<span {...rootProps}>
|
|
119
|
+
{activeTemplate.slice(0, placeholderIndex)}
|
|
120
|
+
{countNode}
|
|
121
|
+
{activeTemplate.slice(placeholderIndex + '{count}'.length)}
|
|
122
|
+
</span>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default ListingResultCountBlock;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
5
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
6
|
+
import {
|
|
7
|
+
listingPlaceholderStyle,
|
|
8
|
+
readToggle,
|
|
9
|
+
useListingContext
|
|
10
|
+
} from './listing-context';
|
|
11
|
+
|
|
12
|
+
const HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'div', 'span'];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Functional listing block: the category/search page title. The text is
|
|
16
|
+
* commerce-driven (category name, or "Search results for …" on search);
|
|
17
|
+
* the editor owns the heading level, the optional result count and styling.
|
|
18
|
+
*
|
|
19
|
+
* Parts: title, count.
|
|
20
|
+
* Properties: tag, fallbackTitle, titleTemplate, maxLength, showCount,
|
|
21
|
+
* countTemplate, countPosition.
|
|
22
|
+
*/
|
|
23
|
+
const ListingTitleBlock = ({
|
|
24
|
+
block,
|
|
25
|
+
currentBreakpoint = 'desktop',
|
|
26
|
+
isDesigner = false
|
|
27
|
+
}: BlockRendererProps) => {
|
|
28
|
+
const listing = useListingContext();
|
|
29
|
+
|
|
30
|
+
const requestedTag = String(
|
|
31
|
+
getResponsiveValue(block.properties?.tag, currentBreakpoint, 'h1')
|
|
32
|
+
);
|
|
33
|
+
const Tag = (
|
|
34
|
+
HEADING_TAGS.includes(requestedTag) ? requestedTag : 'h1'
|
|
35
|
+
) as 'h1';
|
|
36
|
+
const showCount = readToggle(
|
|
37
|
+
block.properties?.showCount,
|
|
38
|
+
currentBreakpoint,
|
|
39
|
+
false
|
|
40
|
+
);
|
|
41
|
+
const countTemplate = String(
|
|
42
|
+
getResponsiveValue(
|
|
43
|
+
block.properties?.countTemplate,
|
|
44
|
+
currentBreakpoint,
|
|
45
|
+
'({count})'
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
const countPosition = String(
|
|
49
|
+
getResponsiveValue(block.properties?.countPosition, currentBreakpoint, 'after')
|
|
50
|
+
);
|
|
51
|
+
const fallbackTitle = String(
|
|
52
|
+
getResponsiveValue(block.properties?.fallbackTitle, currentBreakpoint, '')
|
|
53
|
+
);
|
|
54
|
+
const titleTemplate = String(
|
|
55
|
+
getResponsiveValue(
|
|
56
|
+
block.properties?.titleTemplate,
|
|
57
|
+
currentBreakpoint,
|
|
58
|
+
'{title}'
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
const maxLengthRaw = Math.round(
|
|
62
|
+
Number(getResponsiveValue(block.properties?.maxLength, currentBreakpoint, 0))
|
|
63
|
+
);
|
|
64
|
+
// 0 keeps the full title; anything else is a hard character cap.
|
|
65
|
+
const maxLength = Number.isFinite(maxLengthRaw) && maxLengthRaw > 0 ? maxLengthRaw : 0;
|
|
66
|
+
const ellipsis = String(
|
|
67
|
+
getResponsiveValue(block.properties?.ellipsis, currentBreakpoint, '…')
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// A search page with no commerce title still needs a heading, so the brand
|
|
71
|
+
// can supply one; empty by default, which keeps the old "render nothing".
|
|
72
|
+
const sourceTitle = listing?.title || fallbackTitle;
|
|
73
|
+
|
|
74
|
+
if (!sourceTitle) {
|
|
75
|
+
return isDesigner ? (
|
|
76
|
+
<div data-listing-title="" style={listingPlaceholderStyle}>
|
|
77
|
+
Listing title (no listing data)
|
|
78
|
+
</div>
|
|
79
|
+
) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const truncated =
|
|
83
|
+
maxLength > 0 && sourceTitle.length > maxLength
|
|
84
|
+
? `${sourceTitle.slice(0, maxLength).trimEnd()}${ellipsis}`
|
|
85
|
+
: sourceTitle;
|
|
86
|
+
const title = titleTemplate.includes('{title}')
|
|
87
|
+
? titleTemplate.replace('{title}', truncated)
|
|
88
|
+
: truncated;
|
|
89
|
+
|
|
90
|
+
const resultCount = listing?.resultCount ?? 0;
|
|
91
|
+
const countLabel = countTemplate.includes('{count}')
|
|
92
|
+
? countTemplate.replace('{count}', String(resultCount))
|
|
93
|
+
: `${countTemplate} ${resultCount}`.trim();
|
|
94
|
+
const showCountBefore = countPosition === 'before';
|
|
95
|
+
|
|
96
|
+
const countNode = showCount ? (
|
|
97
|
+
<span
|
|
98
|
+
data-listing-title-count=""
|
|
99
|
+
{...partAttrs('count', { empty: resultCount === 0 })}
|
|
100
|
+
style={showCountBefore ? { marginRight: '8px' } : { marginLeft: '8px' }}
|
|
101
|
+
>
|
|
102
|
+
{countLabel}
|
|
103
|
+
</span>
|
|
104
|
+
) : null;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Tag data-listing-title="" {...partAttrs('title')} style={{ margin: 0 }}>
|
|
108
|
+
{showCountBefore && countNode}
|
|
109
|
+
{title}
|
|
110
|
+
{!showCountBefore && countNode}
|
|
111
|
+
</Tag>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default ListingTitleBlock;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import NextLink from 'next/link';
|
|
5
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { DesignerPlaceholder, readToggle } from './product-block-utils';
|
|
7
|
+
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
8
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Social sign-in on v1 is NOT a next-auth social provider. Brands register only
|
|
12
|
+
* credential providers (`default` for login/registration, `oauth` to adopt an
|
|
13
|
+
* already-established commerce session), so `signIn('google')` would resolve to
|
|
14
|
+
* no provider and bounce to the error page.
|
|
15
|
+
*
|
|
16
|
+
* The real flow is a plain navigation to `/{provider}/login/`, which the core
|
|
17
|
+
* `withOauthLogin` middleware rewrites to `{commerceUrl}/{provider}/login/`.
|
|
18
|
+
* Commerce performs the handshake, sets `osessionid`, and sends the shopper
|
|
19
|
+
* back through `/auth/oauth-login`, where the core component calls
|
|
20
|
+
* `signIn('oauth')` to mint the next-auth session. A real link (rather than a
|
|
21
|
+
* button + router.push) also matters because the middleware stores the return
|
|
22
|
+
* url from the `referer` header that the browser only sends on a navigation.
|
|
23
|
+
*/
|
|
24
|
+
const providerHref = (provider: string) => `/${provider}/login/`;
|
|
25
|
+
|
|
26
|
+
/** Brands configure providers commerce-side, so the label is derived. */
|
|
27
|
+
const providerLabel = (provider: string) =>
|
|
28
|
+
provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `google=Continue with Google,apple=Sign in with Apple`. A provider with no
|
|
32
|
+
* entry keeps the derived label, so an empty setting reproduces the built-in
|
|
33
|
+
* capitalised names.
|
|
34
|
+
*/
|
|
35
|
+
const parseProviderLabels = (raw: string): Record<string, string> => {
|
|
36
|
+
const overrides: Record<string, string> = {};
|
|
37
|
+
|
|
38
|
+
raw
|
|
39
|
+
.split(',')
|
|
40
|
+
.map((entry) => entry.trim())
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.forEach((entry) => {
|
|
43
|
+
const separator = entry.indexOf('=');
|
|
44
|
+
if (separator <= 0) return;
|
|
45
|
+
|
|
46
|
+
const key = entry.slice(0, separator).trim().toLowerCase();
|
|
47
|
+
const label = entry.slice(separator + 1).trim();
|
|
48
|
+
|
|
49
|
+
if (key && label) overrides[key] = label;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return overrides;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Functional auth block: social login buttons. Which providers exist is a
|
|
57
|
+
* commerce-side setting with no storefront manifest to read, so the author
|
|
58
|
+
* lists the ones the brand actually enabled; nothing is rendered for an empty
|
|
59
|
+
* list rather than guessing.
|
|
60
|
+
* Properties: providers, providerLabels, layout, equalWidth, showIcons,
|
|
61
|
+
* emptyLabel.
|
|
62
|
+
* Parts: list, button-{provider}, label, icon, icon-{provider}. Buttons are
|
|
63
|
+
* addressed per provider because Google and Apple both mandate their own
|
|
64
|
+
* button styling, and one shared part could not satisfy both.
|
|
65
|
+
*/
|
|
66
|
+
const OauthButtonsBlock = ({
|
|
67
|
+
block,
|
|
68
|
+
placeholderId,
|
|
69
|
+
sectionId,
|
|
70
|
+
selectedBlockId = null,
|
|
71
|
+
currentBreakpoint = 'desktop',
|
|
72
|
+
isDesigner = false
|
|
73
|
+
}: BlockRendererProps) => {
|
|
74
|
+
const prop = (key: string, fallback: string) =>
|
|
75
|
+
String(
|
|
76
|
+
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const rawProviders = prop('providers', 'google,apple,facebook');
|
|
80
|
+
const layout = prop('layout', 'row') === 'column' ? 'column' : 'row';
|
|
81
|
+
const labelOverrides = parseProviderLabels(prop('providerLabels', ''));
|
|
82
|
+
const showIcons = readToggle(
|
|
83
|
+
block.properties?.showIcons,
|
|
84
|
+
currentBreakpoint,
|
|
85
|
+
false
|
|
86
|
+
);
|
|
87
|
+
const equalWidth = readToggle(
|
|
88
|
+
block.properties?.equalWidth,
|
|
89
|
+
currentBreakpoint,
|
|
90
|
+
true
|
|
91
|
+
);
|
|
92
|
+
const emptyLabel = prop('emptyLabel', 'Social login (no providers listed)');
|
|
93
|
+
|
|
94
|
+
const providers = rawProviders
|
|
95
|
+
.split(',')
|
|
96
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
97
|
+
// The value becomes a url segment, so anything that is not a plain slug is
|
|
98
|
+
// dropped instead of being interpolated into the path.
|
|
99
|
+
.filter((entry) => /^[a-z0-9-]+$/.test(entry));
|
|
100
|
+
|
|
101
|
+
// A designer who marked a child as the button layout gets that layout
|
|
102
|
+
// repeated per provider; otherwise the built-in button renders unchanged.
|
|
103
|
+
const customButtons = hasItemTemplate(block, currentBreakpoint) ? (
|
|
104
|
+
<ItemTemplateList
|
|
105
|
+
block={block}
|
|
106
|
+
items={providers.map((provider) => ({
|
|
107
|
+
provider,
|
|
108
|
+
label: labelOverrides[provider] || providerLabel(provider),
|
|
109
|
+
name: labelOverrides[provider] || providerLabel(provider),
|
|
110
|
+
url: providerHref(provider)
|
|
111
|
+
}))}
|
|
112
|
+
placeholderId={placeholderId}
|
|
113
|
+
sectionId={sectionId}
|
|
114
|
+
isDesigner={isDesigner}
|
|
115
|
+
selectedBlockId={selectedBlockId}
|
|
116
|
+
currentBreakpoint={currentBreakpoint}
|
|
117
|
+
/>
|
|
118
|
+
) : null;
|
|
119
|
+
|
|
120
|
+
if (providers.length === 0 && !customButtons) {
|
|
121
|
+
return isDesigner ? (
|
|
122
|
+
<DesignerPlaceholder
|
|
123
|
+
label={emptyLabel}
|
|
124
|
+
attribute="data-oauth-buttons"
|
|
125
|
+
/>
|
|
126
|
+
) : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
data-oauth-buttons=""
|
|
132
|
+
data-oauth-buttons-layout={layout}
|
|
133
|
+
{...partAttrs('list')}
|
|
134
|
+
style={{
|
|
135
|
+
display: 'flex',
|
|
136
|
+
flexDirection: layout === 'column' ? 'column' : 'row',
|
|
137
|
+
flexWrap: layout === 'row' ? 'wrap' : 'nowrap',
|
|
138
|
+
gap: 'var(--oauth-buttons-gap, 12px)'
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
{customButtons ??
|
|
142
|
+
providers.map((provider) => (
|
|
143
|
+
<NextLink
|
|
144
|
+
key={provider}
|
|
145
|
+
data-oauth-button={provider}
|
|
146
|
+
{...partAttrs(`button-${provider}`)}
|
|
147
|
+
href={isDesigner ? '#' : providerHref(provider)}
|
|
148
|
+
// The handshake leaves the Next.js app entirely, so the client router
|
|
149
|
+
// must not try to prefetch or soft-navigate it.
|
|
150
|
+
prefetch={false}
|
|
151
|
+
onClick={(event) => {
|
|
152
|
+
if (isDesigner) event.preventDefault();
|
|
153
|
+
}}
|
|
154
|
+
style={{
|
|
155
|
+
display: 'inline-flex',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
justifyContent: 'center',
|
|
158
|
+
gap: '8px',
|
|
159
|
+
flex:
|
|
160
|
+
layout === 'row' ? (equalWidth ? '1 1 0' : '0 0 auto') : '0 0 auto',
|
|
161
|
+
padding: '10px 16px',
|
|
162
|
+
border: '1px solid var(--oauth-buttons-border, #d1d5db)',
|
|
163
|
+
color: 'inherit',
|
|
164
|
+
textDecoration: 'none',
|
|
165
|
+
cursor: 'pointer'
|
|
166
|
+
}}
|
|
167
|
+
>
|
|
168
|
+
{showIcons && (
|
|
169
|
+
// Two nested boxes on purpose: the outer one is the shared size
|
|
170
|
+
// slot every provider agrees on, the inner one is where a brand
|
|
171
|
+
// drops that provider's own mark.
|
|
172
|
+
<span
|
|
173
|
+
{...partAttrs('icon')}
|
|
174
|
+
aria-hidden="true"
|
|
175
|
+
style={{
|
|
176
|
+
display: 'inline-flex',
|
|
177
|
+
flex: '0 0 auto',
|
|
178
|
+
width: '18px',
|
|
179
|
+
height: '18px'
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<span
|
|
183
|
+
data-oauth-icon={provider}
|
|
184
|
+
{...partAttrs(`icon-${provider}`)}
|
|
185
|
+
style={{
|
|
186
|
+
display: 'block',
|
|
187
|
+
width: '100%',
|
|
188
|
+
height: '100%',
|
|
189
|
+
backgroundSize: 'contain',
|
|
190
|
+
backgroundRepeat: 'no-repeat',
|
|
191
|
+
backgroundPosition: 'center'
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
</span>
|
|
195
|
+
)}
|
|
196
|
+
<span {...partAttrs('label')}>
|
|
197
|
+
{labelOverrides[provider] || providerLabel(provider)}
|
|
198
|
+
</span>
|
|
199
|
+
</NextLink>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export default OauthButtonsBlock;
|