@shoppexio/storefront-react 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/account.css +1171 -0
- package/dist/account.d.ts +23 -0
- package/dist/account.js +12685 -0
- package/dist/account.js.map +1 -0
- package/dist/chunk-D6XKDVAV.js +181 -0
- package/dist/chunk-D6XKDVAV.js.map +1 -0
- package/dist/index.d.ts +3087 -0
- package/dist/index.js +2801 -0
- package/dist/index.js.map +1 -0
- package/eject/account/account-availability.ts +171 -0
- package/eject/account/account-downloads.ts +118 -0
- package/eject/account/account-favorites.ts +99 -0
- package/eject/account/account-loyalty.ts +241 -0
- package/eject/account/account-page.tsx +410 -0
- package/eject/account/account-payment-method.ts +99 -0
- package/eject/account/account-referrals.ts +68 -0
- package/eject/account/account-reseller-invite.ts +88 -0
- package/eject/account/account-reseller.ts +685 -0
- package/eject/account/account-rewards.ts +104 -0
- package/eject/account/account-settings.ts +181 -0
- package/eject/account/account-signin-link.ts +122 -0
- package/eject/account/account-subscriptions.ts +130 -0
- package/eject/account/account-support.ts +155 -0
- package/eject/account/account-warranties.ts +118 -0
- package/eject/account/customer-account.ts +93 -0
- package/eject/account/dashboard.tsx +139 -0
- package/eject/account/downloads.tsx +343 -0
- package/eject/account/favorites.tsx +201 -0
- package/eject/account/i18n.ts +175 -0
- package/eject/account/icons.tsx +218 -0
- package/eject/account/login.tsx +253 -0
- package/eject/account/messages/ar.json +464 -0
- package/eject/account/messages/de.json +464 -0
- package/eject/account/messages/en.json +464 -0
- package/eject/account/messages/es.json +464 -0
- package/eject/account/messages/fi.json +464 -0
- package/eject/account/messages/fr.json +464 -0
- package/eject/account/messages/hi.json +464 -0
- package/eject/account/messages/it.json +464 -0
- package/eject/account/messages/lt.json +464 -0
- package/eject/account/messages/nl.json +464 -0
- package/eject/account/messages/pl.json +464 -0
- package/eject/account/messages/pt.json +464 -0
- package/eject/account/messages/ru.json +464 -0
- package/eject/account/messages/tr.json +464 -0
- package/eject/account/messages/uk.json +464 -0
- package/eject/account/messages/zh.json +464 -0
- package/eject/account/orders.tsx +791 -0
- package/eject/account/overview.tsx +147 -0
- package/eject/account/referrals.tsx +327 -0
- package/eject/account/reseller.tsx +1365 -0
- package/eject/account/rewards.tsx +641 -0
- package/eject/account/settings.tsx +699 -0
- package/eject/account/shared.tsx +398 -0
- package/eject/account/subscriptions.tsx +421 -0
- package/eject/account/support.tsx +453 -0
- package/package.json +75 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The products a buyer saved for later.
|
|
3
|
+
*
|
|
4
|
+
* The one panel with a list route of its own (`GET /favorites`), and the only
|
|
5
|
+
* one whose collection the dashboard payload does not carry.
|
|
6
|
+
*
|
|
7
|
+
* No price is shown, and that is a decision rather than an omission. The
|
|
8
|
+
* producer sends `price_display` as a bare number with no currency beside it —
|
|
9
|
+
* it is the product's stored display price, and nothing in this payload says
|
|
10
|
+
* which currency it was measured in. Labelling it with whatever currency the
|
|
11
|
+
* page happens to be pricing in turns 19.99 EUR into 19.99 USD on every shop
|
|
12
|
+
* where the two differ, which is worse than not saying.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useState } from 'react';
|
|
16
|
+
import { Link } from 'react-router-dom';
|
|
17
|
+
import {
|
|
18
|
+
favoriteProductHref,
|
|
19
|
+
narrowFavorites,
|
|
20
|
+
withoutFavorite,
|
|
21
|
+
type CustomerFavorite,
|
|
22
|
+
} from './account-favorites';
|
|
23
|
+
import { callCustomerApi } from './customer-account';
|
|
24
|
+
import { t } from './i18n';
|
|
25
|
+
import { ProductPlaceholderIcon } from './icons';
|
|
26
|
+
import {
|
|
27
|
+
AccountEmpty,
|
|
28
|
+
AccountError,
|
|
29
|
+
AccountRowsSkeleton,
|
|
30
|
+
BusySpinner,
|
|
31
|
+
formatAccountDate,
|
|
32
|
+
useAccountResource,
|
|
33
|
+
} from './shared';
|
|
34
|
+
|
|
35
|
+
export function AccountFavorites() {
|
|
36
|
+
const { state, reload } = useAccountResource('favorites', () =>
|
|
37
|
+
callCustomerApi((sdk) => sdk.favorites()));
|
|
38
|
+
const [favorites, setFavorites] = useState<CustomerFavorite[] | null>(null);
|
|
39
|
+
const [contractBroken, setContractBroken] = useState(false);
|
|
40
|
+
const [removing, setRemoving] = useState<string | null>(null);
|
|
41
|
+
const [removeError, setRemoveError] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* The list is copied into state because removing writes to it. Narrowing runs
|
|
45
|
+
* once per load rather than on every render, and a payload that does not
|
|
46
|
+
* match the producer is RECORDED as broken — never rendered as an empty
|
|
47
|
+
* wishlist, which would tell a buyer their saved products are gone.
|
|
48
|
+
*/
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (state.status !== 'ready') return;
|
|
51
|
+
const narrowed = narrowFavorites(state.data);
|
|
52
|
+
setContractBroken(!narrowed.ok);
|
|
53
|
+
setFavorites(narrowed.ok ? narrowed.favorites : null);
|
|
54
|
+
setRemoveError(null);
|
|
55
|
+
}, [state]);
|
|
56
|
+
|
|
57
|
+
if (state.status === 'loading') {
|
|
58
|
+
return <AccountRowsSkeleton rows={3} label={t('favorites.loading')} />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (state.status === 'error') {
|
|
62
|
+
return <AccountError message={state.message} onRetry={reload} />;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (contractBroken) {
|
|
66
|
+
return <AccountError message={t('common.genericError')} onRetry={reload} />;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The response landed but the effect that narrows it has not run yet.
|
|
70
|
+
if (favorites === null) {
|
|
71
|
+
return <AccountRowsSkeleton rows={3} label={t('favorites.loading')} />;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (favorites.length === 0) {
|
|
75
|
+
return (
|
|
76
|
+
<AccountEmpty
|
|
77
|
+
title={t('favorites.emptyTitle')}
|
|
78
|
+
body={t('favorites.emptyBody')}
|
|
79
|
+
action={(
|
|
80
|
+
<Link to="/all-products" className="sx-account-cta sxa:cta-button sxa:inline-flex sxa:h-10 sxa:items-center sxa:px-5 sxa:text-small">
|
|
81
|
+
{t('common.browseStore')}
|
|
82
|
+
</Link>
|
|
83
|
+
)}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const remove = async (favorite: CustomerFavorite) => {
|
|
89
|
+
setRemoving(favorite.product_uniqid);
|
|
90
|
+
setRemoveError(null);
|
|
91
|
+
const result = await callCustomerApi((sdk) => sdk.removeFavorite(favorite.product_uniqid));
|
|
92
|
+
setRemoving(null);
|
|
93
|
+
|
|
94
|
+
if (!result.success) {
|
|
95
|
+
setRemoveError(result.message?.trim() || t('favorites.removeFailed'));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Dropped only once the server confirmed it. Removing the card first and
|
|
100
|
+
// putting it back on failure makes the buyer watch their own action undo
|
|
101
|
+
// itself, and leaves the two lists disagreeing whenever a retry is slow.
|
|
102
|
+
setFavorites((current) => (
|
|
103
|
+
current ? withoutFavorite(current, favorite.product_uniqid) : current
|
|
104
|
+
));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div>
|
|
109
|
+
{removeError ? (
|
|
110
|
+
<div className="sxa:mb-6">
|
|
111
|
+
<AccountError message={removeError} />
|
|
112
|
+
</div>
|
|
113
|
+
) : null}
|
|
114
|
+
|
|
115
|
+
<ul className="sxa:grid sxa:gap-4 sxa:sm:grid-cols-2 sxa:lg:grid-cols-3">
|
|
116
|
+
{favorites.map((favorite) => (
|
|
117
|
+
<li
|
|
118
|
+
key={favorite.product_uniqid}
|
|
119
|
+
className="sxa:flex sxa:flex-col sxa:overflow-hidden sxa:rounded-input sxa:border sxa:bg-surface sxa:shadow-card"
|
|
120
|
+
>
|
|
121
|
+
<FavoriteImage favorite={favorite} />
|
|
122
|
+
<div className="sxa:flex sxa:flex-1 sxa:flex-col sxa:p-5">
|
|
123
|
+
<p className="sxa:text-small sxa:font-semibold">{favorite.title}</p>
|
|
124
|
+
<p className="sxa:mt-1 sxa:text-micro sxa:text-muted">
|
|
125
|
+
{t('favorites.added', { date: formatAccountDate(favorite.created_at) })}
|
|
126
|
+
</p>
|
|
127
|
+
{/* The merchant deleted the product. The row stays so the buyer
|
|
128
|
+
can clear it; the link is what goes away. */}
|
|
129
|
+
{favorite.unavailable ? (
|
|
130
|
+
<p className="sxa:mt-2 sxa:text-micro sxa:font-medium sxa:text-muted">{t('favorites.gone')}</p>
|
|
131
|
+
) : null}
|
|
132
|
+
|
|
133
|
+
<div className="sxa:mt-auto sxa:flex sxa:flex-wrap sxa:items-center sxa:gap-2 sxa:pt-4">
|
|
134
|
+
<FavoriteLink favorite={favorite} />
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={() => void remove(favorite)}
|
|
138
|
+
disabled={removing !== null}
|
|
139
|
+
aria-busy={removing === favorite.product_uniqid || undefined}
|
|
140
|
+
className="sx-account-button sxa:card-button sxa:card-button--add sxa:disabled:cursor-not-allowed sxa:disabled:opacity-50"
|
|
141
|
+
>
|
|
142
|
+
{removing === favorite.product_uniqid ? (
|
|
143
|
+
<BusySpinner className="sxa:mr-1.5 sxa:h-3.5 sxa:w-3.5" />
|
|
144
|
+
) : null}
|
|
145
|
+
{removing === favorite.product_uniqid
|
|
146
|
+
? t('favorites.removing')
|
|
147
|
+
: t('favorites.remove')}
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</li>
|
|
152
|
+
))}
|
|
153
|
+
</ul>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The saved product's picture, or the same placeholder tile the cart draws.
|
|
160
|
+
*
|
|
161
|
+
* An image URL that fails to load falls back to the placeholder rather than
|
|
162
|
+
* leaving a broken-image icon in the grid — the product is still saved either
|
|
163
|
+
* way, and the card must stay readable.
|
|
164
|
+
*/
|
|
165
|
+
function FavoriteImage({ favorite }: { favorite: CustomerFavorite }) {
|
|
166
|
+
const [failed, setFailed] = useState(false);
|
|
167
|
+
|
|
168
|
+
if (!favorite.image_url || failed) {
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
className="sxa:flex sxa:aspect-[4/3] sxa:items-center sxa:justify-center sxa:border-b sxa:bg-bg"
|
|
172
|
+
role="img"
|
|
173
|
+
aria-label={t('favorites.noImage', { title: favorite.title })}
|
|
174
|
+
>
|
|
175
|
+
<ProductPlaceholderIcon className="sxa:h-6 sxa:w-6 sxa:text-muted" strokeWidth="1.25" />
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<img
|
|
182
|
+
src={favorite.image_url}
|
|
183
|
+
alt={favorite.title}
|
|
184
|
+
loading="lazy"
|
|
185
|
+
decoding="async"
|
|
186
|
+
onError={() => setFailed(true)}
|
|
187
|
+
className="sxa:aspect-[4/3] sxa:w-full sxa:border-b sxa:object-cover"
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Nothing at all for a product with nowhere to go — see `favoriteProductHref`. */
|
|
193
|
+
function FavoriteLink({ favorite }: { favorite: CustomerFavorite }) {
|
|
194
|
+
const href = favoriteProductHref(favorite);
|
|
195
|
+
if (!href) return null;
|
|
196
|
+
return (
|
|
197
|
+
<Link to={href} className="sx-account-cta sxa:cta-button sxa:inline-flex sxa:h-8 sxa:items-center sxa:px-4 sxa:text-micro sxa:font-semibold">
|
|
198
|
+
{t('favorites.view')}
|
|
199
|
+
</Link>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The customer account's message catalog.
|
|
3
|
+
*
|
|
4
|
+
* A code storefront is an immutable artifact served from R2 — there is no
|
|
5
|
+
* server render and no request-time catalog injection. Every locale therefore
|
|
6
|
+
* ships INSIDE the account chunk, and the one thing that varies per request is
|
|
7
|
+
* which of them is read.
|
|
8
|
+
*
|
|
9
|
+
* The locale comes from `<html lang>`, which the Shoppex worker rewrites to the
|
|
10
|
+
* locale the page was served in (`withServedHtmlLang` in
|
|
11
|
+
* `workers/storefront/src/code-page-metadata.ts`) before the bundle runs. That
|
|
12
|
+
* is the only locale signal a code storefront gets, and it is the same one the
|
|
13
|
+
* document lane resolves its catalog from — so a buyer on `/de/dashboard` reads
|
|
14
|
+
* German on either lane. `navigator.language` is deliberately NOT consulted: it
|
|
15
|
+
* is the browser's preference, not the page the edge decided to serve, and
|
|
16
|
+
* mixing the two produces a German page with English panels inside it.
|
|
17
|
+
*
|
|
18
|
+
* English is the compiled-in contract. `translate` falls back to it for a key a
|
|
19
|
+
* locale catalog is missing, exactly like the document lane's runtime, and the
|
|
20
|
+
* parity test in `tests/messages.test.ts` is what keeps that fallback from ever
|
|
21
|
+
* being needed in a release.
|
|
22
|
+
*
|
|
23
|
+
* All sixteen are STATIC imports, and that is a decision with a price: they add
|
|
24
|
+
* roughly 66 KB gzipped to a bundle that only ever reads one of them. Splitting
|
|
25
|
+
* them into per-locale chunks would trade those bytes for a second failure mode
|
|
26
|
+
* — a chunk that does not arrive leaves a buyer reading English on a German
|
|
27
|
+
* page, quietly, and the boot would have to become asynchronous to allow for
|
|
28
|
+
* it. Bytes that are always right beat a request that can be wrong; revisit
|
|
29
|
+
* this if the catalog grows past the account surface.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import arMessages from './messages/ar.json';
|
|
33
|
+
import deMessages from './messages/de.json';
|
|
34
|
+
import enMessages from './messages/en.json';
|
|
35
|
+
import esMessages from './messages/es.json';
|
|
36
|
+
import fiMessages from './messages/fi.json';
|
|
37
|
+
import frMessages from './messages/fr.json';
|
|
38
|
+
import hiMessages from './messages/hi.json';
|
|
39
|
+
import itMessages from './messages/it.json';
|
|
40
|
+
import ltMessages from './messages/lt.json';
|
|
41
|
+
import nlMessages from './messages/nl.json';
|
|
42
|
+
import plMessages from './messages/pl.json';
|
|
43
|
+
import ptMessages from './messages/pt.json';
|
|
44
|
+
import ruMessages from './messages/ru.json';
|
|
45
|
+
import trMessages from './messages/tr.json';
|
|
46
|
+
import ukMessages from './messages/uk.json';
|
|
47
|
+
import zhMessages from './messages/zh.json';
|
|
48
|
+
|
|
49
|
+
export type MessageParams = Record<string, string | number>;
|
|
50
|
+
export type MessageCatalog = { [key: string]: string | MessageCatalog };
|
|
51
|
+
|
|
52
|
+
/** The locales the Shoppex storefront edge can serve. */
|
|
53
|
+
export const SUPPORTED_LOCALES = [
|
|
54
|
+
'ar', 'de', 'en', 'es', 'fi', 'fr', 'hi', 'it',
|
|
55
|
+
'lt', 'nl', 'pl', 'pt', 'ru', 'tr', 'uk', 'zh',
|
|
56
|
+
] as const;
|
|
57
|
+
|
|
58
|
+
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
|
59
|
+
|
|
60
|
+
export const DEFAULT_LOCALE: Locale = 'en';
|
|
61
|
+
|
|
62
|
+
export const MESSAGE_CATALOGS: Readonly<Record<Locale, MessageCatalog>> = {
|
|
63
|
+
ar: arMessages as MessageCatalog,
|
|
64
|
+
de: deMessages as MessageCatalog,
|
|
65
|
+
en: enMessages as MessageCatalog,
|
|
66
|
+
es: esMessages as MessageCatalog,
|
|
67
|
+
fi: fiMessages as MessageCatalog,
|
|
68
|
+
fr: frMessages as MessageCatalog,
|
|
69
|
+
hi: hiMessages as MessageCatalog,
|
|
70
|
+
it: itMessages as MessageCatalog,
|
|
71
|
+
lt: ltMessages as MessageCatalog,
|
|
72
|
+
nl: nlMessages as MessageCatalog,
|
|
73
|
+
pl: plMessages as MessageCatalog,
|
|
74
|
+
pt: ptMessages as MessageCatalog,
|
|
75
|
+
ru: ruMessages as MessageCatalog,
|
|
76
|
+
tr: trMessages as MessageCatalog,
|
|
77
|
+
uk: ukMessages as MessageCatalog,
|
|
78
|
+
zh: zhMessages as MessageCatalog,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const EN_MESSAGES: MessageCatalog = MESSAGE_CATALOGS.en;
|
|
82
|
+
|
|
83
|
+
/** Locales written right-to-left, so the account shell can flip its direction. */
|
|
84
|
+
const RTL_LOCALES: ReadonlySet<Locale> = new Set<Locale>(['ar']);
|
|
85
|
+
|
|
86
|
+
export function isSupportedLocale(value: string): value is Locale {
|
|
87
|
+
return (SUPPORTED_LOCALES as readonly string[]).includes(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The catalog a `lang` attribute names.
|
|
92
|
+
*
|
|
93
|
+
* `de-AT` and `DE` both resolve to `de`: the edge writes a bare two-letter code
|
|
94
|
+
* today, and a region-tagged one must not silently read as "no locale at all".
|
|
95
|
+
* Anything that names no catalog is English, which is the complete contract
|
|
96
|
+
* rather than a guess at what the buyer wanted.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveLocale(lang: string | null | undefined): Locale {
|
|
99
|
+
const base = (lang ?? '').trim().toLowerCase().split(/[-_]/)[0] ?? '';
|
|
100
|
+
return isSupportedLocale(base) ? base : DEFAULT_LOCALE;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isRightToLeft(locale: Locale): boolean {
|
|
104
|
+
return RTL_LOCALES.has(locale);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function messageAt(catalog: MessageCatalog | undefined, key: string): string | undefined {
|
|
108
|
+
let value: string | MessageCatalog | undefined = catalog;
|
|
109
|
+
for (const segment of key.split('.')) {
|
|
110
|
+
if (!value || typeof value === 'string') return undefined;
|
|
111
|
+
value = value[segment];
|
|
112
|
+
}
|
|
113
|
+
return typeof value === 'string' ? value : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function interpolate(message: string, params: MessageParams | undefined): string {
|
|
117
|
+
if (!params) return message;
|
|
118
|
+
return message.replace(/\{([^{}]+)\}/g, (token, name: string) => (
|
|
119
|
+
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : token
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether English — the contract — defines this key at all. */
|
|
124
|
+
export function hasMessage(key: string): boolean {
|
|
125
|
+
return messageAt(EN_MESSAGES, key) !== undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function translate(locale: Locale, key: string, params?: MessageParams): string {
|
|
129
|
+
const message = messageAt(MESSAGE_CATALOGS[locale], key) ?? messageAt(EN_MESSAGES, key);
|
|
130
|
+
// The key itself, so a missing string is visible in the UI rather than blank.
|
|
131
|
+
if (message === undefined) return key;
|
|
132
|
+
return interpolate(message, params);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A message only when one exists.
|
|
137
|
+
*
|
|
138
|
+
* Wire enums (invoice status, reward reason, delivery state) are OPEN sets: the
|
|
139
|
+
* server may emit a value this catalog has never heard of. Those callers need
|
|
140
|
+
* "no translation" as an answer they can act on, so they can print the server's
|
|
141
|
+
* own spelling instead of the key.
|
|
142
|
+
*/
|
|
143
|
+
export function translateOptional(
|
|
144
|
+
locale: Locale,
|
|
145
|
+
key: string,
|
|
146
|
+
params?: MessageParams,
|
|
147
|
+
): string | undefined {
|
|
148
|
+
return hasMessage(key) ? translate(locale, key, params) : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let activeLocale: Locale | undefined;
|
|
152
|
+
|
|
153
|
+
function documentLocale(): Locale {
|
|
154
|
+
if (typeof document === 'undefined') return DEFAULT_LOCALE;
|
|
155
|
+
return resolveLocale(document.documentElement.getAttribute('lang'));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The locale this page load is being read in. Resolved once, from the edge. */
|
|
159
|
+
export function getActiveLocale(): Locale {
|
|
160
|
+
if (activeLocale === undefined) activeLocale = documentLocale();
|
|
161
|
+
return activeLocale;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Pins the locale. The tests are the only caller; the browser uses `<html lang>`. */
|
|
165
|
+
export function setActiveLocale(locale: Locale | undefined): void {
|
|
166
|
+
activeLocale = locale;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function t(key: string, params?: MessageParams): string {
|
|
170
|
+
return translate(getActiveLocale(), key, params);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function tOptional(key: string, params?: MessageParams): string | undefined {
|
|
174
|
+
return translateOptional(getActiveLocale(), key, params);
|
|
175
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icons the account draws, inlined.
|
|
3
|
+
*
|
|
4
|
+
* Storefront artifacts are served behind a strict CSP and build with no
|
|
5
|
+
* network, so there is no icon CDN. Every icon is a plain SVG that takes its
|
|
6
|
+
* size and colour from the classes around it (`currentColor`).
|
|
7
|
+
*
|
|
8
|
+
* Outline marks are the 24x24 / stroke-2 Lucide geometry (ISC).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { SVGProps } from 'react';
|
|
12
|
+
|
|
13
|
+
type IconProps = SVGProps<SVGSVGElement>;
|
|
14
|
+
|
|
15
|
+
function OutlineIcon({ children, ...props }: IconProps) {
|
|
16
|
+
return (
|
|
17
|
+
<svg
|
|
18
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
19
|
+
width="24"
|
|
20
|
+
height="24"
|
|
21
|
+
viewBox="0 0 24 24"
|
|
22
|
+
fill="none"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
strokeWidth="2"
|
|
25
|
+
strokeLinecap="round"
|
|
26
|
+
strokeLinejoin="round"
|
|
27
|
+
aria-hidden="true"
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ArrowLeftIcon(props: IconProps) {
|
|
36
|
+
return (
|
|
37
|
+
<OutlineIcon {...props}>
|
|
38
|
+
<path d="m12 19-7-7 7-7" />
|
|
39
|
+
<path d="M19 12H5" />
|
|
40
|
+
</OutlineIcon>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ArrowRightIcon(props: IconProps) {
|
|
45
|
+
return (
|
|
46
|
+
<OutlineIcon {...props}>
|
|
47
|
+
<path d="M5 12h14" />
|
|
48
|
+
<path d="m12 5 7 7-7 7" />
|
|
49
|
+
</OutlineIcon>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Placeholder art for a bundle with no image of its own. */
|
|
54
|
+
export function BoxIcon(props: IconProps) {
|
|
55
|
+
return (
|
|
56
|
+
<OutlineIcon strokeWidth="1.25" {...props}>
|
|
57
|
+
<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
|
|
58
|
+
<path d="M3.3 7 12 12l8.7-5" />
|
|
59
|
+
<path d="M12 22V12" />
|
|
60
|
+
</OutlineIcon>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The tick inside a selected variant pill or a ticked addon box. */
|
|
65
|
+
export function CheckIcon(props: IconProps) {
|
|
66
|
+
return (
|
|
67
|
+
<OutlineIcon strokeWidth="3" {...props}>
|
|
68
|
+
<path d="m5 12 5 5L20 7" />
|
|
69
|
+
</OutlineIcon>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function ChevronDownIcon(props: IconProps) {
|
|
74
|
+
return (
|
|
75
|
+
<OutlineIcon {...props}>
|
|
76
|
+
<path d="m6 9 6 6 6-6" />
|
|
77
|
+
</OutlineIcon>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function CircleAlertIcon(props: IconProps) {
|
|
82
|
+
return (
|
|
83
|
+
<OutlineIcon {...props}>
|
|
84
|
+
<circle cx="12" cy="12" r="10" />
|
|
85
|
+
<line x1="12" x2="12" y1="8" y2="12" />
|
|
86
|
+
<line x1="12" x2="12.01" y1="16" y2="16" />
|
|
87
|
+
</OutlineIcon>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Lucide `copy`. The account surface copies licence keys and referral links. */
|
|
92
|
+
export function CopyIcon(props: IconProps) {
|
|
93
|
+
return (
|
|
94
|
+
<OutlineIcon {...props}>
|
|
95
|
+
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
|
96
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
97
|
+
</OutlineIcon>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Leaves the storefront — every community and portal link carries it. */
|
|
102
|
+
export function ExternalLinkIcon(props: IconProps) {
|
|
103
|
+
return (
|
|
104
|
+
<OutlineIcon {...props}>
|
|
105
|
+
<path d="M15 3h6v6" />
|
|
106
|
+
<path d="M10 14 21 3" />
|
|
107
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
108
|
+
</OutlineIcon>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The reward-program teaser's mark. */
|
|
113
|
+
export function GiftIcon(props: IconProps) {
|
|
114
|
+
return (
|
|
115
|
+
<OutlineIcon {...props}>
|
|
116
|
+
<rect x="3" y="8" width="18" height="4" rx="1" />
|
|
117
|
+
<path d="M12 8v13" />
|
|
118
|
+
<path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7" />
|
|
119
|
+
<path d="M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5" />
|
|
120
|
+
</OutlineIcon>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The spinner on a submitting form. It is the only mark the theme rotates. */
|
|
125
|
+
export function LoaderIcon(props: IconProps) {
|
|
126
|
+
return (
|
|
127
|
+
<OutlineIcon {...props}>
|
|
128
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
129
|
+
</OutlineIcon>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function LockIcon(props: IconProps) {
|
|
134
|
+
return (
|
|
135
|
+
<OutlineIcon {...props}>
|
|
136
|
+
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
|
137
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
138
|
+
</OutlineIcon>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function MailIcon(props: IconProps) {
|
|
143
|
+
return (
|
|
144
|
+
<OutlineIcon {...props}>
|
|
145
|
+
<path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
|
|
146
|
+
<rect x="2" y="4" width="20" height="16" rx="2" />
|
|
147
|
+
</OutlineIcon>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The reviews badge, the contact form's header and its message field. */
|
|
152
|
+
export function MessageSquareIcon(props: IconProps) {
|
|
153
|
+
return (
|
|
154
|
+
<OutlineIcon {...props}>
|
|
155
|
+
<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
|
|
156
|
+
</OutlineIcon>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Placeholder art for a product with no image. */
|
|
161
|
+
export function ProductPlaceholderIcon(props: IconProps) {
|
|
162
|
+
return (
|
|
163
|
+
<OutlineIcon strokeWidth="1.25" {...props}>
|
|
164
|
+
<path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
|
|
165
|
+
<path d="M12 22V12" />
|
|
166
|
+
<polyline points="3.29 7 12 12 20.71 7" />
|
|
167
|
+
<path d="m7.5 4.27 9 5.15" />
|
|
168
|
+
</OutlineIcon>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Marks the order/invoice field the merchant can make mandatory. */
|
|
173
|
+
export function ReceiptIcon(props: IconProps) {
|
|
174
|
+
return (
|
|
175
|
+
<OutlineIcon {...props}>
|
|
176
|
+
<path d="M12 17V7" />
|
|
177
|
+
<path d="M16 8h-6a2 2 0 0 0 0 4h4a2 2 0 0 1 0 4H8" />
|
|
178
|
+
<path d="M4 3a1 1 0 0 1 1-1 1.3 1.3 0 0 1 .7.2l.933.6a1.3 1.3 0 0 0 1.4 0l.934-.6a1.3 1.3 0 0 1 1.4 0l.933.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6A1.3 1.3 0 0 1 19 2a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1 1.3 1.3 0 0 1-.7-.2l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.934.6a1.3 1.3 0 0 1-1.4 0l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-1.4 0l-.934-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-.7.2 1 1 0 0 1-1-1z" />
|
|
179
|
+
</OutlineIcon>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function SearchIcon(props: IconProps) {
|
|
184
|
+
return (
|
|
185
|
+
<OutlineIcon {...props}>
|
|
186
|
+
<path d="m21 21-4.34-4.34" />
|
|
187
|
+
<circle cx="11" cy="11" r="8" />
|
|
188
|
+
</OutlineIcon>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Shared by the trust strip and the why-choose-us feature panels. */
|
|
193
|
+
export function ShieldIcon(props: IconProps) {
|
|
194
|
+
return (
|
|
195
|
+
<OutlineIcon {...props}>
|
|
196
|
+
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
|
|
197
|
+
<path d="m9 12 2 2 4-4" />
|
|
198
|
+
</OutlineIcon>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function TagIcon(props: IconProps) {
|
|
203
|
+
return (
|
|
204
|
+
<OutlineIcon {...props}>
|
|
205
|
+
<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" />
|
|
206
|
+
<circle cx="7.5" cy="7.5" r=".5" fill="currentColor" />
|
|
207
|
+
</OutlineIcon>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Shared by the trust strip and the buy box's delivery badge. */
|
|
212
|
+
export function ZapIcon(props: IconProps) {
|
|
213
|
+
return (
|
|
214
|
+
<OutlineIcon {...props}>
|
|
215
|
+
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z" />
|
|
216
|
+
</OutlineIcon>
|
|
217
|
+
);
|
|
218
|
+
}
|