Roadmap
Avançado

Ecommerce em 2024

O jeito mais moderno de criar um ecommerce

tailwind
vercel
github
medusajs
render
nuxt

Toda loja tem uma página para você ver todos os detalhes do produto, bem como as variantes (cor, tamanho, etc) para você selecionar e adicionar ao carrinho.

O intuito dessa aula é fazer uma página de produto simples e que você possa evoluir com mais informações no futuro, como o número de itens no estoque, descrições mais detalhadas, campos customizados e muito mais.

Criando o componente de variante de produto

Vá na pasta components, crie um arquivo chamado ProductVariant.vue e cole o seguinte código:

ProductVariant.vue
<script setup lang="ts">
defineProps<{
  variant: string;
  active?: boolean;
}>()
</script>

<template>
  <button class="rounded bg-white border border-zinc-300 p-3 text-sm flex items-center justify-center hover:bg-zinc-100 transition"
  :class="{'!bg-zinc-900 !text-white !border-zinc-900': active}">
    {{ variant }}
  </button>
</template>
Clique aqui para expandir

Esse componente é o seletor de variante, ou seja, ele tem uma propriedade chamada active que vai atualizar o estilo do componente toda vez que alguém clicar numa variante na página de produto.

Criando a página de produto

Agora vamos criar a página de produto em si, para isso vamos abrir a pasta /pages, criar uma pasta chamada /produtos e dentro dessa pasta vamos criar um arquivo [...handle].vue. Lembra que eu expliquei na aula da homepage que o handle era a url do produto? Então, toda vez que abrirmos a página de produto, vai carregar o produto de acordo com o handle da url acessada.

Dentro do arquivo [...handle].vue cole o seguinte código:

[...handle
<script setup lang="ts">
const { params, query } = useRoute();
const router = useRouter();
const client = useMedusaClient();
const { products } = await client.products.list({handle: params.handle[0] as string});

const selectedVariantId = ref(query.variant);
const loading = ref(false);

const product = computed(() => {
  return products[0];
})

const firstImage = computed(() => {
  if(!product.value.images) return null;

  return product.value.images[0];
})

const otherImages = computed(() => {
  if(!product.value.images) return [];
  if(product.value.images.length <= 1) return [];

  product.value.images.shift();

  return product.value.images;
})

const currentPrice = computed(() => {
  if(!selectedVariantId.value) return 0;

  const currentVariant = product.value.variants.find(e => e.id === selectedVariantId.value);
  if(!currentVariant) return 0;

  return currentVariant.prices[0].amount / 100;
})

async function addToCart() {
  try {
    loading.value = true;

    let cartId = localStorage.getItem("cart_id");

    if(!cartId) {
      const response = await client.carts.create();
      cartId = response.cart.id;
      localStorage.setItem("cart_id", response.cart.id);
    }

    await client.carts.lineItems.create(cartId, {
      variant_id: selectedVariantId.value as string,
      quantity: 1
    })

    router.push('/carrinho');
    
  } finally {
    loading.value = false;
  }
}
</script>

<template>
  <section class="container grid gap-10 grid-cols-2 pt-24">
    <div>
      <div class="aspect-square w-full h-auto overflow-hidden rounded-lg mb-4">
        <img v-if="firstImage" :src="firstImage.url" :alt="product.title" />
      </div>
      <div class="grid grid-cols-[repeat(auto-fill,_minmax(200px,_1fr))] gap-4">
        <div class="aspect-square rounded-lg overflow-hidden" v-for="image in otherImages" :key="image.id">
          <img :src="image.url" :alt="product.title" class="w-full h-full object-cover object-center">
        </div>
      </div>
    </div>
    <div>
      <h1 class="text-2xl font-extrabold mb-2">
        {{ product.title }}
      </h1>
      <div class="font-light text-3xl mb-4">
        {{ new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(currentPrice) }}
      </div>
      <p class="text-base text-zinc-500 mb-8">
        {{ product.description }}
      </p>
      <div class="mb-12" v-if="product.variants">
        <div class="text-sm mb-2">Variantes</div>
        <div class="grid grid-cols-[repeat(auto-fill,_minmax(90px,_1fr))] gap-3">
          <ProductVariant 
            v-for="variant in product.variants" 
            :key="variant.id" 
            :variant="(variant.title as string)" 
            :active="selectedVariantId === variant.id" 
            @click="selectedVariantId = (variant.id as string)"
          />
        </div>
      </div>
      <Button variant="default" size="lg" @click="addToCart()">
        {{ loading ? 'Carregando...' : 'Adicionar ao carrinho' }}
      </Button>
    </div>
  </section>
</template>
Clique aqui para expandir

O código dessa página é bem grande mas não é muito difícil de entender, o que você precisa saber é que a função client.products.list({handle: params.handle[0] as string}) é a que carrega o produto de acordo com a url e também temos uma função chamada addToCart que vai adicionar o produto ao carrinho seguindo exatamente a documentação do medusajs.

Outra coisa interessante é que vamos salvar o cart_id (identificador do carrinho) no localStorage, ou seja, sempre que a pessoa abrir o navegador e abrir sua loja novamente, ela vai continuar com os mesmos produtos no carrinho.

Opa, calma aí!

Parece que você não está logado, caso tenha interesse em salvar seu progresso de estudo faça login agora.