#Ecommerce cart with content collection

1 messages · Page 1 of 1 (latest)

bold fjordBOT
#

Hi @buoyant vector. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

I'll help you understand how to implement an ecommerce cart system in Astro while using content collections.

Using Content Collections for Products

First, you'll want to set up a content collection for your products. Content collections in Astro provide type safety and better organization for your product data.

  1. Create a collection for your products in src/content/:
// src/content.config.ts
import { defineCollection, z } from 'astro:content';

const products = defineCollection({
  schema: z.object({
    title: z.string(),
    price: z.number(),
    description: z.string(),
    image: z.string(),
    // Add other product fields as needed
  })
});

export const collections = {
  'products': products
};
  1. Add your product files in src/content/products/ as Markdown or MDX files with the appropriate frontmatter.

Implementing the Cart Functionality

For the cart functionality, you can use nanostores to share state between islands. This is perfect for an ecommerce cart that needs to be accessible from multiple components.

Here's how to implement it:

  1. First, set up your cart store:
// src/cartStore.js
import { atom, map } from 'nanostores';

export const isCartOpen = atom(false);

/** 
 * @typedef {Object} CartItem
 * @property {string} id
 * @property {string} name
 * @property {string} imageSrc
 * @property {number} price
 * @property {number} quantity
 */

/** @type {import('nanostores').MapStore<Record<string, CartItem>>} */
export const cartItems = map({});

export function addCartItem({ id, name, imageSrc, price }) {
  const existingEntry = cartItems.get()[id];
  if (existingEntry) {
    cartItems.setKey(id, {
      ...existingEntry,
      quantity: existingEntry.quantity + 1,
    })
  } else {
    cartItems.setKey(
      id,
      { id, name, imageSrc, price, quantity: 1 }
    );
  }
}
#
  1. Create an "Add to Cart" component:
// src/components/AddToCartForm.jsx
import { addCartItem, isCartOpen } from '../cartStore';

export default function AddToCartForm({ product, children }) {
  function addToCart(e) {
    e.preventDefault();
    isCartOpen.set(true);
    addCartItem({
      id: product.id,
      name: product.data.title,
      imageSrc: product.data.image,
      price: product.data.price
    });
  }

  return (
    <form onSubmit={addToCart}>
      {children}
    </form>
  );
}
  1. Create a cart flyout component:
// src/components/CartFlyout.jsx
import { useStore } from '@nanostores/react';
import { isCartOpen, cartItems } from '../cartStore';

export default function CartFlyout() {
  const $isCartOpen = useStore(isCartOpen);
  const $cartItems = useStore(cartItems);
  
  const cartTotal = Object.values($cartItems).reduce(
    (total, item) => total + (item.price * item.quantity), 0
  );

  return $isCartOpen ? (
    <aside>
      {Object.values($cartItems).length ? (
        <>
          <ul>
            {Object.values($cartItems).map(cartItem => (
              <li key={cartItem.id}>
                <img src={cartItem.imageSrc} alt={cartItem.name} />
                <h3>{cartItem.name}</h3>
                <p>Quantity: {cartItem.quantity}</p>
                <p>Price: ${cartItem.price}</p>
              </li>
            ))}
          </ul>
          <p>Total: ${cartTotal.toFixed(2)}</p>
        </>
      ) : <p>Your cart is empty!</p>}
    </aside>
  ) : null;
}
  1. Implement a toggle for the cart:
// src/components/CartFlyoutToggle.jsx
import { useStore } from '@nanostores/react';
import { isCartOpen, cartItems } from '../cartStore';
#

export default function CartFlyoutToggle() {
const $isCartOpen = useStore(isCartOpen);
const $cartItems = useStore(cartItems);

const itemsCount = Object.values($cartItems).reduce(
(count, item) => count + item.quantity, 0
);

return (
<button onClick={() => isCartOpen.set(!$isCartOpen)}>
Cart ({itemsCount})
</button>
);
}


5. Use these components in your Astro page:

```astro
---
import { getCollection } from 'astro:content';
import CartFlyoutToggle from '../components/CartFlyoutToggle';
import CartFlyout from '../components/CartFlyout';
import AddToCartForm from '../components/AddToCartForm';

const products = await getCollection('products');
---

<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>My Astro Shop</title>
</head>
<body>
  <header>
    <nav>
      <a href="/">Astro Shop</a>
      <CartFlyoutToggle client:load />
    </nav>
  </header>
  
  <main>
    <h1>Products</h1>
    <div class="products">
      {products.map(product => (
        <div class="product">
          <img src={product.data.image} alt={product.data.title} />
          <h2>{product.data.title}</h2>
          <p>${product.data.price}</p>
          <AddToCartForm product={product} client:load>
            <button type="submit">Add to Cart</button>
          </AddToCartForm>
        </div>
      ))}
    </div>
  </main>
  
  <CartFlyout client:load />
</body>
</html>

This implementation combines Astro's content collections for managing product data with nanostores for handling the cart state across different UI components. The example is based on the approach shown in the sharing state between islands recipe.

#

@buoyant vector Kapa.ai is still learning and improving, please let me know how I did by reacting below.

bold fjordBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Je vais vous montrer comment afficher simplement les messages d'erreur d'authentification dans votre page Astro avec Supabase.

Pour afficher le message d'erreur dans votre page de connexion, vous pouvez modifier votre endpoint d'API et votre page de connexion comme suit:

  1. Modifiez votre endpoint d'API pour rediriger avec le message d'erreur:
// src/pages/api/auth/signin.ts
import type { APIRoute } from "astro";
import { supabase } from "../../../lib/supabase";

export const POST: APIRoute = async ({ request, cookies, redirect }) => {
  const formData = await request.formData();
  const email = formData.get("email")?.toString();
  const password = formData.get("password")?.toString();

  if (!email || !password) {
    return redirect("/signin?error=" + encodeURIComponent("Email and password are required"));
  }

  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });

  if (error) {
    return redirect("/signin?error=" + encodeURIComponent(error.message));
  }

  const { access_token, refresh_token } = data.session;
  cookies.set("sb-access-token", access_token, {
    path: "/",
  });
  cookies.set("sb-refresh-token", refresh_token, {
    path: "/",
  });
  return redirect("/");
};
  1. Ensuite, dans votre page de connexion, récupérez et affichez le message d'erreur:
---
import Layout from "../layouts/Layout.astro";

const { cookies, redirect } = Astro;
const accessToken = cookies.get("sb-access-token");
const refreshToken = cookies.get("sb-refresh-token");

if (accessToken && refreshToken) {
  return redirect("/");
}

// Récupérer le message d'erreur depuis l'URL
const errorMessage = Astro.url.searchParams.get("error");
---
#

<Layout title="Sign in">
<h1>Sign in</h1>
<p>New here? <a href="/register">Create an account</a></p>

{errorMessage && <p class="error">{errorMessage}</p>}

<form action="/api/auth/signin" method="post">
<label for="email">Email</label>
<input type="email" name="email" id="email" />
<label for="password">Password</label>
<input type="password" name="password" id="password" />
<button type="submit">Login</button>
</form>
</Layout>

<style>
.error {
color: red;
background-color: #ffeeee;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
}
</style>


Cette approche est simple et efficace pour afficher les messages d'erreur d'authentification:

1. Lorsqu'une erreur se produit, nous redirigeons vers la page de connexion avec le message d'erreur dans l'URL
2. La page de connexion récupère ce message et l'affiche si présent
3. Le style CSS rend le message d'erreur visible et distinct

Cette méthode est basée sur les exemples d'authentification Supabase dans la documentation Astro [comme indiqué ici](<https://docs.astro.build/en/guides/backend/supabase/#creating-auth-pages>).

Pour une gestion d'erreur plus avancée, vous pourriez également utiliser les [formulaires avec gestion d'erreurs](<https://docs.astro.build/en/recipes/build-forms/#recipe>) ou les [actions Astro](<https://docs.astro.build/en/guides/actions/#handle-form-action-errors>) qui offrent des fonctionnalités supplémentaires.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

@buoyant vector Kapa.ai is still learning and improving, please let me know how I did by reacting below.

buoyant vector
#

@bold fjord Je veux faire un content collection pour les produits

bold fjordBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Je vais vous montrer comment créer une collection de contenu pour les produits dans Astro.

Création d'une collection pour les produits

Voici comment configurer une collection de contenu pour vos produits :

  1. Créez un fichier de configuration src/content.config.ts à la racine de votre projet :
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob, file } from 'astro/loaders';

// Définition de la collection de produits
const products = defineCollection({
  // Utilisation du loader glob pour charger des fichiers individuels
  loader: glob({ pattern: '**/*.md', base: './src/data/products' }),
  // Définition du schéma pour valider la structure des produits
  schema: z.object({
    title: z.string(),
    price: z.number(),
    description: z.string(),
    image: z.string(),
    inStock: z.boolean().optional().default(true),
    categories: z.array(z.string()).optional(),
    features: z.array(z.string()).optional(),
    // Ajoutez d'autres champs selon vos besoins
  }),
});

// Exportation des collections
export const collections = { products };
  1. Créez un dossier pour stocker vos produits, par exemple src/data/products/

  2. Ajoutez vos produits sous forme de fichiers Markdown dans ce dossier :

---
title: "T-shirt Premium"
price: 29.99
description: "T-shirt en coton bio de haute qualité"
image: "/images/tshirt-premium.jpg"
categories: ["vêtements", "t-shirts"]
features: ["100% coton bio", "Disponible en plusieurs tailles", "Lavable en machine"]
---

Ce t-shirt premium est fabriqué à partir de coton bio de la plus haute qualité, offrant confort et durabilité.

Utilisation de la collection de produits

Pour utiliser votre collection de produits dans une page :

---
import { getCollection } from 'astro:content';
#

// Récupérer tous les produits
const allProducts = await getCollection('products');

// Filtrer les produits si nécessaire
const inStockProducts = allProducts.filter(product => product.data.inStock);

<h1>Nos Produits</h1>

<div class="products-grid">
{allProducts.map(product => (
<div class="product-card">
<img src={product.data.image} alt={product.data.title} />
<h2>{product.data.title}</h2>
<p class="price">{product.data.price.toFixed(2)} €</p>
<p>{product.data.description}</p>
<a href={/products/${product.slug}}>Voir détails</a>
</div>
))}
</div>


## Affichage d'un produit individuel

Pour créer une page de détail de produit :

```astro
---
import { getCollection, getEntry } from 'astro:content';

// Générer les chemins pour chaque produit
export async function getStaticPaths() {
  const products = await getCollection('products');
  return products.map(product => ({
    params: { slug: product.slug },
    props: { product },
  }));
}

// Récupérer les données du produit
const { product } = Astro.props;
const { Content } = await product.render();
---

<div class="product-detail">
  <img src={product.data.image} alt={product.data.title} />
  <h1>{product.data.title}</h1>
  <p class="price">{product.data.price.toFixed(2)} €</p>
  
  {product.data.features && (
    <div class="features">
      <h3>Caractéristiques</h3>
      <ul>
        {product.data.features.map(feature => (
          <li>{feature}</li>
        ))}
      </ul>
    </div>
  )}
  
  <div class="description">
    <Content />
  </div>
  
  <button>Ajouter au panier</button>
</div>
#

Les collections de contenu dans Astro sont parfaites pour gérer des produits car elles offrent validation des données, type-safety et des API intuitives pour interroger votre contenu. Vous pouvez également utiliser le loader file() si vous préférez stocker tous vos produits dans un seul fichier JSON ou YAML.

Cette approche vous permet de bénéficier de la validation des données et de la sécurité des types pour votre catalogue de produits, tout en gardant une structure organisée et maintenable.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@buoyant vector Kapa.ai is still learning and improving, please let me know how I did by reacting below.

buoyant vector
#

@bold fjord Comment je peux faire pour que les achats des utilisateurs soit sauvegarder dans une base de données avec les articles achetés

bold fjordBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Je vais vous expliquer comment sauvegarder les achats des utilisateurs dans une base de données avec Astro.

Utilisation d'Astro DB pour stocker les achats

Astro DB est une solution idéale pour stocker les achats des utilisateurs. Voici comment procéder:

1. Installation d'Astro DB

Commencez par installer l'intégration @astrojs/db:

npx astro add db

2. Configuration de la base de données

Créez un schéma pour stocker les achats dans db/config.ts:

import { defineDb, defineTable, column } from 'astro:db';

// Table pour les utilisateurs
const User = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    email: column.text(),
    name: column.text(),
  }
});

// Table pour les produits
const Product = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    name: column.text(),
    price: column.number(),
    description: column.text(),
  }
});

// Table pour les commandes
const Order = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    userId: column.number({ references: () => User.columns.id }),
    date: column.date(),
    status: column.text(),
    total: column.number(),
  }
});

// Table pour les articles de commande
const OrderItem = defineTable({
  columns: {
    id: column.number({ primaryKey: true }),
    orderId: column.number({ references: () => Order.columns.id }),
    productId: column.number({ references: () => Product.columns.id }),
    quantity: column.number(),
    price: column.number(),
  }
});

export default defineDb({
  tables: { User, Product, Order, OrderItem },
});

3. Création d'un endpoint pour enregistrer les achats

#

Pour enregistrer les achats, vous devez configurer votre projet pour le rendu à la demande et ajouter un adaptateur pour votre environnement de déploiement.

Créez un endpoint API pour traiter les commandes:

// src/pages/api/checkout.ts
import { db, Order, OrderItem } from 'astro:db';
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request }) => {
  try {
    const data = await request.json();
    const { userId, cartItems, total } = data;
    
    // Insérer la commande principale
    const [newOrder] = await db.insert(Order).values({
      userId,
      date: new Date(),
      status: 'pending',
      total
    }).returning();
    
    // Insérer les articles de la commande
    for (const item of cartItems) {
      await db.insert(OrderItem).values({
        orderId: newOrder.id,
        productId: item.id,
        quantity: item.quantity,
        price: item.price
      });
    }
    
    return new Response(JSON.stringify({ 
      success: true, 
      orderId: newOrder.id 
    }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' }
    });
  } catch (error) {
    return new Response(JSON.stringify({ 
      success: false, 
      message: error.message 
    }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

4. Utilisation des sessions pour le panier

Pour maintenir l'état du panier entre les requêtes, vous pouvez utiliser l'API de sessions d'Astro:

// src/components/CartButton.astro
---
export const prerender = false;
const cart = await Astro.session?.get('cart') || [];
---

<a href="/checkout">🛒 {cart.length} articles</a>

5. Ajout d'un produit au panier

// src/pages/api/cart/add.ts
import type { APIRoute } from 'astro';
#

export const POST: APIRoute = async ({ request, redirect, session }) => {
const data = await request.json();
const { productId, quantity, price, name } = data;

// Récupérer le panier actuel
const cart = await session.get('cart') || [];

// Vérifier si le produit est déjà dans le panier
const existingItemIndex = cart.findIndex(item => item.productId === productId);

if (existingItemIndex >= 0) {
// Mettre à jour la quantité
cart[existingItemIndex].quantity += quantity;
} else {
// Ajouter un nouvel article
cart.push({ productId, quantity, price, name });
}

// Sauvegarder le panier mis à jour dans la session
await session.set('cart', cart);

return redirect('/cart');
};


### 6. Finalisation de l'achat

Créez une page de paiement qui envoie les données du panier à votre API:

```astro
---
// src/pages/checkout.astro
export const prerender = false;

// Récupérer le panier depuis la session
const cart = await Astro.session?.get('cart') || [];
const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);

// Récupérer l'utilisateur connecté (si vous avez l'authentification)
const userId = Astro.cookies.get('userId')?.value;
---

<h1>Finaliser votre commande</h1>
#

{cart.length === 0 ? (
<p>Votre panier est vide</p>
) : (
<>
<div class="cart-items">
{cart.map(item => (
<div class="cart-item">
<h3>{item.name}</h3>
<p>Quantité: {item.quantity}</p>
<p>Prix: {item.price}€</p>
</div>
))}
</div>

<div class="total">
  <h2>Total: {total}€</h2>
</div>

<button id="checkout-btn">Payer maintenant</button>

<script>
  // Script côté client pour envoyer les données à l'API
  document.getElementById('checkout-btn').addEventListener('click', async () => {
    try {
      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          userId: '{userId}',
          cartItems: {JSON.stringify(cart)},
          total: {total}
        })
      });
      
      const result = await response.json();
      
      if (result.success) {
        // Rediriger vers la page de confirmation
        window.location.href = `/order-confirmation?id=${result.orderId}`;
      } else {
        alert('Erreur lors du traitement de la commande');
      }
    } catch (error) {
      console.error('Erreur:', error);
    }
  });
</script>

</>
)}


## Connexion à une base de données de production

Pour un environnement de production, vous pouvez connecter Astro DB à une base de données libSQL comme Turso:

1. Créez une base de données Turso:

```bash
turso db create ma-boutique
  1. Obtenez les informations de connexion:
turso db show ma-boutique
turso db tokens create ma-boutique
  1. Configurez les variables d'environnement:
#
ASTRO_DB_REMOTE_URL=libsql://ma-boutique-xxx.turso.io
ASTRO_DB_APP_TOKEN=eyJhbGciOiJF...
  1. Poussez votre schéma vers la base de données distante:
astro db push --remote

Cette approche vous permet de stocker les achats des utilisateurs dans une base de données tout en bénéficiant de la sécurité des types et de la validation des données d'Astro DB.

Pour une solution e-commerce plus complète, vous pourriez également explorer des solutions e-commerce spécialisées comme Snipcart qui s'intègrent bien avec Astro.

J'espère que cela vous aide à mettre en place votre système de gestion des achats!

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: