diff --git a/proyecto/settings.py b/proyecto/settings.py
index 9267575..1acf350 100644
--- a/proyecto/settings.py
+++ b/proyecto/settings.py
@@ -104,6 +104,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.forms',
'compressor',
+ 'ninja',
]
if S3_ENABLE:
diff --git a/proyecto/urls.py b/proyecto/urls.py
index d7813f5..f6261f5 100644
--- a/proyecto/urls.py
+++ b/proyecto/urls.py
@@ -19,11 +19,17 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from tienda import views as tienda_views
+from tienda.api import router as api_router
+from ninja import NinjaAPI
+
+api = NinjaAPI(title="Comercialmeria API", version="1.0.0")
+api.add_router("/", api_router)
urlpatterns = [
path('', tienda_views.home, name='home'),
path('admin/', admin.site.urls),
- path('tienda/', include('tienda.urls'))
+ path('tienda/', include('tienda.urls')),
+ path('api/', api.urls),
]
if settings.DEBUG and (
diff --git a/pyproject.toml b/pyproject.toml
index 916647b..ae3369b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,6 +7,7 @@ dependencies = [
"celery==5.6.3",
"Django==6.0.5",
"django-compressor==4.6.0",
+ "django-ninja>=1.6.2",
"django-redis==6.0.0",
# S3 backend requerido por tienda/storage_backends.py cuando S3_ENABLE=True.
"django-storages[s3]==1.14.6",
diff --git a/tienda/api.py b/tienda/api.py
new file mode 100644
index 0000000..b676ba5
--- /dev/null
+++ b/tienda/api.py
@@ -0,0 +1,93 @@
+from typing import Optional
+from ninja import Router, Schema
+from django.db.models import Count
+from django.shortcuts import get_object_or_404
+from .models import Category, Product
+
+router = Router()
+
+class CategoryOut(Schema):
+ id: int
+ name: str
+ product_count: int
+
+class ImageInfo(Schema):
+ url: str
+ alt: str
+
+class ProductListOut(Schema):
+ id: int
+ name: str
+ sku: Optional[str] = None
+ briefdesc: str
+ price: float
+ price_with_vat: float
+ stock: int
+ category_id: int
+ category_name: str
+ primary_image: Optional[ImageInfo] = None
+ average_rating: float
+ reviews_count: int
+
+class ProductDetailOut(ProductListOut):
+ description: str
+ secondary_images: list[ImageInfo]
+
+
+def _image_info(img, request):
+ if not img:
+ return None
+ return ImageInfo(
+ url=request.build_absolute_uri(img.image.url),
+ alt=img.alt or img.name,
+ )
+
+def _product_to_list_out(p, request):
+ return ProductListOut(
+ id=p.id,
+ name=p.name,
+ sku=p.sku,
+ briefdesc=p.briefdesc,
+ price=p.price,
+ price_with_vat=p.get_price_with_vat(),
+ stock=p.stock,
+ category_id=p.category_id,
+ category_name=p.category.name,
+ primary_image=_image_info(p.primary_image, request),
+ average_rating=p.get_average_rating(),
+ reviews_count=p.get_reviews_count(),
+ )
+
+def _product_to_detail_out(p, request):
+ base = _product_to_list_out(p, request)
+ data = base.dict()
+ data["description"] = p.description
+ data["secondary_images"] = [
+ _image_info(img, request) for img in p.secondary_images.all()
+ ]
+ return ProductDetailOut(**data)
+
+
+@router.get("/categorias", response=list[CategoryOut])
+def listar_categorias(request):
+ qs = Category.objects.annotate(product_count=Count("product"))
+ return [
+ CategoryOut(id=c.id, name=c.name, product_count=c.product_count)
+ for c in qs
+ ]
+
+@router.get("/productos", response=list[ProductListOut])
+def listar_productos(request, categoria_id: Optional[int] = None):
+ qs = Product.objects.select_related("category", "primary_image")
+ if categoria_id:
+ qs = qs.filter(category_id=categoria_id)
+ return [_product_to_list_out(p, request) for p in qs]
+
+@router.get("/productos/{product_id}", response=ProductDetailOut)
+def detalle_producto(request, product_id: int):
+ p = get_object_or_404(
+ Product.objects.select_related("category", "primary_image")
+ .prefetch_related("secondary_images"),
+ id=product_id,
+ )
+ return _product_to_detail_out(p, request)
diff --git a/tienda/static/css/custom.css b/tienda/static/css/custom.css
index fecb4ce..632a028 100644
--- a/tienda/static/css/custom.css
+++ b/tienda/static/css/custom.css
@@ -318,3 +318,23 @@ p.price {
overflow-wrap: break-word;
word-wrap: break-word;
}
+
+:root {
+ --chat--color--primary: #513CB0;
+ --chat--color--primary-shade-50: #3f2a8f;
+ --chat--color--primary--shade-100: #361DA7;
+ --chat--color--secondary: #513CB0;
+ --chat--color-secondary-shade-50: #3f2a8f;
+ --chat--color--typing: #513CB0;
+ --chat--color-dark: #101330;
+ --chat--window--border-radius: 12px;
+ --chat--toggle--background: #513CB0;
+ --chat--toggle--hover--background: #3f2a8f;
+ --chat--toggle--active--background: #361DA7;
+ --chat--message--bot--background: #f0f0f0;
+ --chat--message--user--background: #513CB0;
+ --chat--header--background: #513CB0;
+ --chat--input--send--button--color: #513CB0;
+ --chat--input--send--button--color-hover: #3f2a8f;
+ --chat--close--button--color-hover: #FC3F44;
+}
diff --git a/tienda/templates/tienda/base.html b/tienda/templates/tienda/base.html
index 8cc1491..3226fc2 100644
--- a/tienda/templates/tienda/base.html
+++ b/tienda/templates/tienda/base.html
@@ -344,5 +344,27 @@
});
{% endcache %}
+
+
+