summaryrefslogtreecommitdiff
path: root/storefront/pages
diff options
context:
space:
mode:
Diffstat (limited to 'storefront/pages')
-rw-r--r--storefront/pages/_app.js6
-rw-r--r--storefront/pages/cart.js101
-rw-r--r--storefront/pages/checkout.js192
-rw-r--r--storefront/pages/index.js53
-rw-r--r--storefront/pages/login.js82
-rw-r--r--storefront/pages/order-confirmation.js17
-rw-r--r--storefront/pages/register.js100
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>
+ )
+}