@base44/app-plugin-commerce 0.9.4 → 0.10.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/README.md +6 -3
- package/base44/functions/commerce/storefront-catalog/entry.ts +5 -0
- package/package.json +1 -1
- package/scripts/install.js +3 -0
- package/skills/commerce/SKILL.md +36 -39
- package/skills/commerce/docs/api-storefront.md +1 -1
- package/skills/commerce/installation/install.md +76 -165
- package/skills/commerce/references/admin-localization.md +6 -4
- package/skills/commerce/references/storefront-ui.md +154 -0
- package/src/commerce/storefront/index.js +6 -3
- package/src/commerce/storefront-ui/CartButton.jsx +33 -0
- package/src/commerce/storefront-ui/CartPage.jsx +140 -0
- package/src/commerce/storefront-ui/CheckoutPage.jsx +287 -0
- package/src/commerce/storefront-ui/MiniCart.jsx +128 -0
- package/src/commerce/storefront-ui/OrderReceivedPage.jsx +159 -0
- package/src/commerce/storefront-ui/i18n/index.js +51 -0
- package/src/commerce/storefront-ui/i18n/locales/de.js +89 -0
- package/src/commerce/storefront-ui/i18n/locales/en.js +100 -0
- package/src/commerce/storefront-ui/i18n/locales/es.js +89 -0
- package/src/commerce/storefront-ui/i18n/locales/fr.js +89 -0
- package/src/commerce/storefront-ui/i18n/locales/ja.js +89 -0
- package/src/commerce/storefront-ui/i18n/locales/pt.js +89 -0
- package/src/commerce/storefront-ui/index.js +16 -0
- package/src/commerce/storefront-ui/internal.jsx +218 -0
- package/src/commerce/storefront-ui/storefront-ui.css +448 -0
- package/src/commerce/storefront-ui/theme.js +76 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Français — vouvoiement systématique. Clés identiques à en.js. */
|
|
2
|
+
export default {
|
|
3
|
+
$locale: "fr",
|
|
4
|
+
|
|
5
|
+
"common.loading": "Chargement…",
|
|
6
|
+
"common.error.title": "Une erreur est survenue",
|
|
7
|
+
"common.error.retry": "Réessayer",
|
|
8
|
+
"common.remove": "Retirer",
|
|
9
|
+
"common.quantity": "Quantité",
|
|
10
|
+
"common.decrease": "Diminuer la quantité",
|
|
11
|
+
"common.increase": "Augmenter la quantité",
|
|
12
|
+
"common.free": "Offert",
|
|
13
|
+
"common.totals.subtotal": "Sous-total",
|
|
14
|
+
"common.totals.discount": "Remise",
|
|
15
|
+
"common.totals.shipping": "Livraison",
|
|
16
|
+
"common.totals.tax": "Taxes",
|
|
17
|
+
"common.totals.total": "Total",
|
|
18
|
+
"common.coupon.label": "Code de réduction",
|
|
19
|
+
"common.coupon.placeholder": "Saisissez le code",
|
|
20
|
+
"common.coupon.apply": "Appliquer",
|
|
21
|
+
"common.coupon.applied": "Code {code} appliqué",
|
|
22
|
+
"common.coupon.remove": "Retirer le code",
|
|
23
|
+
|
|
24
|
+
"cart.title": "Votre panier",
|
|
25
|
+
"cart.summaryTitle": "Récapitulatif de commande",
|
|
26
|
+
"cart.checkoutCta": "Passer au paiement",
|
|
27
|
+
"cart.continueShopping": "Continuer vos achats",
|
|
28
|
+
"cart.empty.title": "Votre panier est vide",
|
|
29
|
+
"cart.empty.body": "Tout ce que vous ajoutez apparaîtra ici.",
|
|
30
|
+
"cart.empty.cta": "Découvrir la boutique",
|
|
31
|
+
"cart.notes.label": "Notes de commande",
|
|
32
|
+
"cart.notes.placeholder": "Un détail à nous signaler concernant votre commande ?",
|
|
33
|
+
"cart.taxNote": "Les frais de livraison et les taxes sont calculés au paiement.",
|
|
34
|
+
|
|
35
|
+
"minicart.title": "Votre panier",
|
|
36
|
+
"minicart.close": "Fermer le panier",
|
|
37
|
+
"minicart.subtotal": "Sous-total",
|
|
38
|
+
"minicart.checkout": "Paiement",
|
|
39
|
+
"minicart.viewCart": "Voir le panier",
|
|
40
|
+
"minicart.empty.title": "Votre panier est vide",
|
|
41
|
+
"minicart.empty.cta": "Continuer à explorer",
|
|
42
|
+
"minicart.button": "Ouvrir le panier",
|
|
43
|
+
|
|
44
|
+
"checkout.title": "Paiement",
|
|
45
|
+
"checkout.contactTitle": "Contact et adresse de facturation",
|
|
46
|
+
"checkout.shipToDifferent": "Livrer à une autre adresse",
|
|
47
|
+
"checkout.shippingAddressTitle": "Adresse de livraison",
|
|
48
|
+
"checkout.shippingTitle": "Mode de livraison",
|
|
49
|
+
"checkout.paymentTitle": "Paiement",
|
|
50
|
+
"checkout.summaryTitle": "Récapitulatif de commande",
|
|
51
|
+
"checkout.notes.label": "Notes de commande",
|
|
52
|
+
"checkout.notes.placeholder": "Un détail à nous signaler concernant votre commande ?",
|
|
53
|
+
"checkout.terms": "J'accepte les conditions de vente",
|
|
54
|
+
"checkout.placeOrder": "Commander",
|
|
55
|
+
"checkout.placing": "Votre commande est en cours…",
|
|
56
|
+
"checkout.submitted": "Commande passée — redirection vers votre confirmation…",
|
|
57
|
+
"checkout.empty.title": "Il n'y a encore rien à régler",
|
|
58
|
+
"checkout.empty.cta": "Retour à la boutique",
|
|
59
|
+
"checkout.hint.missing_address": "Saisissez votre adresse pour voir les options de livraison.",
|
|
60
|
+
"checkout.hint.none_available": "Nous ne livrons pas encore à cette adresse.",
|
|
61
|
+
"checkout.hint.syncing": "Mise à jour des options de livraison…",
|
|
62
|
+
"checkout.hint.payment_none": "Le paiement est temporairement indisponible — aucun moyen de paiement n'est activé.",
|
|
63
|
+
"checkout.blocker.cart_loading": "Chargement de votre panier…",
|
|
64
|
+
"checkout.blocker.empty_cart": "Votre panier est vide.",
|
|
65
|
+
"checkout.blocker.billing_incomplete": "Renseignez les champs de contact obligatoires.",
|
|
66
|
+
"checkout.blocker.shipping_address_incomplete": "Renseignez l'adresse de livraison.",
|
|
67
|
+
"checkout.blocker.shipping_recalculating": "Mise à jour des frais de livraison…",
|
|
68
|
+
"checkout.blocker.shipping_address_required": "Saisissez votre adresse pour voir les options de livraison.",
|
|
69
|
+
"checkout.blocker.shipping_method_required": "Choisissez un mode de livraison.",
|
|
70
|
+
"checkout.blocker.shipping_not_available": "Nous ne livrons pas encore à cette adresse.",
|
|
71
|
+
"checkout.blocker.payment_method_required": "Choisissez votre moyen de paiement.",
|
|
72
|
+
"checkout.error.card_payment_in_preview": "Le paiement par carte est impossible dans l'aperçu — ouvrez la boutique publiée pour payer.",
|
|
73
|
+
|
|
74
|
+
"order.orderLabel": "Commande",
|
|
75
|
+
"order.dateLabel": "Date",
|
|
76
|
+
"order.qty": "Qté {count}",
|
|
77
|
+
"order.backToStore": "Retour à la boutique",
|
|
78
|
+
"order.paid.title": "Merci — votre commande est confirmée",
|
|
79
|
+
"order.paid.body": "Une confirmation est en route vers votre boîte mail.",
|
|
80
|
+
"order.unpaid.title": "Commande reçue — en attente de paiement",
|
|
81
|
+
"order.unpaid.body": "Votre commande est en attente jusqu'à réception du paiement. Utilisez les informations ci-dessous.",
|
|
82
|
+
"order.instructionsTitle": "Instructions de paiement",
|
|
83
|
+
"order.payNow": "Finaliser le paiement",
|
|
84
|
+
"order.cancelled.title": "Cette commande a été annulée",
|
|
85
|
+
"order.cancelled.body": "Si ce n'était pas votre intention, vous pouvez retenter le paiement.",
|
|
86
|
+
"order.retryPayment": "Retenter le paiement",
|
|
87
|
+
"order.error.title": "Impossible de vérifier cette commande",
|
|
88
|
+
"order.error.body": "Le lien est peut-être incomplet — utilisez celui de votre e-mail de confirmation.",
|
|
89
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** 日本語 — です・ます調で統一。キーは en.js と同一。 */
|
|
2
|
+
export default {
|
|
3
|
+
$locale: "ja",
|
|
4
|
+
|
|
5
|
+
"common.loading": "読み込み中…",
|
|
6
|
+
"common.error.title": "問題が発生しました",
|
|
7
|
+
"common.error.retry": "再試行",
|
|
8
|
+
"common.remove": "削除",
|
|
9
|
+
"common.quantity": "数量",
|
|
10
|
+
"common.decrease": "数量を減らす",
|
|
11
|
+
"common.increase": "数量を増やす",
|
|
12
|
+
"common.free": "無料",
|
|
13
|
+
"common.totals.subtotal": "小計",
|
|
14
|
+
"common.totals.discount": "割引",
|
|
15
|
+
"common.totals.shipping": "配送料",
|
|
16
|
+
"common.totals.tax": "税",
|
|
17
|
+
"common.totals.total": "合計",
|
|
18
|
+
"common.coupon.label": "クーポンコード",
|
|
19
|
+
"common.coupon.placeholder": "コードを入力",
|
|
20
|
+
"common.coupon.apply": "適用",
|
|
21
|
+
"common.coupon.applied": "コード{code}を適用しました",
|
|
22
|
+
"common.coupon.remove": "コードを削除",
|
|
23
|
+
|
|
24
|
+
"cart.title": "カート",
|
|
25
|
+
"cart.summaryTitle": "ご注文内容",
|
|
26
|
+
"cart.checkoutCta": "レジに進む",
|
|
27
|
+
"cart.continueShopping": "買い物を続ける",
|
|
28
|
+
"cart.empty.title": "カートは空です",
|
|
29
|
+
"cart.empty.body": "追加した商品はここに表示されます。",
|
|
30
|
+
"cart.empty.cta": "ストアを見る",
|
|
31
|
+
"cart.notes.label": "ご注文メモ",
|
|
32
|
+
"cart.notes.placeholder": "ご注文について何かあればお書きください。",
|
|
33
|
+
"cart.taxNote": "配送料と税はレジで計算されます。",
|
|
34
|
+
|
|
35
|
+
"minicart.title": "カート",
|
|
36
|
+
"minicart.close": "カートを閉じる",
|
|
37
|
+
"minicart.subtotal": "小計",
|
|
38
|
+
"minicart.checkout": "レジに進む",
|
|
39
|
+
"minicart.viewCart": "カートを見る",
|
|
40
|
+
"minicart.empty.title": "カートは空です",
|
|
41
|
+
"minicart.empty.cta": "買い物を続ける",
|
|
42
|
+
"minicart.button": "カートを開く",
|
|
43
|
+
|
|
44
|
+
"checkout.title": "ご購入手続き",
|
|
45
|
+
"checkout.contactTitle": "連絡先・請求先住所",
|
|
46
|
+
"checkout.shipToDifferent": "別の住所に配送する",
|
|
47
|
+
"checkout.shippingAddressTitle": "お届け先住所",
|
|
48
|
+
"checkout.shippingTitle": "配送方法",
|
|
49
|
+
"checkout.paymentTitle": "お支払い",
|
|
50
|
+
"checkout.summaryTitle": "ご注文内容",
|
|
51
|
+
"checkout.notes.label": "ご注文メモ",
|
|
52
|
+
"checkout.notes.placeholder": "ご注文について何かあればお書きください。",
|
|
53
|
+
"checkout.terms": "販売条件に同意します",
|
|
54
|
+
"checkout.placeOrder": "注文を確定する",
|
|
55
|
+
"checkout.placing": "ご注文を処理しています…",
|
|
56
|
+
"checkout.submitted": "ご注文を承りました — 確認ページへ移動します…",
|
|
57
|
+
"checkout.empty.title": "ご購入いただける商品がまだありません",
|
|
58
|
+
"checkout.empty.cta": "ストアに戻る",
|
|
59
|
+
"checkout.hint.missing_address": "住所を入力すると配送方法が表示されます。",
|
|
60
|
+
"checkout.hint.none_available": "申し訳ありませんが、その住所への配送はまだ承っておりません。",
|
|
61
|
+
"checkout.hint.syncing": "配送方法を更新しています…",
|
|
62
|
+
"checkout.hint.payment_none": "現在ご購入手続きをご利用いただけません — 有効なお支払い方法がありません。",
|
|
63
|
+
"checkout.blocker.cart_loading": "カートを読み込んでいます…",
|
|
64
|
+
"checkout.blocker.empty_cart": "カートは空です。",
|
|
65
|
+
"checkout.blocker.billing_incomplete": "必須の連絡先項目をご入力ください。",
|
|
66
|
+
"checkout.blocker.shipping_address_incomplete": "お届け先住所をご入力ください。",
|
|
67
|
+
"checkout.blocker.shipping_recalculating": "配送料を更新しています…",
|
|
68
|
+
"checkout.blocker.shipping_address_required": "住所を入力すると配送方法が表示されます。",
|
|
69
|
+
"checkout.blocker.shipping_method_required": "配送方法をお選びください。",
|
|
70
|
+
"checkout.blocker.shipping_not_available": "申し訳ありませんが、その住所への配送はまだ承っておりません。",
|
|
71
|
+
"checkout.blocker.payment_method_required": "お支払い方法をお選びください。",
|
|
72
|
+
"checkout.error.card_payment_in_preview": "プレビューではカード決済を完了できません — 公開されたストアでお支払いください。",
|
|
73
|
+
|
|
74
|
+
"order.orderLabel": "注文番号",
|
|
75
|
+
"order.dateLabel": "日付",
|
|
76
|
+
"order.qty": "数量 {count}",
|
|
77
|
+
"order.backToStore": "ストアに戻る",
|
|
78
|
+
"order.paid.title": "ありがとうございます — ご注文が確定しました",
|
|
79
|
+
"order.paid.body": "確認メールをお送りしています。",
|
|
80
|
+
"order.unpaid.title": "ご注文を受け付けました — お支払い待ち",
|
|
81
|
+
"order.unpaid.body": "お支払いの確認まで、ご注文は保留となります。以下の情報をご利用ください。",
|
|
82
|
+
"order.instructionsTitle": "お支払い方法のご案内",
|
|
83
|
+
"order.payNow": "お支払いを完了する",
|
|
84
|
+
"order.cancelled.title": "このご注文はキャンセルされました",
|
|
85
|
+
"order.cancelled.body": "お心当たりがない場合は、お支払いを再試行できます。",
|
|
86
|
+
"order.retryPayment": "お支払いを再試行",
|
|
87
|
+
"order.error.title": "ご注文を確認できませんでした",
|
|
88
|
+
"order.error.body": "リンクが不完全な可能性があります — 確認メールのリンクをお使いください。",
|
|
89
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Português (Brasil) — tom caloroso e direto. Chaves iguais a en.js. */
|
|
2
|
+
export default {
|
|
3
|
+
$locale: "pt",
|
|
4
|
+
|
|
5
|
+
"common.loading": "Carregando…",
|
|
6
|
+
"common.error.title": "Algo deu errado",
|
|
7
|
+
"common.error.retry": "Tentar novamente",
|
|
8
|
+
"common.remove": "Remover",
|
|
9
|
+
"common.quantity": "Quantidade",
|
|
10
|
+
"common.decrease": "Diminuir quantidade",
|
|
11
|
+
"common.increase": "Aumentar quantidade",
|
|
12
|
+
"common.free": "Grátis",
|
|
13
|
+
"common.totals.subtotal": "Subtotal",
|
|
14
|
+
"common.totals.discount": "Desconto",
|
|
15
|
+
"common.totals.shipping": "Frete",
|
|
16
|
+
"common.totals.tax": "Impostos",
|
|
17
|
+
"common.totals.total": "Total",
|
|
18
|
+
"common.coupon.label": "Cupom de desconto",
|
|
19
|
+
"common.coupon.placeholder": "Digite o código",
|
|
20
|
+
"common.coupon.apply": "Aplicar",
|
|
21
|
+
"common.coupon.applied": "Cupom {code} aplicado",
|
|
22
|
+
"common.coupon.remove": "Remover cupom",
|
|
23
|
+
|
|
24
|
+
"cart.title": "Seu carrinho",
|
|
25
|
+
"cart.summaryTitle": "Resumo do pedido",
|
|
26
|
+
"cart.checkoutCta": "Finalizar compra",
|
|
27
|
+
"cart.continueShopping": "Continuar comprando",
|
|
28
|
+
"cart.empty.title": "Seu carrinho está vazio",
|
|
29
|
+
"cart.empty.body": "Tudo o que você adicionar vai aparecer aqui.",
|
|
30
|
+
"cart.empty.cta": "Ver a loja",
|
|
31
|
+
"cart.notes.label": "Observações do pedido",
|
|
32
|
+
"cart.notes.placeholder": "Algo que devemos saber sobre o seu pedido?",
|
|
33
|
+
"cart.taxNote": "Frete e impostos são calculados na finalização da compra.",
|
|
34
|
+
|
|
35
|
+
"minicart.title": "Seu carrinho",
|
|
36
|
+
"minicart.close": "Fechar carrinho",
|
|
37
|
+
"minicart.subtotal": "Subtotal",
|
|
38
|
+
"minicart.checkout": "Finalizar compra",
|
|
39
|
+
"minicart.viewCart": "Ver carrinho",
|
|
40
|
+
"minicart.empty.title": "Seu carrinho está vazio",
|
|
41
|
+
"minicart.empty.cta": "Continuar explorando",
|
|
42
|
+
"minicart.button": "Abrir carrinho",
|
|
43
|
+
|
|
44
|
+
"checkout.title": "Finalizar compra",
|
|
45
|
+
"checkout.contactTitle": "Contato e endereço de cobrança",
|
|
46
|
+
"checkout.shipToDifferent": "Entregar em outro endereço",
|
|
47
|
+
"checkout.shippingAddressTitle": "Endereço de entrega",
|
|
48
|
+
"checkout.shippingTitle": "Forma de entrega",
|
|
49
|
+
"checkout.paymentTitle": "Pagamento",
|
|
50
|
+
"checkout.summaryTitle": "Resumo do pedido",
|
|
51
|
+
"checkout.notes.label": "Observações do pedido",
|
|
52
|
+
"checkout.notes.placeholder": "Algo que devemos saber sobre o seu pedido?",
|
|
53
|
+
"checkout.terms": "Aceito as condições de venda",
|
|
54
|
+
"checkout.placeOrder": "Fazer pedido",
|
|
55
|
+
"checkout.placing": "Processando seu pedido…",
|
|
56
|
+
"checkout.submitted": "Pedido realizado — levando você para a confirmação…",
|
|
57
|
+
"checkout.empty.title": "Ainda não há nada para finalizar",
|
|
58
|
+
"checkout.empty.cta": "Voltar para a loja",
|
|
59
|
+
"checkout.hint.missing_address": "Informe seu endereço para ver as opções de entrega.",
|
|
60
|
+
"checkout.hint.none_available": "Ainda não entregamos nesse endereço.",
|
|
61
|
+
"checkout.hint.syncing": "Atualizando opções de entrega…",
|
|
62
|
+
"checkout.hint.payment_none": "A finalização está temporariamente indisponível — nenhuma forma de pagamento habilitada.",
|
|
63
|
+
"checkout.blocker.cart_loading": "Carregando seu carrinho…",
|
|
64
|
+
"checkout.blocker.empty_cart": "Seu carrinho está vazio.",
|
|
65
|
+
"checkout.blocker.billing_incomplete": "Preencha os campos de contato obrigatórios.",
|
|
66
|
+
"checkout.blocker.shipping_address_incomplete": "Preencha o endereço de entrega.",
|
|
67
|
+
"checkout.blocker.shipping_recalculating": "Atualizando o valor do frete…",
|
|
68
|
+
"checkout.blocker.shipping_address_required": "Informe seu endereço para ver as opções de entrega.",
|
|
69
|
+
"checkout.blocker.shipping_method_required": "Escolha uma forma de entrega.",
|
|
70
|
+
"checkout.blocker.shipping_not_available": "Ainda não entregamos nesse endereço.",
|
|
71
|
+
"checkout.blocker.payment_method_required": "Escolha como você quer pagar.",
|
|
72
|
+
"checkout.error.card_payment_in_preview": "O pagamento com cartão não funciona na pré-visualização — abra a loja publicada para pagar.",
|
|
73
|
+
|
|
74
|
+
"order.orderLabel": "Pedido",
|
|
75
|
+
"order.dateLabel": "Data",
|
|
76
|
+
"order.qty": "Qtd. {count}",
|
|
77
|
+
"order.backToStore": "Voltar para a loja",
|
|
78
|
+
"order.paid.title": "Obrigado — seu pedido está confirmado",
|
|
79
|
+
"order.paid.body": "Uma confirmação está a caminho do seu e-mail.",
|
|
80
|
+
"order.unpaid.title": "Pedido recebido — aguardando pagamento",
|
|
81
|
+
"order.unpaid.body": "Seu pedido fica em espera até recebermos o pagamento. Use as informações abaixo para concluir.",
|
|
82
|
+
"order.instructionsTitle": "Instruções de pagamento",
|
|
83
|
+
"order.payNow": "Concluir pagamento",
|
|
84
|
+
"order.cancelled.title": "Este pedido foi cancelado",
|
|
85
|
+
"order.cancelled.body": "Se não foi intencional, você pode tentar o pagamento novamente.",
|
|
86
|
+
"order.retryPayment": "Tentar pagamento novamente",
|
|
87
|
+
"order.error.title": "Não conseguimos verificar este pedido",
|
|
88
|
+
"order.error.body": "O link pode estar incompleto — use o link do seu e-mail de confirmação.",
|
|
89
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* storefront-ui — the shipped, tested conversion surfaces: cart page, cart
|
|
3
|
+
* drawer, checkout and order-received. They render complete on their own; a
|
|
4
|
+
* store integrates them with a route (or one layout mount for the drawer),
|
|
5
|
+
* a ≤12-token theme block in index.css, and `brand` wording props. Functional
|
|
6
|
+
* labels are localized in ./i18n (repoint one import); brand wording comes in
|
|
7
|
+
* via props and overrides the matching labels.
|
|
8
|
+
*
|
|
9
|
+
* Customize through tokens, `brand`, `sections` and `slots` — these files are
|
|
10
|
+
* plugin-owned and re-copied on kit updates, so direct edits do not survive.
|
|
11
|
+
*/
|
|
12
|
+
export { CartPage } from "./CartPage.jsx";
|
|
13
|
+
export { MiniCart } from "./MiniCart.jsx";
|
|
14
|
+
export { CartButton } from "./CartButton.jsx";
|
|
15
|
+
export { CheckoutPage } from "./CheckoutPage.jsx";
|
|
16
|
+
export { OrderReceivedPage } from "./OrderReceivedPage.jsx";
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import { useCart, useCartLine, useFormatMoney } from "@/commerce/storefront";
|
|
3
|
+
import { attributesLabel, cartTotalsLines } from "@/commerce/utils";
|
|
4
|
+
import { useThemeGuard } from "./theme.js";
|
|
5
|
+
import { t } from "./i18n/index.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal building blocks shared by the shipped storefront surfaces. Not part
|
|
9
|
+
* of the public barrel — pages compose these; stores customize via tokens,
|
|
10
|
+
* `brand` props and `slots`, never by importing from here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Every surface's root: applies the .sfui scope and the theme-solidity guard. */
|
|
14
|
+
export function SfuiRoot({ as: As = "section", className = "", children, ...rest }) {
|
|
15
|
+
const ref = useRef(null);
|
|
16
|
+
useThemeGuard(ref);
|
|
17
|
+
return (
|
|
18
|
+
<As ref={ref} className={`sfui ${className}`} {...rest}>
|
|
19
|
+
{children}
|
|
20
|
+
</As>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function LoadingState({ label }) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="sfui-state" role="status">
|
|
27
|
+
<span className="sfui-spinner" aria-hidden="true" />
|
|
28
|
+
<p className="sfui-muted">{label ?? t("common.loading")}</p>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ErrorState({ title, body, onRetry, retryLabel }) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="sfui-state" role="alert">
|
|
36
|
+
<h2 className="sfui-heading sfui-h2">{title ?? t("common.error.title")}</h2>
|
|
37
|
+
{body && <p className="sfui-muted">{body}</p>}
|
|
38
|
+
{onRetry && (
|
|
39
|
+
<button type="button" className="sfui-btn sfui-btn-ghost sfui-btn-inline" onClick={onRetry}>
|
|
40
|
+
{retryLabel ?? t("common.error.retry")}
|
|
41
|
+
</button>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One cart line, used by CartPage and MiniCart. Quantity edits are optimistic
|
|
49
|
+
* and row-scoped (useCartLine); the stepper never disables during a pending
|
|
50
|
+
* update — the row dims instead.
|
|
51
|
+
*/
|
|
52
|
+
export function CartLineRow({ item, tt, productHref, compact = false, lineExtra }) {
|
|
53
|
+
const line = useCartLine(item);
|
|
54
|
+
const formatMoney = useFormatMoney();
|
|
55
|
+
const attrs = attributesLabel(item.attributes);
|
|
56
|
+
const href = productHref ? productHref(item) : null;
|
|
57
|
+
return (
|
|
58
|
+
<li className="sfui-line" data-pending={line.pending || undefined} data-compact={compact || undefined}>
|
|
59
|
+
<div className="sfui-line-media" aria-hidden="true">
|
|
60
|
+
{item.image?.src && <img src={item.image.src} alt="" loading="lazy" />}
|
|
61
|
+
</div>
|
|
62
|
+
<div className="sfui-line-main">
|
|
63
|
+
<p className="sfui-line-name">
|
|
64
|
+
{href ? (
|
|
65
|
+
<a className="sfui-link" href={href}>
|
|
66
|
+
{item.name}
|
|
67
|
+
</a>
|
|
68
|
+
) : (
|
|
69
|
+
item.name
|
|
70
|
+
)}
|
|
71
|
+
</p>
|
|
72
|
+
{attrs && <p className="sfui-line-attrs">{attrs}</p>}
|
|
73
|
+
<div className="sfui-qty" role="group" aria-label={tt("common.quantity")}>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={line.decrease}
|
|
77
|
+
disabled={!line.canDecrease}
|
|
78
|
+
aria-label={tt("common.decrease")}
|
|
79
|
+
>
|
|
80
|
+
−
|
|
81
|
+
</button>
|
|
82
|
+
<span aria-live="polite">{line.quantity}</span>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
onClick={line.increase}
|
|
86
|
+
disabled={!line.canIncrease}
|
|
87
|
+
aria-label={tt("common.increase")}
|
|
88
|
+
>
|
|
89
|
+
+
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
{line.error && <p className="sfui-error">{line.error.message}</p>}
|
|
93
|
+
{lineExtra && lineExtra({ item, line })}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="sfui-line-end">
|
|
96
|
+
<span className="sfui-line-price">{formatMoney(item.total ?? item.subtotal)}</span>
|
|
97
|
+
<button type="button" className="sfui-line-remove" onClick={line.remove}>
|
|
98
|
+
{tt("common.remove")}
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
</li>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The totals breakdown, labels localized through the bound `tt`. */
|
|
106
|
+
export function TotalsBlock({ cart, tt }) {
|
|
107
|
+
const formatMoney = useFormatMoney();
|
|
108
|
+
const lines = cartTotalsLines(cart, {
|
|
109
|
+
formatMoney,
|
|
110
|
+
labels: {
|
|
111
|
+
subtotal: tt("common.totals.subtotal"),
|
|
112
|
+
discount: tt("common.totals.discount"),
|
|
113
|
+
shipping: tt("common.totals.shipping"),
|
|
114
|
+
tax: tt("common.totals.tax"),
|
|
115
|
+
total: tt("common.totals.total"),
|
|
116
|
+
},
|
|
117
|
+
}).filter((l) => !l.hidden);
|
|
118
|
+
return (
|
|
119
|
+
<dl className="sfui-totals">
|
|
120
|
+
{lines.map((l) => (
|
|
121
|
+
<div key={l.key} data-emphasis={l.emphasis || undefined}>
|
|
122
|
+
<dt>{l.label}</dt>
|
|
123
|
+
<dd>{l.formatted}</dd>
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</dl>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Discount-code entry plus the applied-coupon rows. Renders errors from
|
|
132
|
+
* `applyCoupon`'s own result — never throws into the page.
|
|
133
|
+
*/
|
|
134
|
+
export function CouponField({ tt }) {
|
|
135
|
+
const { cart, applyCoupon, removeCoupon } = useCart();
|
|
136
|
+
const [code, setCode] = useState("");
|
|
137
|
+
const [busy, setBusy] = useState(false);
|
|
138
|
+
const [error, setError] = useState(null);
|
|
139
|
+
const applied = cart?.coupons ?? [];
|
|
140
|
+
|
|
141
|
+
const submit = async () => {
|
|
142
|
+
const value = code.trim();
|
|
143
|
+
if (!value || busy) return;
|
|
144
|
+
setBusy(true);
|
|
145
|
+
setError(null);
|
|
146
|
+
const res = await applyCoupon(value);
|
|
147
|
+
setBusy(false);
|
|
148
|
+
if (res.ok) setCode("");
|
|
149
|
+
else setError(res.message || res.code || "error");
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div>
|
|
154
|
+
<label className="sfui-label" htmlFor="sfui-coupon">
|
|
155
|
+
{tt("common.coupon.label")}
|
|
156
|
+
</label>
|
|
157
|
+
<div className="sfui-coupon">
|
|
158
|
+
<input
|
|
159
|
+
id="sfui-coupon"
|
|
160
|
+
value={code}
|
|
161
|
+
placeholder={tt("common.coupon.placeholder")}
|
|
162
|
+
onChange={(e) => setCode(e.target.value)}
|
|
163
|
+
onKeyDown={(e) => {
|
|
164
|
+
if (e.key === "Enter") {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
submit();
|
|
167
|
+
}
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
className="sfui-btn sfui-btn-ghost sfui-btn-inline"
|
|
173
|
+
onClick={submit}
|
|
174
|
+
disabled={busy || !code.trim()}
|
|
175
|
+
>
|
|
176
|
+
{tt("common.coupon.apply")}
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
{error && <p className="sfui-error">{error}</p>}
|
|
180
|
+
{applied.map((c) => (
|
|
181
|
+
<div className="sfui-coupon-applied" key={c.code} style={{ marginBlockStart: "0.5rem" }}>
|
|
182
|
+
<span>{tt("common.coupon.applied", { code: String(c.code).toUpperCase() })}</span>
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
className="sfui-line-remove"
|
|
186
|
+
onClick={() => removeCoupon(c.code)}
|
|
187
|
+
aria-label={tt("common.coupon.remove")}
|
|
188
|
+
>
|
|
189
|
+
{tt("common.coupon.remove")}
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** `sections.coupon: "auto"` — show when coupons exist (store-level or on the cart). */
|
|
198
|
+
export function couponVisible(section, cart, info) {
|
|
199
|
+
if (section === true || section === false) return section;
|
|
200
|
+
return Boolean(cart?.coupons?.length || info?.has_coupons);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The order-notes textarea; the value travels cart → checkout via sessionStorage. */
|
|
204
|
+
export const ORDER_NOTE_KEY = "sfui-order-note";
|
|
205
|
+
export function readOrderNote() {
|
|
206
|
+
try {
|
|
207
|
+
return window.sessionStorage.getItem(ORDER_NOTE_KEY) ?? "";
|
|
208
|
+
} catch {
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export function writeOrderNote(value) {
|
|
213
|
+
try {
|
|
214
|
+
window.sessionStorage.setItem(ORDER_NOTE_KEY, value);
|
|
215
|
+
} catch {
|
|
216
|
+
/* private mode — the note just won't persist across pages */
|
|
217
|
+
}
|
|
218
|
+
}
|