feat: cart detail
This commit is contained in:
@@ -0,0 +1,142 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
<section class="bg-white py-4 antialiased dark:bg-gray-900 md:py-16">
|
||||||
|
<div class="mx-auto max-w-screen-xl px-4 2xl:px-0">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">{% translate 'Carrito' %}</h2>
|
||||||
|
|
||||||
|
<div class="mt-4 sm:mt-6 md:gap-6 lg:flex lg:items-start xl:gap-8">
|
||||||
|
<div class="mx-auto w-full flex-none lg:max-w-2xl xl:max-w-4xl">
|
||||||
|
<div class="space-y-6">
|
||||||
|
|
||||||
|
{% for item in items %}
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 md:p-6">
|
||||||
|
<div class="space-y-4 md:flex md:items-center md:justify-between md:gap-6 md:space-y-0">
|
||||||
|
<a href="#" class="shrink-0 md:order-1">
|
||||||
|
<img class="h-20 w-20 dark:hidden" src="{{ item.product.images.all|first }}"
|
||||||
|
alt="{{ item.product.name }}"/>
|
||||||
|
<img class="hidden h-20 w-20 dark:block" src="{{ item.product.images.all|first }}"
|
||||||
|
alt="{{ item.product.name }}"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<label for="counter-input" class="sr-only">{% translate 'Cantidad' %}</label>
|
||||||
|
<div class="flex items-center justify-between md:order-3 md:justify-end">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="w-10 shrink-0 border-0 bg-transparent text-center text-sm font-medium text-gray-900 focus:outline-none focus:ring-0 dark:text-white">
|
||||||
|
{{ item.quantity }}x
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-end md:order-4 md:w-32">
|
||||||
|
<p class="text-base font-bold text-gray-900 dark:text-white">{{ item.product.price.price_with_tax }}
|
||||||
|
€</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full min-w-0 flex-1 space-y-4 md:order-2 md:max-w-md">
|
||||||
|
<a href="#"
|
||||||
|
class="text-base font-medium text-gray-900 hover:underline dark:text-white">{{ item.product.name }}</a>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-900 hover:underline dark:text-gray-400 dark:hover:text-white">
|
||||||
|
{% include 'components/icons/like.svg' %}
|
||||||
|
{% translate 'Añadir a favoritos' %}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<form class="flex" hx-post="{% url 'web:delete_cart_item' pk=item.pk %}"
|
||||||
|
hx-swap="none">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="inline-flex items-center text-sm font-medium text-red-600 hover:underline dark:text-red-500"
|
||||||
|
>
|
||||||
|
{% include 'components/icons/x.svg' %}
|
||||||
|
{% translate 'Quitar' %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if items.count == 0 %}
|
||||||
|
<span class="text-base font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
{% translate 'No hay nada en el carrito...' %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mx-auto mt-6 max-w-4xl flex-1 space-y-6 lg:mt-0 lg:w-full">
|
||||||
|
<div
|
||||||
|
class="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">
|
||||||
|
<p class="text-xl font-semibold text-gray-900 dark:text-white">{% translate 'Resumen del pedido' %}</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<dl class="flex items-center justify-between gap-4">
|
||||||
|
<dt class="text-base font-normal text-gray-500 dark:text-gray-400">{% translate 'Base imponible' %}</dt>
|
||||||
|
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ base_total }}</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="flex items-center justify-between gap-4">
|
||||||
|
<dt class="text-base font-normal text-gray-500 dark:text-gray-400">{% translate 'Descuentos' %}</dt>
|
||||||
|
<dd class="text-base font-medium text-green-600">-{{ savings }}</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="flex items-center justify-between gap-4">
|
||||||
|
<dt
|
||||||
|
class="text-base font-normal text-gray-500 dark:text-gray-400">{% translate 'Gastos de envío' %}</dt>
|
||||||
|
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ shipping_cost }} €</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<dl class="flex items-center justify-between gap-4">
|
||||||
|
<dt class="text-base font-normal text-gray-500 dark:text-gray-400">{% translate 'Impuestos' %}</dt>
|
||||||
|
<dd class="text-base font-medium text-gray-900 dark:text-white">{{ tax_total }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="flex items-center justify-between gap-4 border-t border-gray-200 pt-2 dark:border-gray-700">
|
||||||
|
<dt class="text-base font-bold text-gray-900 dark:text-white">{% translate 'Total' %}</dt>
|
||||||
|
<dd class="text-base font-bold text-gray-900 dark:text-white">{{ total }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="">
|
||||||
|
<button
|
||||||
|
class="flex w-full items-center justify-center rounded-lg bg-primary-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
|
||||||
|
{% translate 'Ir a pagar' %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<span class="text-sm font-normal text-gray-500 dark:text-gray-400"> {% translate 'o' %} </span>
|
||||||
|
<a href="{% url 'web:index' %}" title=""
|
||||||
|
class="inline-flex items-center gap-2 text-sm font-medium text-primary-700 underline hover:no-underline dark:text-primary-500">
|
||||||
|
{% translate 'Seguir comprando' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# <div#}
|
||||||
|
{# class="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800 sm:p-6">#}
|
||||||
|
{# <form class="space-y-4">#}
|
||||||
|
{# <div>#}
|
||||||
|
{# <label for="voucher" class="mb-2 block text-sm font-medium text-gray-900 dark:text-white"> Do you have a#}
|
||||||
|
{# voucher or gift card? </label>#}
|
||||||
|
{# <input type="text" id="voucher"#}
|
||||||
|
{# class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"#}
|
||||||
|
{# placeholder="" required/>#}
|
||||||
|
{# </div>#}
|
||||||
|
{# <button type="submit"#}
|
||||||
|
{# class="flex w-full items-center justify-center rounded-lg bg-primary-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-800 focus:outline-none focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">#}
|
||||||
|
{# Apply Code#}
|
||||||
|
{# </button>#}
|
||||||
|
{# </form>#}
|
||||||
|
{# </div>#}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
|
|
||||||
{% if cart.items.count > 0 %}
|
{% if cart.items.count > 0 %}
|
||||||
<a href="{% url 'web:cart_detail' %}"
|
<a href="{% url 'web:cart_detail' %}"
|
||||||
class="mb-2 me-2 inline-flex w-full items-center justify-center rounded-lg bg-gray-700 px-5 py-2.5 text-sm
|
class="mb-2 me-2 inline-flex w-full items-center justify-center rounded-lg bg-gray-200 px-5 py-2.5 text-sm
|
||||||
font-medium text-white hover:bg-gray-800 focus:outline-none focus:ring-4 focus:ring-gray-300
|
font-medium dark:text-white hover:bg-gray-300 focus:outline-none focus:ring-4 focus:ring-gray-300
|
||||||
dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800"
|
||||||
role="button"> {% translate 'Ver carrito' %}
|
role="button"> {% translate 'Ver carrito' %}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg class="me-1.5 h-5 w-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M6 18 17.94 6M18 18 6.06 6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 288 B |
@@ -5,7 +5,7 @@
|
|||||||
<section class="bg-gray-50 dark:bg-gray-900">
|
<section class="bg-gray-50 dark:bg-gray-900">
|
||||||
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
||||||
|
|
||||||
<a href="#" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
<a href="{% url 'web:index' %}" class="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||||
<img class="w-8 h-8 mr-2" src="{% get_logo_image %}" alt="logo">
|
<img class="w-8 h-8 mr-2" src="{% get_logo_image %}" alt="logo">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -3,26 +3,5 @@
|
|||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
{% include 'components/generic/navbar.html' %}
|
{% include 'components/generic/navbar.html' %}
|
||||||
<section class="p-16 mx-auto justify-between mb-4 grid gap-4 sm:grid-cols-1 md:mb-8 lg:grid-cols-2 xl:grid-cols-2">
|
<section hx-get="{% url 'web:cart' %}" hx-trigger="load, updated-cart"></section>
|
||||||
<section class="p-2 border border-gray-100 dark:border-gray-600 rounded-l">
|
|
||||||
<ul>
|
|
||||||
{% for item in items %}
|
|
||||||
<li class="p-4">
|
|
||||||
<a href="{% url 'web:product_detail' pk=item.product.pk slug=item.product.slug %}"
|
|
||||||
class="text-lg font-semibold leading-tight text-gray-900 hover:underline dark:text-white">
|
|
||||||
<img class="mx-auto h-24 w-auto dark:hidden" src="{{ item.product.images.all|first }}"
|
|
||||||
alt="{{ product.name }}"/>
|
|
||||||
<img class="mx-auto hidden h-24 w-auto dark:block" src="{{ item.product.images.all|first }}"
|
|
||||||
alt="{{ product.name }}"/>
|
|
||||||
|
|
||||||
{{ item.product.name }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section class="p-2 border border-gray-100 dark:border-gray-600 rounded-l">
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from web.views.components import cart_dropdown, list_products
|
from web.views.components import cart, cart_dropdown, list_products
|
||||||
from web.views.users import login, logout, register, reset_password
|
from web.views.users import login, logout, register, reset_password
|
||||||
from web.views.web import (
|
from web.views.web import (
|
||||||
add_cart_item,
|
add_cart_item,
|
||||||
@@ -12,7 +12,6 @@ from web.views.web import (
|
|||||||
|
|
||||||
app_name = "web"
|
app_name = "web"
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index, name="index"),
|
path("", index, name="index"),
|
||||||
path("products/<int:pk>/<str:slug>/", product_detail, name="product_detail"),
|
path("products/<int:pk>/<str:slug>/", product_detail, name="product_detail"),
|
||||||
@@ -25,6 +24,7 @@ urlpatterns = [
|
|||||||
# components
|
# components
|
||||||
path("web/components/products/", list_products, name="list_products"),
|
path("web/components/products/", list_products, name="list_products"),
|
||||||
path("web/components/cart-dropdown/", cart_dropdown, name="cart_dropdown"),
|
path("web/components/cart-dropdown/", cart_dropdown, name="cart_dropdown"),
|
||||||
|
path("web/components/cart/", cart, name="cart"),
|
||||||
# api
|
# api
|
||||||
path("web/add-cart-item/", add_cart_item, name="add_cart_item"),
|
path("web/add-cart-item/", add_cart_item, name="add_cart_item"),
|
||||||
path("web/delete-cart-item/<int:pk>/", delete_cart_item, name="delete_cart_item"),
|
path("web/delete-cart-item/<int:pk>/", delete_cart_item, name="delete_cart_item"),
|
||||||
|
|||||||
+11
-1
@@ -1,4 +1,8 @@
|
|||||||
from shop.models import Cart
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
from shop.models import Cart, CartItem
|
||||||
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
|
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
|
||||||
|
|
||||||
|
|
||||||
@@ -16,3 +20,9 @@ def get_or_create_cart(request) -> [Cart, bool]:
|
|||||||
cart, created = Cart.objects.get_or_create(user=request.user)
|
cart, created = Cart.objects.get_or_create(user=request.user)
|
||||||
|
|
||||||
return cart, created
|
return cart, created
|
||||||
|
|
||||||
|
|
||||||
|
def get_cart_total(cart: Cart):
|
||||||
|
return CartItem.objects.filter(cart=cart).annotate(cart_total=Sum("total")).get(
|
||||||
|
"cart_total"
|
||||||
|
) or Decimal("0.00")
|
||||||
|
|||||||
+36
-2
@@ -1,11 +1,14 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db.models import Sum
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from shop.filters import ProductFilter
|
from shop.filters import ProductFilter
|
||||||
from shop.models import Product
|
from shop.models import CartItem, Product, ProductPrice
|
||||||
from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin
|
from web.mixins import FilteredQuerysetMixin, PaginatedQuerysetMixin
|
||||||
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
|
from web.settings import ANONYMOUS_CART_ID_COOKIE_NAME
|
||||||
from web.utils import get_or_create_cart
|
from web.utils import get_cart_total, get_or_create_cart
|
||||||
|
|
||||||
|
|
||||||
class ListProducts(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin):
|
class ListProducts(TemplateView, FilteredQuerysetMixin, PaginatedQuerysetMixin):
|
||||||
@@ -42,4 +45,35 @@ def cart_dropdown(request, *args, **kwargs):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def cart(request, *args, **kwargs):
|
||||||
|
cart, created = get_or_create_cart(request)
|
||||||
|
items = CartItem.objects.filter(cart=cart)
|
||||||
|
|
||||||
|
base_total = Decimal("0.00")
|
||||||
|
tax_total = Decimal("0.00")
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
price = ProductPrice.objects.filter(product=item.product, current=True).first()
|
||||||
|
base_total += price.price * item.quantity
|
||||||
|
tax_total += round(price.price * Decimal(price.tax.value / 100), 2)
|
||||||
|
|
||||||
|
total = base_total + tax_total
|
||||||
|
|
||||||
|
response = render(
|
||||||
|
request,
|
||||||
|
"components/cart/cart.html",
|
||||||
|
context={
|
||||||
|
"cart": cart,
|
||||||
|
"items": items,
|
||||||
|
"total": total,
|
||||||
|
"base_total": base_total,
|
||||||
|
"tax_total": tax_total,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
response.set_cookie(ANONYMOUS_CART_ID_COOKIE_NAME, cart.uuid)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
list_products = ListProducts.as_view()
|
list_products = ListProducts.as_view()
|
||||||
|
|||||||
Reference in New Issue
Block a user