feat: implement shopping cart feature with user authentication support
All checks were successful
Build and Push Docker Image / build (push) Successful in 42s
All checks were successful
Build and Push Docker Image / build (push) Successful in 42s
- Added `Cart` and `CartItem` models with migrations for managing user-specific carts and items - Developed services for cart-related operations: adding, removing, and updating cart items - Built specialized templates (`cart.html`) and styled them with new CSS assets (`cart.css`) - Enabled real-time UI updates via AJAX in shopping cart interactions (`cart.js`) - Modified header to dynamically display cart link with user authentication awareness - Added Leaflet-based interactive map to the contacts page and integrated CSS/JS dependencies - Expanded tests for cart functionality, item detail page cart integration, and contacts page maps
This commit is contained in:
@@ -85,7 +85,7 @@
|
||||
<path d="m8 2.748-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.989 6.286 6.357 3.452-2.368 5.365-4.542 6.286-6.357.955-1.886.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01zM8 15C-7.333 4.868 3.279-3.04 7.824 1.143q.09.083.176.171a3 3 0 0 1 .176-.17C12.72-3.042 23.333 4.867 8 15"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#">
|
||||
<a href="{% if request.user.is_authenticated %}{% url 'orders:cart' %}{% else %}{% url 'users:login' %}?next={% url 'orders:cart' %}{% endif %}" aria-label="Корзина">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||
class="bi bi-cart2" viewBox="0 0 16 16">
|
||||
<path d="M0 2.5A.5.5 0 0 1 .5 2H2a.5.5 0 0 1 .485.379L2.89 4H14.5a.5.5 0 0 1 .485.621l-1.5 6A.5.5 0 0 1 13 11H4a.5.5 0 0 1-.485-.379L1.61 3H.5a.5.5 0 0 1-.5-.5M3.14 5l1.25 5h8.22l1.25-5zM5 13a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0m9-1a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-2 1a2 2 0 1 1 4 0 2 2 0 0 1-4 0"></path>
|
||||
|
||||
@@ -54,5 +54,7 @@
|
||||
})
|
||||
</script>
|
||||
<script src="{% static 'js/favorites.js' %}"></script>
|
||||
<script src="{% static 'js/cart.js' %}"></script>
|
||||
{% block add_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -61,7 +61,28 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="product-summary__actions">
|
||||
<button class="product-summary__button product-summary__button--primary" type="button">В корзину</button>
|
||||
{% if item.id in cart_item_ids %}
|
||||
<a class="product-summary__button product-summary__button--primary product-summary__button--link" href="{% url 'orders:cart' %}">
|
||||
Перейти в корзину
|
||||
</a>
|
||||
{% elif request.user.is_authenticated %}
|
||||
<form method="post" action="{% url 'orders:add' %}" class="product-summary__cart-form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="next" value="{{ request.get_full_path }}">
|
||||
<button class="product-summary__button product-summary__button--primary" type="submit" {% if not item.is_available %}disabled{% endif %}>
|
||||
В корзину
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a
|
||||
class="product-summary__button product-summary__button--primary product-summary__button--link"
|
||||
href="{% url 'users:login' %}?next={{ request.get_full_path|urlencode }}"
|
||||
{% if not item.is_available %}aria-disabled="true"{% endif %}
|
||||
>
|
||||
В корзину
|
||||
</a>
|
||||
{% endif %}
|
||||
<button class="product-summary__button product-summary__button--secondary" type="button">Купить</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,16 +1,110 @@
|
||||
{% extends "_layouts/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Главная{% endblock %}
|
||||
{% block title %}Контакты{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
|
||||
<link rel="stylesheet" href="{% static 'css/contacts.css' %}">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""
|
||||
>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_fragments/header.html" %}
|
||||
{% include "_fragments/header.html" with active="contacts" breadcrumb=1 %}
|
||||
|
||||
<main class="container contacts-page">
|
||||
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2='/contacts/' text_2='Контакты' %}
|
||||
|
||||
<section class="contacts-page__hero">
|
||||
<h1>КОНТАКТЫ</h1>
|
||||
|
||||
<div class="contacts-grid">
|
||||
<div class="contacts-column">
|
||||
<section class="contacts-card">
|
||||
<h2>Адрес:</h2>
|
||||
<p>{{ contact_address }}</p>
|
||||
</section>
|
||||
|
||||
<section class="contacts-card">
|
||||
<h2>Режим работы:</h2>
|
||||
<p>{{ contact_hours_weekdays }}</p>
|
||||
<p>{{ contact_hours_weekends }}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="contacts-column">
|
||||
<section class="contacts-card">
|
||||
<h2>Телефон:</h2>
|
||||
<p><a href="tel:{{ contact_phone_primary_url }}">{{ contact_phone_primary }}</a></p>
|
||||
<p><a href="tel:{{ contact_phone_secondary_url }}">{{ contact_phone_secondary }}</a></p>
|
||||
</section>
|
||||
|
||||
<section class="contacts-card">
|
||||
<h2>E-mail</h2>
|
||||
<p><a href="mailto:{{ contact_email }}">{{ contact_email }}</a></p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="contacts-map-section" aria-labelledby="contacts-map-title">
|
||||
<h2 class="contacts-map-section__title" id="contacts-map-title">Как нас найти</h2>
|
||||
|
||||
<div
|
||||
id="contacts-map"
|
||||
class="contacts-map"
|
||||
data-lat="{{ map_lat }}"
|
||||
data-lon="{{ map_lon }}"
|
||||
data-zoom="{{ map_zoom }}"
|
||||
data-title="SantehRay"
|
||||
></div>
|
||||
|
||||
<noscript>
|
||||
<div class="contacts-map contacts-map--fallback contacts-map--noscript">
|
||||
<div class="contacts-map-fallback">
|
||||
<h3>Для интерактивной карты нужен JavaScript.</h3>
|
||||
<a href="{{ map_external_url }}" target="_blank" rel="noreferrer">Открыть адрес в OpenStreetMap</a>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{# TODO #}
|
||||
В разработке
|
||||
{% include "_fragments/footer.html" %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block add_js %}
|
||||
<script
|
||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""
|
||||
></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const mapRoot = document.getElementById('contacts-map')
|
||||
if (!mapRoot || typeof L === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const lat = Number(mapRoot.dataset.lat)
|
||||
const lon = Number(mapRoot.dataset.lon)
|
||||
const zoom = Number(mapRoot.dataset.zoom)
|
||||
const title = mapRoot.dataset.title || 'SantehRay'
|
||||
|
||||
const map = L.map(mapRoot, {
|
||||
scrollWheelZoom: true,
|
||||
}).setView([lat, lon], zoom)
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
}).addTo(map)
|
||||
|
||||
L.marker([lat, lon]).addTo(map).bindPopup(title).openPopup()
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
113
templates/orders/cart.html
Normal file
113
templates/orders/cart.html
Normal file
@@ -0,0 +1,113 @@
|
||||
{% extends "_layouts/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Корзина{% endblock %}
|
||||
|
||||
{% block add_css %}
|
||||
<link rel="stylesheet" href="{% static "css/cart.css" %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "_fragments/header.html" %}
|
||||
|
||||
{% include '_fragments/breadcrumbs.html' with url_1='/' text_1='Главная' url_2='/orders/cart/' text_2='Корзина' %}
|
||||
|
||||
<main class="container cart-page">
|
||||
<section class="cart-hero">
|
||||
<p class="cart-kicker">Покупки</p>
|
||||
<div class="cart-hero__row">
|
||||
<div>
|
||||
<h1 class="cart-title">Корзина</h1>
|
||||
<p class="cart-description">
|
||||
Проверьте выбранные позиции, измените количество или удалите лишнее перед оформлением.
|
||||
</p>
|
||||
</div>
|
||||
<div class="cart-summary-card">
|
||||
<span class="cart-summary-card__value" data-cart-total-quantity>{{ total_quantity }}</span>
|
||||
<span class="cart-summary-card__label">товаров в корзине</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cart-content" aria-label="Содержимое корзины">
|
||||
{% if cart_items %}
|
||||
<div class="cart-items">
|
||||
{% for cart_item in cart_items %}
|
||||
{% with item=cart_item.item %}
|
||||
<article class="cart-item" data-auth-cart-item data-current-quantity="{{ cart_item.quantity }}">
|
||||
<a class="cart-item__media" href="{% url 'detail' item.category.slug item.slug %}">
|
||||
{% with primary_image=item.primary_image %}
|
||||
{% if primary_image %}
|
||||
<img src="{{ primary_image.image.url }}" alt="{{ primary_image.alt|default:item.name }}">
|
||||
{% else %}
|
||||
<div class="cart-item__placeholder">Нет фото</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
|
||||
<div class="cart-item__body">
|
||||
<div class="cart-item__main">
|
||||
<p class="cart-item__category">
|
||||
{{ item.category.name }}{% if item.subcategory %} / {{ item.subcategory.name }}{% endif %}
|
||||
</p>
|
||||
<a class="cart-item__title" href="{% url 'detail' item.category.slug item.slug %}">{{ item.name }}</a>
|
||||
<p class="cart-item__price">{{ item.price }} руб.</p>
|
||||
</div>
|
||||
|
||||
<div class="cart-item__controls">
|
||||
<div class="cart-quantity">
|
||||
<button
|
||||
class="cart-quantity__button"
|
||||
type="button"
|
||||
data-cart-decrement
|
||||
data-remove-form-id="cart-remove-{{ cart_item.id }}"
|
||||
data-update-form-id="cart-decrement-{{ cart_item.id }}"
|
||||
data-current-quantity="{{ cart_item.quantity }}"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span class="cart-quantity__value">{{ cart_item.quantity }}</span>
|
||||
<form id="cart-increment-{{ cart_item.id }}" method="post" action="{% url 'orders:update_quantity' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="quantity" value="{{ cart_item.quantity|add:1 }}">
|
||||
<input type="hidden" name="next" value="{% url 'orders:cart' %}">
|
||||
<button class="cart-quantity__button" type="submit">+</button>
|
||||
</form>
|
||||
<form id="cart-decrement-{{ cart_item.id }}" method="post" action="{% url 'orders:update_quantity' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="quantity" value="{{ cart_item.quantity|add:-1 }}">
|
||||
<input type="hidden" name="next" value="{% url 'orders:cart' %}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="cart-item__aside">
|
||||
<span class="cart-item__subtotal">{{ cart_item.subtotal }} руб.</span>
|
||||
<form id="cart-remove-{{ cart_item.id }}" method="post" action="{% url 'orders:remove' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="item_id" value="{{ item.id }}">
|
||||
<input type="hidden" name="next" value="{% url 'orders:cart' %}">
|
||||
<button class="cart-item__remove" type="submit">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<aside class="cart-totals">
|
||||
<p class="cart-totals__label">Итого</p>
|
||||
<p class="cart-totals__value" data-cart-total-amount>{{ total_amount }}</p>
|
||||
<p class="cart-totals__meta">Позиции можно изменить прямо на этой странице.</p>
|
||||
</aside>
|
||||
{% else %}
|
||||
<p class="cart-empty">Корзина пока пуста.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{% include "_fragments/footer.html" %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user