Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/fredy-rizo/ecommerce-delivery-frontend/llms.txt

Use this file to discover all available pages before exploring further.

The product store is the main customer-facing catalog for Ecommerce Delivery. Any visitor with a valid session can scroll through available products, filter by name or shirt type, and open a detailed product modal. Admin users (role 2) additionally see an Agregar button that opens the FormProducts dialog to create or edit catalog entries.

Route & components

The route /Store (also registered as posts) renders pages/store/MainStore.vue. That page composes three child components:
ComponentResponsibility
BarFilterProductsDebounced text search input; accepts getProductsByType callback for category filtering
ProductCardCard tile for each product in the grid and inside the detail modal
FormProductsCreate / edit form submitted to the REST API

Browsing products

Products load through a q-infinite-scroll component. Each scroll event calls listProducts(index, done), which increments the page counter and fires the following fetch:
const response = await fetch(
  process.env.API_SERVER +
    `/api/product/list-product/${pagination.value.pag}/${pagination.value.perpage}`,
  {
    method: "POST",
    mode: "cors",
    headers,
    body: JSON.stringify(filter.value), // { title: "" }
  }
);
The server returns { status, listPrd: [...], pagination: { pags } }. When pagination.pags === pagination.pag (no more pages), done() is called and scrolling stops.

Filtering

By name

BarFilterProducts watches its query ref with a 400 ms debounce and calls getDataFilter(query) in the parent:
const response = await fetch(
  `${process.env.API_SERVER}/api/product/search-product/${encodeURIComponent(query)}`,
  {
    method: "POST",
    headers,
  }
);
The query string is URL-encoded before being embedded in the path.

By category (shirt type)

When a type is selected, getProductsByType(typeValue) is called:
const url = `${process.env.API_SERVER}/api/product/get-product-type-prd/${typeValue}/${pagination.value.pag}/${pagination.value.perpage}`;

const response = await fetch(url, {
  method: "POST",
  mode: "cors",
  headers,
});

Resetting the filter

When the search input is cleared, getDataFilter receives an empty string and falls back to resetListProducts(), which resets pagination.pag to 1, clears the products array, and re-fetches from the first page.

Product detail modal

Clicking a ProductCard tile calls showModalProduct(product), which fetches full product data by ID and opens a q-dialog:
const response = await fetch(
  process.env.API_SERVER + `/api/product/list-id/${product._id}`,
  {
    method: "POST",
    mode: "cors",
    headers,
  }
);
On success, oneProduct is populated and detailmodal is set to true. Inside the dialog, ProductCard is rendered with fulldescription: true and details: true, which expands the description, color swatches, available sizes, and stock count.

Admin: adding a product

The Agregar button is conditionally rendered only for admin users:
<div
  class="col-auto flex items-center q-ml-md"
  v-if="dataUser.roles.find((itm) => itm.value == 2)"
>
  <q-btn color="green-7" label="Agregar" @click="dialogmdel = true" />
</div>
Clicking the button opens a q-dialog containing FormProducts. On submit, the form builds a FormData object and posts to POST /api/product/create:
const options = {
  method: "POST",
  headers: { Authorization: "Bearer " + sessionUser.token },
  body: formData,
};
fetch(process.env.API_SERVER + "/api/product/create", options);

Form fields

FieldTypeNotes
titletextProduct name
descriptiontextareaFull product description
pricenumberPrice in COP
minCantnumberAvailable stock
colorsSizemulti-selectJSON-serialised array of { label, value }
sizesmulti-selectJSON-serialised array of { label }
typeShirtselectSingle label string
productImgfile (multiple)multipart/form-data field, image/*

Available colors (24 options)

[
  { "label": "Blanco",           "value": "1"  },
  { "label": "Negro",            "value": "2"  },
  { "label": "Gris oscuro",      "value": "3"  },
  { "label": "Gris claro",       "value": "4"  },
  { "label": "Azul celeste",     "value": "5"  },
  { "label": "Azul marino",      "value": "6"  },
  { "label": "Azul rey",         "value": "7"  },
  { "label": "Beige",            "value": "8"  },
  { "label": "Cafe claro",       "value": "9"  },
  { "label": "Cafe oscuro",      "value": "10" },
  { "label": "Verde militar",    "value": "11" },
  { "label": "Verde bosque",     "value": "12" },
  { "label": "Verde menta",      "value": "13" },
  { "label": "Rojo",             "value": "14" },
  { "label": "Vino",             "value": "15" },
  { "label": "Rosa pastel",      "value": "16" },
  { "label": "Fucsia",           "value": "17" },
  { "label": "Amarillo mostaza", "value": "18" },
  { "label": "Amarillo crema",   "value": "19" },
  { "label": "Naranja",          "value": "20" },
  { "label": "Lila",             "value": "21" },
  { "label": "Morado",           "value": "22" },
  { "label": "Turquesa",         "value": "23" },
  { "label": "Amarillo",         "value": "24" }
]

Available sizes

[
  { "label": "XS",       "value": "1" },
  { "label": "S",        "value": "2" },
  { "label": "M",        "value": "3" },
  { "label": "L",        "value": "4" },
  { "label": "XL",       "value": "5" },
  { "label": "XXL/2XL",  "value": "6" },
  { "label": "XXXL/3XL", "value": "7" }
]

Shirt types

[
  { "label": "Overside",      "value": "1" },
  { "label": "CropTop",       "value": "2" },
  { "label": "Regular Fit",   "value": "3" },
  { "label": "Semi-Overside", "value": "4" },
  { "label": "Hoodie",        "value": "5" }
]

Admin: editing a product

The same FormProducts component is reused for editing. The parent passes the selected product as the oneProduct prop, which pre-populates all fields via onMounted:
// FormProducts.vue – onMounted
if (props.oneProduct) {
  const colors = parseIfString(props.oneProduct.colorsSize);
  const sizes  = parseIfString(props.oneProduct.sizes);

  typesColorsSize.value = Array.isArray(colors)
    ? colors.map((c) => ({ label: c.label, value: c.value }))
    : [];

  typeSizes.value = Array.isArray(sizes)
    ? sizes.map((s) => ({ label: s.label, value: s.value }))
    : [];
}
The submit handler detects an existing product by props.oneProduct._id and posts to POST /api/product/update/:id instead of the create endpoint.

Discount field (edit mode only)

The discount input is rendered only when oneProduct is present:
<div class="column q-gutter-md q-pa-md" v-if="oneProduct">
  <q-input
    v-model.number="discount"
    label="Descuento (%)"
    type="number"
    min="0"
    max="100"
  />
  <q-input
    v-if="discount > 0"
    :model-value="priceDiscount"
    label="Precio con descuento"
    readonly
    hint="Calculado automáticamente"
  />
</div>
priceDiscount is a computed property:
const priceDiscount = computed(() => {
  if (!price.value || !discount.value || discount.value <= 0) return 0;
  const discountAmount = (price.value * discount.value) / 100;
  return Math.round(price.value - discountAmount);
});

Preserving existing images

If no new image files are selected during an edit, the existing images are sent back as JSON so the server keeps them:
if (
  (!dataimg.value || dataimg.value.length === 0) &&
  props.oneProduct?.productImg?.length > 0
) {
  formData.append(
    "productImgOld",
    JSON.stringify(props.oneProduct.productImg)
  );
}

Authorization header for admin actions

All create and update requests include a Bearer token in the Authorization header:
const options = {
  method: "POST",
  headers: { Authorization: "Bearer " + sessionUser.token },
  body: formData,
};
sessionUser is obtained from validateUser({ rol: 2 }), which reads the stored session and verifies the user has the admin role. If validation fails, the request is blocked and the user is notified.

Build docs developers (and LLMs) love