@agence-zef/astro 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/components/AccountForm.astro +253 -0
- package/components/AddToCart.astro +107 -0
- package/components/Cart.astro +141 -0
- package/components/Checkout.astro +307 -0
- package/components/ReservedNotice.astro +33 -0
- package/components/Subscriptions.astro +91 -0
- package/components/VideoPlayer.astro +71 -0
- package/components/routes/account.ts +23 -0
- package/components/routes/reserved.astro +54 -0
- package/components/routes/shop.ts +21 -0
- package/components/virtual.d.ts +4 -0
- package/dist/accounts.d.ts +65 -0
- package/dist/accounts.js +301 -0
- package/dist/helpers.d.ts +9 -0
- package/dist/helpers.js +28 -2
- package/dist/index.d.ts +22 -0
- package/dist/index.js +48 -1
- package/dist/middleware.d.ts +7 -0
- package/dist/middleware.js +20 -0
- package/dist/overlay.js +10 -2
- package/dist/shop.d.ts +62 -0
- package/dist/shop.js +384 -0
- package/package.json +26 -4
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# @agence-zef/astro
|
|
2
2
|
|
|
3
|
-
Intégration Astro du CMS de l'agence ZEF : route des pages, aperçu de l'éditeur, composants `Button`, `Image`, `Video`, `RichText`, `Form`, `Popups`, `CookieBanner
|
|
3
|
+
Intégration Astro du CMS de l'agence ZEF : route des pages, aperçu de l'éditeur, composants `Button`, `Image`, `Video`, `RichText`, `Form`, `Popups`, `CookieBanner`, et comptes du site (option `accounts`, `AccountForm`, `getAccount`).
|
|
4
4
|
|
|
5
5
|
Paquet du CMS de l'agence ZEF (dépôt privé). Licence MIT.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Formulaire de compte du module Comptes (§ 7 U), sans style imposé : connexion, inscription,
|
|
4
|
+
* déconnexion, mot de passe oublié, nouveau mot de passe (page du lien reçu, `?token=`),
|
|
5
|
+
* profil, renvoi du lien de vérification, suppression du compte. Envoyé sans JavaScript à
|
|
6
|
+
* `/__zef/account/<action>` (option `accounts` de l'intégration), dans une page rendue à la
|
|
7
|
+
* demande (`export const prerender = false`). Messages d'erreur et de succès affichés au retour.
|
|
8
|
+
* Inscription et profil affichent aussi les champs personnalisés du compte déclarés dans
|
|
9
|
+
* `zef.config.ts` (`defineFields`, entité `accounts.account`), nommés `custom.<champ>`.
|
|
10
|
+
* Classes `zef-account__*` et attributs `data-*` pour le CSS du site.
|
|
11
|
+
*/
|
|
12
|
+
import config from "virtual:zef/config";
|
|
13
|
+
import {
|
|
14
|
+
ACCOUNT_ERRORS,
|
|
15
|
+
ACCOUNT_SUCCESS,
|
|
16
|
+
type AccountAction,
|
|
17
|
+
getAccount,
|
|
18
|
+
getAccountsSettings,
|
|
19
|
+
MESSAGE_COOKIE,
|
|
20
|
+
} from "@agence-zef/astro/accounts";
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
action: AccountAction;
|
|
24
|
+
/** Page où aller après un succès (sinon `?redirect=` de l'adresse, puis l'accueil). */
|
|
25
|
+
redirect?: string;
|
|
26
|
+
/** Libellé du bouton. */
|
|
27
|
+
submitLabel?: string;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { action, submitLabel, class: className } = Astro.props;
|
|
32
|
+
const params = Astro.url.searchParams;
|
|
33
|
+
const redirect = Astro.props.redirect ?? params.get("redirect") ?? "";
|
|
34
|
+
const settings = await getAccountsSettings();
|
|
35
|
+
const needsAccount =
|
|
36
|
+
action === "profile" || action === "resend-verification" || action === "delete";
|
|
37
|
+
const account = needsAccount ? await getAccount(Astro) : null;
|
|
38
|
+
const errorCode = params.get("zef_form") === action ? params.get("zef_error") : null;
|
|
39
|
+
const success = params.get("zef_ok") === action ? ACCOUNT_SUCCESS[action] : null;
|
|
40
|
+
// Page du lien « mot de passe oublié » ouverte sans jeton : rien à saisir.
|
|
41
|
+
const missingToken = action === "reset-password" && !params.get("token") && !success;
|
|
42
|
+
const errorKey = errorCode ?? (missingToken ? "invalid_token" : null);
|
|
43
|
+
const error = errorKey
|
|
44
|
+
? errorKey === "refused" && Astro.cookies.get(MESSAGE_COOKIE)?.value
|
|
45
|
+
? Astro.cookies.get(MESSAGE_COOKIE)?.value
|
|
46
|
+
: (ACCOUNT_ERRORS[errorKey] ?? ACCOUNT_ERRORS.unknown)
|
|
47
|
+
: null;
|
|
48
|
+
// Mot de passe enregistré ou lien manquant : le formulaire n'a plus lieu d'être.
|
|
49
|
+
const done = action === "reset-password" && (Boolean(success) || missingToken);
|
|
50
|
+
const token = params.get("token") ?? "";
|
|
51
|
+
const id = `zef-account-${action}`;
|
|
52
|
+
|
|
53
|
+
const LABELS: Record<AccountAction, string> = {
|
|
54
|
+
"sign-in": "Se connecter",
|
|
55
|
+
"sign-up": "Créer mon compte",
|
|
56
|
+
"sign-out": "Se déconnecter",
|
|
57
|
+
"forgot-password": "Recevoir un lien",
|
|
58
|
+
"reset-password": "Enregistrer le mot de passe",
|
|
59
|
+
profile: "Enregistrer",
|
|
60
|
+
"resend-verification": "Renvoyer le lien de vérification",
|
|
61
|
+
delete: "Supprimer mon compte",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type FieldDef = {
|
|
65
|
+
name: string;
|
|
66
|
+
label: string;
|
|
67
|
+
type: string;
|
|
68
|
+
autocomplete: string;
|
|
69
|
+
required: boolean;
|
|
70
|
+
value?: string;
|
|
71
|
+
minlength?: number;
|
|
72
|
+
};
|
|
73
|
+
const email = (value?: string): FieldDef => ({
|
|
74
|
+
name: "email",
|
|
75
|
+
label: "Email",
|
|
76
|
+
type: "email",
|
|
77
|
+
autocomplete: "email",
|
|
78
|
+
required: true,
|
|
79
|
+
value,
|
|
80
|
+
});
|
|
81
|
+
const password = (autocomplete: string, label = "Mot de passe"): FieldDef => ({
|
|
82
|
+
name: "password",
|
|
83
|
+
label,
|
|
84
|
+
type: "password",
|
|
85
|
+
autocomplete,
|
|
86
|
+
required: true,
|
|
87
|
+
minlength: autocomplete === "new-password" ? 8 : undefined,
|
|
88
|
+
});
|
|
89
|
+
const names = (a?: { firstName: string; lastName: string } | null): FieldDef[] => [
|
|
90
|
+
{
|
|
91
|
+
name: "firstName",
|
|
92
|
+
label: "Prénom",
|
|
93
|
+
type: "text",
|
|
94
|
+
autocomplete: "given-name",
|
|
95
|
+
required: true,
|
|
96
|
+
value: a?.firstName,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "lastName",
|
|
100
|
+
label: "Nom",
|
|
101
|
+
type: "text",
|
|
102
|
+
autocomplete: "family-name",
|
|
103
|
+
required: true,
|
|
104
|
+
value: a?.lastName,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
const phone = (value?: string | null): FieldDef[] =>
|
|
108
|
+
!settings || settings.phone === "none"
|
|
109
|
+
? []
|
|
110
|
+
: [
|
|
111
|
+
{
|
|
112
|
+
name: "phone",
|
|
113
|
+
label: "Téléphone",
|
|
114
|
+
type: "tel",
|
|
115
|
+
autocomplete: "tel",
|
|
116
|
+
required: settings.phone === "required",
|
|
117
|
+
value: value ?? undefined,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const fields: FieldDef[] = {
|
|
122
|
+
"sign-in": [email(), password("current-password")],
|
|
123
|
+
"sign-up": [...names(), email(), ...phone(), password("new-password")],
|
|
124
|
+
"sign-out": [],
|
|
125
|
+
"forgot-password": [email()],
|
|
126
|
+
"reset-password": [password("new-password", "Nouveau mot de passe")],
|
|
127
|
+
profile: account ? [...names(account), email(account.email), ...phone(account.phone)] : [],
|
|
128
|
+
"resend-verification": [],
|
|
129
|
+
delete: [password("current-password", "Mot de passe pour confirmer")],
|
|
130
|
+
}[action];
|
|
131
|
+
// Champs personnalisés du compte (`defineFields`), à l'inscription et dans le profil.
|
|
132
|
+
const customFields =
|
|
133
|
+
action === "sign-up" || (action === "profile" && account)
|
|
134
|
+
? Object.entries(config.fields?.find((d) => d.entity === "accounts.account")?.fields ?? {})
|
|
135
|
+
: [];
|
|
136
|
+
const customValue = (key: string): unknown =>
|
|
137
|
+
(account?.custom as Record<string, unknown> | undefined)?.[key];
|
|
138
|
+
const turnstile = action === "sign-up" ? settings?.turnstileSiteKey : null;
|
|
139
|
+
const hidden =
|
|
140
|
+
(needsAccount && !account) || (action === "resend-verification" && account?.emailVerified);
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
{
|
|
144
|
+
settings && !hidden && (
|
|
145
|
+
<form id={id} class:list={["zef-account", className]} data-action={action} method="post" action={`/__zef/account/${action}`}>
|
|
146
|
+
<input type="hidden" name="zef_page" value={Astro.url.pathname} />
|
|
147
|
+
{redirect && <input type="hidden" name="zef_redirect" value={redirect} />}
|
|
148
|
+
{action === "reset-password" && <input type="hidden" name="token" value={token} />}
|
|
149
|
+
{error && (
|
|
150
|
+
<p class="zef-account__error" role="alert" data-code={errorKey}>
|
|
151
|
+
{error}
|
|
152
|
+
</p>
|
|
153
|
+
)}
|
|
154
|
+
{success && (
|
|
155
|
+
<p class="zef-account__success" role="status">
|
|
156
|
+
{success}
|
|
157
|
+
</p>
|
|
158
|
+
)}
|
|
159
|
+
{action === "profile" && account && !account.emailVerified && (
|
|
160
|
+
<p class="zef-account__notice">Adresse email pas encore vérifiée : cliquez sur le lien reçu.</p>
|
|
161
|
+
)}
|
|
162
|
+
{!done && fields.map((f) => (
|
|
163
|
+
<div class="zef-account__field" data-field={f.name}>
|
|
164
|
+
<label for={`${id}-${f.name}`}>
|
|
165
|
+
{f.label}
|
|
166
|
+
{!f.required && <span class="zef-account__optional"> (facultatif)</span>}
|
|
167
|
+
</label>
|
|
168
|
+
<input
|
|
169
|
+
id={`${id}-${f.name}`}
|
|
170
|
+
name={f.name}
|
|
171
|
+
type={f.type}
|
|
172
|
+
autocomplete={f.autocomplete}
|
|
173
|
+
required={f.required}
|
|
174
|
+
minlength={f.minlength}
|
|
175
|
+
value={f.value}
|
|
176
|
+
/>
|
|
177
|
+
{f.minlength && <p class="zef-account__help">{f.minlength} caractères minimum.</p>}
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
{!done && customFields.length > 0 && <input type="hidden" name="zef_custom" value={customFields.map(([k]) => k).join(",")} />}
|
|
181
|
+
{!done && customFields.map(([key, f]) => {
|
|
182
|
+
const fid = `${id}-custom-${key}`;
|
|
183
|
+
const name = `custom.${key}`;
|
|
184
|
+
const value = customValue(key);
|
|
185
|
+
const optional = !f.required && <span class="zef-account__optional"> (facultatif)</span>;
|
|
186
|
+
if (f.type === "boolean") {
|
|
187
|
+
return (
|
|
188
|
+
<div class="zef-account__field" data-field={name} data-type="boolean">
|
|
189
|
+
<label for={fid}>
|
|
190
|
+
<input id={fid} name={name} type="checkbox" value="1" checked={value === true} />
|
|
191
|
+
{f.label}
|
|
192
|
+
</label>
|
|
193
|
+
{f.help && <p class="zef-account__help">{f.help}</p>}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (f.type === "select" && f.multiple) {
|
|
198
|
+
const chosen = Array.isArray(value) ? value : [];
|
|
199
|
+
return (
|
|
200
|
+
<fieldset class="zef-account__field" data-field={name} data-type="select">
|
|
201
|
+
<legend>{f.label}{optional}</legend>
|
|
202
|
+
{Object.entries(f.options).map(([k, label]) => (
|
|
203
|
+
<label>
|
|
204
|
+
<input name={name} type="checkbox" value={k} checked={chosen.includes(k)} />
|
|
205
|
+
{label}
|
|
206
|
+
</label>
|
|
207
|
+
))}
|
|
208
|
+
{f.help && <p class="zef-account__help">{f.help}</p>}
|
|
209
|
+
</fieldset>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return (
|
|
213
|
+
<div class="zef-account__field" data-field={name} data-type={f.type}>
|
|
214
|
+
<label for={fid}>
|
|
215
|
+
{f.label}
|
|
216
|
+
{optional}
|
|
217
|
+
</label>
|
|
218
|
+
{f.type === "textarea" ? (
|
|
219
|
+
<textarea id={fid} name={name} required={f.required} maxlength={f.maxLength}>{typeof value === "string" ? value : ""}</textarea>
|
|
220
|
+
) : f.type === "select" ? (
|
|
221
|
+
<select id={fid} name={name} required={f.required}>
|
|
222
|
+
<option value="">—</option>
|
|
223
|
+
{Object.entries(f.options).map(([k, label]) => (
|
|
224
|
+
<option value={k} selected={value === k}>{label}</option>
|
|
225
|
+
))}
|
|
226
|
+
</select>
|
|
227
|
+
) : (
|
|
228
|
+
<input
|
|
229
|
+
id={fid}
|
|
230
|
+
name={name}
|
|
231
|
+
type={f.type === "number" ? "number" : f.type === "date" ? (f.time ? "datetime-local" : "date") : "text"}
|
|
232
|
+
required={f.required}
|
|
233
|
+
maxlength={f.type === "text" ? f.maxLength : undefined}
|
|
234
|
+
min={f.type === "number" ? f.min : undefined}
|
|
235
|
+
max={f.type === "number" ? f.max : undefined}
|
|
236
|
+
step={f.type === "number" ? (f.step ?? "any") : undefined}
|
|
237
|
+
value={value === null || value === undefined ? undefined : String(value)}
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
{f.help && <p class="zef-account__help">{f.help}</p>}
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
})}
|
|
244
|
+
{turnstile && <div class="cf-turnstile zef-account__turnstile" data-sitekey={turnstile} data-size="flexible" />}
|
|
245
|
+
{!done && (
|
|
246
|
+
<button type="submit" class="zef-account__submit">
|
|
247
|
+
{submitLabel ?? LABELS[action]}
|
|
248
|
+
</button>
|
|
249
|
+
)}
|
|
250
|
+
</form>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
{turnstile && <script is:inline src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer />}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { formatPrice } from "@agence-zef/astro/shop";
|
|
3
|
+
/**
|
|
4
|
+
* Ajout au panier d'un produit de la Boutique, sans style imposé : choix de la variante (si le
|
|
5
|
+
* produit en a plusieurs), quantité, bouton. Envoi sans JavaScript à `/__zef/shop/add`, puis
|
|
6
|
+
* le panier affiche le résultat (la fiche produit peut rester statique) ; un visiteur non
|
|
7
|
+
* connecté passe par la connexion. Classes `zef-cart-add__*`.
|
|
8
|
+
*
|
|
9
|
+
* Carte cadeau (lot 14e4b2) : montant proposé ou libre, destinataire (nom, email), message et
|
|
10
|
+
* date d'envoi facultative (`/__zef/shop/buy-gift-card`). Classes `zef-gift-card__*`.
|
|
11
|
+
*/
|
|
12
|
+
import type { ApiProduct } from "@agence-zef/sdk";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
product: ApiProduct | undefined;
|
|
16
|
+
/** Libellé du bouton. */
|
|
17
|
+
label?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { product, label = "Ajouter au panier", class: className } = Astro.props;
|
|
22
|
+
const variants = product?.variants ?? [];
|
|
23
|
+
const available = variants.filter((v) => v.available);
|
|
24
|
+
const variantLabel = (v: ApiProduct["variants"][number]) =>
|
|
25
|
+
product?.options
|
|
26
|
+
.map((o) => o.values.find((x) => x.id === v.options[o.id])?.label)
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.join(" · ") || "";
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
product?.type === "gift_card" && product.giftCard && (
|
|
33
|
+
<form class:list={["zef-cart-add", "zef-gift-card", className]} method="post" action="/__zef/shop/buy-gift-card">
|
|
34
|
+
<input type="hidden" name="zef_page" value={Astro.url.pathname} />
|
|
35
|
+
<input type="hidden" name="zef_locale" value={Astro.currentLocale ?? ""} />
|
|
36
|
+
<input type="hidden" name="productId" value={product.id} />
|
|
37
|
+
<fieldset class="zef-gift-card__amounts">
|
|
38
|
+
<legend>Montant</legend>
|
|
39
|
+
{product.giftCard.amounts.map((a, i) => (
|
|
40
|
+
<label>
|
|
41
|
+
<input type="radio" name="amount" value={a} checked={i === 0} />
|
|
42
|
+
{formatPrice(a)}
|
|
43
|
+
</label>
|
|
44
|
+
))}
|
|
45
|
+
{product.giftCard.custom && (
|
|
46
|
+
<label class="zef-gift-card__custom">
|
|
47
|
+
Autre montant ({formatPrice(product.giftCard.custom.min)} à {formatPrice(product.giftCard.custom.max)})
|
|
48
|
+
<input name="customAmount" inputmode="decimal" placeholder={String(product.giftCard.custom.min / 100)} />
|
|
49
|
+
</label>
|
|
50
|
+
)}
|
|
51
|
+
</fieldset>
|
|
52
|
+
<label>
|
|
53
|
+
Nom du destinataire
|
|
54
|
+
<input name="recipientName" required autocomplete="off" />
|
|
55
|
+
</label>
|
|
56
|
+
<label>
|
|
57
|
+
Email du destinataire
|
|
58
|
+
<input name="recipientEmail" type="email" required autocomplete="off" />
|
|
59
|
+
</label>
|
|
60
|
+
<label>
|
|
61
|
+
Message <span class="zef-gift-card__optional">(facultatif)</span>
|
|
62
|
+
<textarea name="message" rows="3" maxlength="500"></textarea>
|
|
63
|
+
</label>
|
|
64
|
+
<label>
|
|
65
|
+
Date d'envoi <span class="zef-gift-card__optional">(facultatif : sinon dès le paiement)</span>
|
|
66
|
+
<input name="sendOn" type="date" />
|
|
67
|
+
</label>
|
|
68
|
+
<button type="submit" class="zef-cart-add__submit" disabled={!product.available}>
|
|
69
|
+
{product.available ? label : "Indisponible"}
|
|
70
|
+
</button>
|
|
71
|
+
</form>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
{
|
|
75
|
+
product && product.type !== "gift_card" && (
|
|
76
|
+
<form class:list={["zef-cart-add", className]} method="post" action="/__zef/shop/add" data-available={String(product.available)}>
|
|
77
|
+
<input type="hidden" name="zef_page" value={Astro.url.pathname} />
|
|
78
|
+
<input type="hidden" name="zef_locale" value={Astro.currentLocale ?? ""} />
|
|
79
|
+
{variants.length > 1 ? (
|
|
80
|
+
<label class="zef-cart-add__variant">
|
|
81
|
+
<span>{product.options.map((o) => o.name).join(" · ") || "Choix"}</span>
|
|
82
|
+
<select name="variantId">
|
|
83
|
+
{variants.map((v) => (
|
|
84
|
+
<option value={v.id} disabled={!v.available} selected={v.id === available[0]?.id}>
|
|
85
|
+
{variantLabel(v)} — {formatPrice(v.price)}
|
|
86
|
+
{v.available ? (v.lowStock ? " · bientôt épuisé" : "") : " · épuisé"}
|
|
87
|
+
</option>
|
|
88
|
+
))}
|
|
89
|
+
</select>
|
|
90
|
+
</label>
|
|
91
|
+
) : (
|
|
92
|
+
variants[0] && <input type="hidden" name="variantId" value={variants[0].id} />
|
|
93
|
+
)}
|
|
94
|
+
{product.type === "access" || product.type === "subscription" ? (
|
|
95
|
+
<input type="hidden" name="quantity" value="1" />
|
|
96
|
+
) : (
|
|
97
|
+
<label class="zef-cart-add__quantity">
|
|
98
|
+
<span>Quantité</span>
|
|
99
|
+
<input type="number" name="quantity" min="1" max="99" value="1" />
|
|
100
|
+
</label>
|
|
101
|
+
)}
|
|
102
|
+
<button type="submit" class="zef-cart-add__submit" disabled={!product.available}>
|
|
103
|
+
{product.available ? (product.type === "subscription" && !Astro.props.label ? "S'abonner" : label) : "Épuisé"}
|
|
104
|
+
</button>
|
|
105
|
+
</form>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Panier du compte connecté, sans style imposé : une ligne par variante (nom, variante, prix,
|
|
4
|
+
* quantité modifiable, Retirer), code promo (`/__zef/shop/promo`, gardé dans un cookie ; remise
|
|
5
|
+
* ou raison du refus affichée), sous-total, total, lien vers le paiement. Formulaires sans
|
|
6
|
+
* JavaScript. Classes `zef-cart__*`. Page rendue à la demande.
|
|
7
|
+
*/
|
|
8
|
+
import { formatPrice, getCart, SHOP_ERRORS } from "@agence-zef/astro/shop";
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** Page du paiement (`shop.checkout` de l'intégration par défaut). */
|
|
12
|
+
checkoutHref?: string;
|
|
13
|
+
empty?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
checkoutHref = "/paiement",
|
|
19
|
+
empty = "Votre panier est vide.",
|
|
20
|
+
class: className,
|
|
21
|
+
} = Astro.props;
|
|
22
|
+
const cart = await getCart(Astro, Astro.currentLocale);
|
|
23
|
+
const errorCode = Astro.url.searchParams.get("zef_shop_error");
|
|
24
|
+
const added = Astro.url.searchParams.get("zef_shop_ok") === "add";
|
|
25
|
+
const blocked = cart?.lines.some((l) => !l.sellable || l.quantity > l.available);
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<div class:list={["zef-cart", className]}>
|
|
29
|
+
{added && <p class="zef-cart__success" role="status">Ajouté au panier.</p>}
|
|
30
|
+
{errorCode && <p class="zef-cart__error" role="alert">{SHOP_ERRORS[errorCode] ?? SHOP_ERRORS.unknown}</p>}
|
|
31
|
+
{
|
|
32
|
+
!cart || cart.lines.length === 0 ? (
|
|
33
|
+
<p class="zef-cart__empty">{empty}</p>
|
|
34
|
+
) : (
|
|
35
|
+
<>
|
|
36
|
+
<ul class="zef-cart__lines">
|
|
37
|
+
{cart.lines.map((l) => (
|
|
38
|
+
<li class="zef-cart__line" data-blocked={String(!l.sellable || l.quantity > l.available)}>
|
|
39
|
+
<span class="zef-cart__name">
|
|
40
|
+
{l.name}
|
|
41
|
+
{l.variantLabel && <span class="zef-cart__variant"> · {l.variantLabel}</span>}
|
|
42
|
+
</span>
|
|
43
|
+
<span class="zef-cart__price">{formatPrice(l.unitPrice)}</span>
|
|
44
|
+
{l.productType === "access" || l.productType === "subscription" ? (
|
|
45
|
+
<span class="zef-cart__single" />
|
|
46
|
+
) : l.giftCard ? (
|
|
47
|
+
<span class="zef-cart__gift-card-to">
|
|
48
|
+
{l.giftCard.recipientEmail}
|
|
49
|
+
{l.giftCard.sendAt && ` · envoi le ${new Date(l.giftCard.sendAt).toLocaleDateString("fr-FR", { day: "numeric", month: "long" })}`}
|
|
50
|
+
</span>
|
|
51
|
+
) : (
|
|
52
|
+
<form method="post" action="/__zef/shop/update" class="zef-cart__quantity">
|
|
53
|
+
<input type="hidden" name="zef_page" value={Astro.url.pathname} />
|
|
54
|
+
<input type="hidden" name="variantId" value={l.variantId} />
|
|
55
|
+
<input type="number" name="quantity" min="0" max="99" value={l.quantity} aria-label={`Quantité de ${l.name}`} />
|
|
56
|
+
<button type="submit">Mettre à jour</button>
|
|
57
|
+
</form>
|
|
58
|
+
)}
|
|
59
|
+
<span class="zef-cart__total">{formatPrice(l.total)}</span>
|
|
60
|
+
<form method="post" action={l.giftCardLineId ? "/__zef/shop/remove-gift-card" : "/__zef/shop/remove"}>
|
|
61
|
+
<input type="hidden" name="zef_page" value={Astro.url.pathname} />
|
|
62
|
+
<input type="hidden" name="variantId" value={l.variantId} />
|
|
63
|
+
<input type="hidden" name="lineId" value={l.giftCardLineId ?? ""} />
|
|
64
|
+
<button type="submit" class="zef-cart__remove">Retirer</button>
|
|
65
|
+
</form>
|
|
66
|
+
{!l.sellable ? (
|
|
67
|
+
<p class="zef-cart__warning">Plus disponible : retirez-le pour commander.</p>
|
|
68
|
+
) : l.quantity > l.available ? (
|
|
69
|
+
<p class="zef-cart__warning">Il n'en reste que {l.available}.</p>
|
|
70
|
+
) : null}
|
|
71
|
+
</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
<form method="post" action="/__zef/shop/promo" class="zef-cart__promo">
|
|
75
|
+
<input type="hidden" name="zef_page" value={Astro.url.pathname} />
|
|
76
|
+
{cart.promo ? (
|
|
77
|
+
<p class="zef-cart__promo-applied">
|
|
78
|
+
Code <strong>{cart.promo.code}</strong> : {cart.promo.freeShipping ? "livraison offerte" : cart.promo.label}
|
|
79
|
+
<button type="submit" name="remove" value="1" class="zef-cart__promo-remove">Retirer</button>
|
|
80
|
+
</p>
|
|
81
|
+
) : (
|
|
82
|
+
<>
|
|
83
|
+
<label>
|
|
84
|
+
Code promo
|
|
85
|
+
<input name="code" autocomplete="off" />
|
|
86
|
+
</label>
|
|
87
|
+
<button type="submit">Appliquer</button>
|
|
88
|
+
{cart.promoError && (
|
|
89
|
+
<>
|
|
90
|
+
<p class="zef-cart__warning" role="alert">{cart.promoError}</p>
|
|
91
|
+
<button type="submit" name="remove" value="1" class="zef-cart__promo-remove">Retirer le code</button>
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</>
|
|
95
|
+
)}
|
|
96
|
+
</form>
|
|
97
|
+
<form method="post" action="/__zef/shop/giftcard" class="zef-cart__gift-card">
|
|
98
|
+
<input type="hidden" name="zef_page" value={Astro.url.pathname} />
|
|
99
|
+
{cart.giftCard ? (
|
|
100
|
+
<p class="zef-cart__gift-card-applied">
|
|
101
|
+
Carte cadeau •••• {cart.giftCard.last4} : solde {formatPrice(cart.giftCard.available)}, déduit au paiement
|
|
102
|
+
<button type="submit" name="remove" value="1" class="zef-cart__gift-card-remove">Retirer</button>
|
|
103
|
+
</p>
|
|
104
|
+
) : (
|
|
105
|
+
<>
|
|
106
|
+
<label>
|
|
107
|
+
Carte cadeau
|
|
108
|
+
<input name="code" autocomplete="off" />
|
|
109
|
+
</label>
|
|
110
|
+
<button type="submit">Utiliser</button>
|
|
111
|
+
{cart.giftCardError && (
|
|
112
|
+
<>
|
|
113
|
+
<p class="zef-cart__warning" role="alert">{cart.giftCardError}</p>
|
|
114
|
+
<button type="submit" name="remove" value="1" class="zef-cart__gift-card-remove">Retirer la carte</button>
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
</>
|
|
118
|
+
)}
|
|
119
|
+
</form>
|
|
120
|
+
<p class="zef-cart__subtotal">
|
|
121
|
+
Sous-total <strong>{formatPrice(cart.subtotal)}</strong>
|
|
122
|
+
</p>
|
|
123
|
+
{cart.promo && cart.promo.discount > 0 && (
|
|
124
|
+
<p class="zef-cart__discount">
|
|
125
|
+
Code {cart.promo.code} <strong>−{formatPrice(cart.promo.discount)}</strong>
|
|
126
|
+
</p>
|
|
127
|
+
)}
|
|
128
|
+
{cart.total !== cart.subtotal && (
|
|
129
|
+
<p class="zef-cart__total-due">
|
|
130
|
+
Total <strong>{formatPrice(cart.total)}</strong> hors livraison
|
|
131
|
+
</p>
|
|
132
|
+
)}
|
|
133
|
+
{blocked ? (
|
|
134
|
+
<p class="zef-cart__warning">Corrigez le panier pour commander.</p>
|
|
135
|
+
) : (
|
|
136
|
+
<a class="zef-cart__checkout" href={checkoutHref}>Passer au paiement</a>
|
|
137
|
+
)}
|
|
138
|
+
</>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
</div>
|