diff options
Diffstat (limited to 'storefront/pages')
| -rw-r--r-- | storefront/pages/_app.js | 6 | ||||
| -rw-r--r-- | storefront/pages/cart.js | 101 | ||||
| -rw-r--r-- | storefront/pages/checkout.js | 192 | ||||
| -rw-r--r-- | storefront/pages/index.js | 53 | ||||
| -rw-r--r-- | storefront/pages/login.js | 82 | ||||
| -rw-r--r-- | storefront/pages/order-confirmation.js | 17 | ||||
| -rw-r--r-- | storefront/pages/register.js | 100 |
7 files changed, 548 insertions, 3 deletions
diff --git a/storefront/pages/_app.js b/storefront/pages/_app.js index 3737ae7..1513a72 100644 --- a/storefront/pages/_app.js +++ b/storefront/pages/_app.js @@ -1,6 +1,6 @@ import { MedusaProvider } from "medusa-react"
import { QueryClient } from "@tanstack/react-query"
-import { medusaClient } from "../lib/medusa-client"
+import Layout from "../components/Layout"
const queryClient = new QueryClient()
@@ -12,7 +12,9 @@ export default function App({ Component, pageProps }) { process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL || "http://localhost:9000"
}
>
- <Component {...pageProps} />
+ <Layout>
+ <Component {...pageProps} />
+ </Layout>
</MedusaProvider>
)
}
diff --git a/storefront/pages/cart.js b/storefront/pages/cart.js new file mode 100644 index 0000000..fa625a9 --- /dev/null +++ b/storefront/pages/cart.js @@ -0,0 +1,101 @@ +import { useCallback, useEffect, useState } from "react" +import { medusaClient } from "../lib/medusa-client" +import { formatAmount } from "../lib/format" +import { getStoredCartId, clearStoredCartId } from "../lib/storefront" + +export default function CartPage() { + const [cart, setCart] = useState(null) + const [status, setStatus] = useState("") + const [isLoading, setIsLoading] = useState(true) + + const loadCart = useCallback(async () => { + const storedCartId = getStoredCartId() + if (!storedCartId) { + setCart(null) + setIsLoading(false) + return + } + + try { + const { cart: fetchedCart } = await medusaClient.carts.retrieve(storedCartId) + setCart(fetchedCart) + } catch (error) { + clearStoredCartId() + setCart(null) + } finally { + setIsLoading(false) + } + }, []) + + useEffect(() => { + loadCart() + }, [loadCart]) + + const handleRemove = async (lineItemId) => { + if (!cart) { + return + } + setStatus("") + try { + await medusaClient.carts.lineItems.delete(cart.id, lineItemId) + await loadCart() + } catch (error) { + setStatus("Impossible de retirer l'article.") + } + } + + if (isLoading) { + return <p>Chargement du panier...</p> + } + + if (!cart || !cart.items?.length) { + return <p>Votre panier est vide.</p> + } + + return ( + <div style={{ maxWidth: "720px", margin: "0 auto" }}> + <h1>Panier</h1> + {status && <p>{status}</p>} + <div style={{ display: "grid", gap: "1rem", marginTop: "1rem" }}> + {cart.items.map((item) => ( + <div + key={item.id} + style={{ + border: "1px solid #ccc", + borderRadius: "8px", + padding: "1rem", + display: "flex", + justifyContent: "space-between", + gap: "1rem", + }} + > + <div> + <strong>{item.title}</strong> + <p>Quantité : {item.quantity}</p> + <p> + {formatAmount(item.unit_price, cart.region?.currency_code || "eur")} + </p> + </div> + <button + type="button" + onClick={() => handleRemove(item.id)} + style={{ + border: "1px solid #ccc", + background: "#fff", + borderRadius: "6px", + padding: "0.4rem 0.8rem", + cursor: "pointer", + height: "fit-content", + }} + > + Retirer + </button> + </div> + ))} + </div> + <p style={{ marginTop: "1.5rem", fontWeight: 600 }}> + Total : {formatAmount(cart.total, cart.region?.currency_code || "eur")} + </p> + </div> + ) +} diff --git a/storefront/pages/checkout.js b/storefront/pages/checkout.js new file mode 100644 index 0000000..1970df4 --- /dev/null +++ b/storefront/pages/checkout.js @@ -0,0 +1,192 @@ +import { useEffect, useState } from "react" +import { useRouter } from "next/router" +import { medusaClient } from "../lib/medusa-client" +import { getStoredCartId, clearStoredCartId } from "../lib/storefront" + +const initialForm = { + email: "", + first_name: "", + last_name: "", + address_1: "", + postal_code: "", + city: "", + country_code: "fr", +} + +export default function CheckoutPage() { + const router = useRouter() + const [form, setForm] = useState(initialForm) + const [status, setStatus] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [cartId, setCartId] = useState(null) + + useEffect(() => { + const storedCartId = getStoredCartId() + setCartId(storedCartId) + }, []) + + const handleChange = (event) => { + const { name, value } = event.target + setForm((prev) => ({ ...prev, [name]: value })) + } + + const handleSubmit = async (event) => { + event.preventDefault() + setStatus("") + setIsLoading(true) + + if (!cartId) { + setStatus("Votre panier est vide.") + setIsLoading(false) + return + } + + try { + await medusaClient.carts.update(cartId, { + email: form.email, + shipping_address: { + first_name: form.first_name, + last_name: form.last_name, + address_1: form.address_1, + postal_code: form.postal_code, + city: form.city, + country_code: form.country_code, + }, + }) + + const { shipping_options: shippingOptions } = + await medusaClient.shippingOptions.listCartOptions(cartId) + + if (!shippingOptions?.length) { + throw new Error("Aucune option de livraison disponible.") + } + + await medusaClient.carts.addShippingMethod(cartId, { + option_id: shippingOptions[0].id, + }) + + const { cart: cartWithPayments } = await medusaClient.carts.createPaymentSessions( + cartId + ) + + const manualSession = cartWithPayments?.payment_sessions?.find( + (session) => session.provider_id === "manual" + ) + const providerId = + manualSession?.provider_id || + cartWithPayments?.payment_sessions?.[0]?.provider_id + + if (!providerId) { + throw new Error("Aucun moyen de paiement disponible.") + } + + await medusaClient.carts.setPaymentSession(cartId, { provider_id: providerId }) + + const { type, data } = await medusaClient.carts.complete(cartId) + if (type === "order" && data?.id) { + clearStoredCartId() + router.push(`/order-confirmation?order_id=${data.id}`) + return + } + + setStatus("Commande validée, mais sans numéro de commande.") + } catch (error) { + setStatus("Impossible de finaliser la commande.") + } finally { + setIsLoading(false) + } + } + + return ( + <div style={{ maxWidth: "520px", margin: "0 auto" }}> + <h1>Finaliser la commande</h1> + <form onSubmit={handleSubmit} style={{ display: "grid", gap: "1rem" }}> + <label> + Email + <input + name="email" + type="email" + value={form.email} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Prénom + <input + name="first_name" + value={form.first_name} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Nom + <input + name="last_name" + value={form.last_name} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Adresse + <input + name="address_1" + value={form.address_1} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Code postal + <input + name="postal_code" + value={form.postal_code} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Ville + <input + name="city" + value={form.city} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Pays + <input + name="country_code" + value={form.country_code} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <button + type="submit" + disabled={isLoading} + style={{ + border: "1px solid #ccc", + background: "#fff", + borderRadius: "6px", + padding: "0.6rem", + cursor: "pointer", + }} + > + {isLoading ? "Validation..." : "Passer la commande"} + </button> + {status && <p>{status}</p>} + </form> + </div> + ) +} diff --git a/storefront/pages/index.js b/storefront/pages/index.js index fa4a592..dda2a6d 100644 --- a/storefront/pages/index.js +++ b/storefront/pages/index.js @@ -1,18 +1,69 @@ +import { useState } from "react"
import { useProducts } from "medusa-react"
+import { medusaClient } from "../lib/medusa-client"
+import { ensureCart } from "../lib/storefront"
+import { formatAmount } from "../lib/format"
export default function Home() {
const { products, isLoading } = useProducts()
+ const [status, setStatus] = useState("")
+ const [addingId, setAddingId] = useState(null)
+
+ const handleAddToCart = async (product) => {
+ const variantId = product?.variants?.[0]?.id
+ if (!variantId) {
+ setStatus("Aucune variante disponible pour ce produit.")
+ return
+ }
+
+ setAddingId(product.id)
+ setStatus("")
+
+ try {
+ const cart = await ensureCart(medusaClient)
+ await medusaClient.carts.lineItems.create(cart.id, {
+ variant_id: variantId,
+ quantity: 1,
+ })
+ setStatus(`${product.title} a été ajouté au panier.`)
+ } catch (error) {
+ setStatus("Impossible d'ajouter au panier pour le moment.")
+ } finally {
+ setAddingId(null)
+ }
+ }
return (
- <div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
+ <div>
<h1>Bienvenue sur la boutique Lucien-sens-bon</h1>
{isLoading && <span>Chargement des produits...</span>}
+ {status && <p style={{ marginTop: "1rem" }}>{status}</p>}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: "1rem" }}>
{products && products.map((product) => (
<div key={product.id} style={{ border: "1px solid #ccc", padding: "1rem", borderRadius: "8px" }}>
<h3>{product.title}</h3>
<p>{product.description}</p>
+ <p style={{ fontWeight: 600 }}>
+ {formatAmount(
+ product?.variants?.[0]?.prices?.[0]?.amount,
+ product?.variants?.[0]?.prices?.[0]?.currency_code || "eur"
+ )}
+ </p>
+ <button
+ type="button"
+ onClick={() => handleAddToCart(product)}
+ disabled={addingId === product.id}
+ style={{
+ border: "1px solid #ccc",
+ background: "#fff",
+ borderRadius: "6px",
+ padding: "0.4rem 0.8rem",
+ cursor: "pointer",
+ }}
+ >
+ {addingId === product.id ? "Ajout..." : "Ajouter au panier"}
+ </button>
</div>
))}
</div>
diff --git a/storefront/pages/login.js b/storefront/pages/login.js new file mode 100644 index 0000000..033f9b5 --- /dev/null +++ b/storefront/pages/login.js @@ -0,0 +1,82 @@ +import { useState } from "react" +import { useRouter } from "next/router" +import { medusaClient } from "../lib/medusa-client" +import { setStoredToken } from "../lib/storefront" + +export default function LoginPage() { + const router = useRouter() + const [form, setForm] = useState({ email: "", password: "" }) + const [status, setStatus] = useState("") + const [isLoading, setIsLoading] = useState(false) + + const handleChange = (event) => { + const { name, value } = event.target + setForm((prev) => ({ ...prev, [name]: value })) + } + + const handleSubmit = async (event) => { + event.preventDefault() + setStatus("") + setIsLoading(true) + + try { + const { access_token: accessToken } = await medusaClient.auth.getToken({ + email: form.email, + password: form.password, + }) + + setStoredToken(accessToken) + medusaClient.setToken(accessToken) + setStatus("Connexion réussie.") + router.push("/") + } catch (error) { + setStatus("Identifiants invalides ou indisponibles.") + } finally { + setIsLoading(false) + } + } + + return ( + <div style={{ maxWidth: "420px", margin: "0 auto" }}> + <h1>Se connecter</h1> + <form onSubmit={handleSubmit} style={{ display: "grid", gap: "1rem" }}> + <label> + Email + <input + name="email" + type="email" + value={form.email} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Mot de passe + <input + name="password" + type="password" + value={form.password} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <button + type="submit" + disabled={isLoading} + style={{ + border: "1px solid #ccc", + background: "#fff", + borderRadius: "6px", + padding: "0.6rem", + cursor: "pointer", + }} + > + {isLoading ? "Connexion..." : "Se connecter"} + </button> + {status && <p>{status}</p>} + </form> + </div> + ) +} diff --git a/storefront/pages/order-confirmation.js b/storefront/pages/order-confirmation.js new file mode 100644 index 0000000..4c7930f --- /dev/null +++ b/storefront/pages/order-confirmation.js @@ -0,0 +1,17 @@ +import { useRouter } from "next/router" + +export default function OrderConfirmationPage() { + const router = useRouter() + const { order_id: orderId } = router.query + + return ( + <div style={{ maxWidth: "520px", margin: "0 auto" }}> + <h1>Merci pour votre commande</h1> + {orderId ? ( + <p>Votre commande a bien été enregistrée : {orderId}</p> + ) : ( + <p>Votre commande a bien été enregistrée.</p> + )} + </div> + ) +} diff --git a/storefront/pages/register.js b/storefront/pages/register.js new file mode 100644 index 0000000..48831ea --- /dev/null +++ b/storefront/pages/register.js @@ -0,0 +1,100 @@ +import { useState } from "react" +import { useRouter } from "next/router" +import { medusaClient } from "../lib/medusa-client" + +export default function RegisterPage() { + const router = useRouter() + const [form, setForm] = useState({ + first_name: "", + last_name: "", + email: "", + password: "", + }) + const [status, setStatus] = useState("") + const [isLoading, setIsLoading] = useState(false) + + const handleChange = (event) => { + const { name, value } = event.target + setForm((prev) => ({ ...prev, [name]: value })) + } + + const handleSubmit = async (event) => { + event.preventDefault() + setStatus("") + setIsLoading(true) + + try { + await medusaClient.customers.create(form) + setStatus("Compte créé. Vous pouvez vous connecter.") + router.push("/login") + } catch (error) { + setStatus("Impossible de créer le compte pour le moment.") + } finally { + setIsLoading(false) + } + } + + return ( + <div style={{ maxWidth: "420px", margin: "0 auto" }}> + <h1>Créer un compte</h1> + <form onSubmit={handleSubmit} style={{ display: "grid", gap: "1rem" }}> + <label> + Prénom + <input + name="first_name" + value={form.first_name} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Nom + <input + name="last_name" + value={form.last_name} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Email + <input + name="email" + type="email" + value={form.email} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <label> + Mot de passe + <input + name="password" + type="password" + value={form.password} + onChange={handleChange} + required + style={{ width: "100%", padding: "0.5rem", marginTop: "0.5rem" }} + /> + </label> + <button + type="submit" + disabled={isLoading} + style={{ + border: "1px solid #ccc", + background: "#fff", + borderRadius: "6px", + padding: "0.6rem", + cursor: "pointer", + }} + > + {isLoading ? "Création..." : "Créer mon compte"} + </button> + {status && <p>{status}</p>} + </form> + </div> + ) +} |
