feat: implement shopping cart feature with user authentication support
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:
2026-05-15 23:18:16 +02:00
parent d2126e0fba
commit 52cbc1c2b3
22 changed files with 1317 additions and 18 deletions

268
static/css/cart.css Normal file
View File

@@ -0,0 +1,268 @@
.cart-page {
margin-bottom: 88px;
}
.cart-hero {
padding: 28px 0 34px;
}
.cart-kicker {
color: #63768d;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.cart-hero__row {
margin-top: 12px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
}
.cart-title {
font-size: 44px;
line-height: 1.04;
}
.cart-description {
margin-top: 12px;
max-width: 620px;
color: #554971;
font-size: 18px;
line-height: 1.4;
}
.cart-summary-card {
min-width: 220px;
padding: 18px 22px;
border: 1px solid rgba(54, 33, 62, 0.12);
background: rgba(99, 118, 141, 0.06);
}
.cart-summary-card__value {
display: block;
font-size: 34px;
font-weight: 700;
}
.cart-summary-card__label {
display: block;
margin-top: 6px;
color: #554971;
}
.cart-content {
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 28px;
align-items: start;
}
.cart-content--guest {
min-height: 280px;
}
.cart-items {
display: flex;
flex-direction: column;
gap: 18px;
}
.cart-item {
display: grid;
grid-template-columns: 180px minmax(0, 1fr);
gap: 22px;
padding: 22px;
border: 1px solid rgba(54, 33, 62, 0.12);
background: #ffffff;
}
.cart-item__media {
display: block;
aspect-ratio: 4 / 3;
background: rgba(99, 118, 141, 0.08);
overflow: hidden;
}
.cart-item__media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.cart-item__placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #63768d;
}
.cart-item__body {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 18px;
min-width: 0;
}
.cart-item__category {
color: #63768d;
font-size: 15px;
}
.cart-item__title {
display: inline-block;
margin-top: 8px;
font-family: 'BerlinType', serif;
font-size: 28px;
line-height: 1.1;
}
.cart-item__price {
margin-top: 10px;
color: #554971;
font-size: 18px;
}
.cart-item__controls {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
}
.cart-quantity {
display: inline-flex;
align-items: center;
gap: 10px;
}
.cart-quantity form {
margin: 0;
}
.cart-quantity__button {
width: 38px;
height: 38px;
border: 1px solid #63768d;
background: transparent;
color: #36213e;
font-size: 24px;
line-height: 1;
cursor: pointer;
}
.cart-quantity__value {
min-width: 24px;
text-align: center;
font-size: 18px;
}
.cart-item__aside {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
}
.cart-item__subtotal {
font-size: 17px;
color: #36213e;
}
.cart-item__remove {
border: 0;
background: transparent;
color: #a14b4b;
cursor: pointer;
text-decoration: underline;
}
.cart-totals {
position: sticky;
top: 120px;
padding: 24px;
border: 1px solid rgba(54, 33, 62, 0.12);
background: rgba(99, 118, 141, 0.06);
}
.cart-totals__label {
color: #554971;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.cart-totals__value {
margin-top: 10px;
font-family: 'BerlinType', serif;
font-size: 36px;
line-height: 1.1;
}
.cart-totals__meta {
margin-top: 12px;
color: #554971;
font-size: 16px;
line-height: 1.4;
}
.cart-empty {
padding: 32px;
border: 1px solid rgba(54, 33, 62, 0.12);
color: #554971;
font-size: 18px;
line-height: 1.4;
}
.cart-item__summary-line {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
color: #554971;
font-size: 16px;
}
@media (max-width: 900px) {
.cart-page {
padding-left: 24px;
padding-right: 24px;
}
.cart-hero__row,
.cart-content {
grid-template-columns: 1fr;
display: grid;
}
.cart-summary-card,
.cart-totals {
min-width: 0;
position: static;
}
}
@media (max-width: 680px) {
.cart-item {
grid-template-columns: 1fr;
}
.cart-item__controls {
align-items: flex-start;
flex-direction: column;
}
.cart-item__aside {
align-items: flex-start;
}
.cart-title {
font-size: 34px;
}
}

View File

@@ -373,6 +373,10 @@ aside {
margin-top: 88px;
}
.product-summary__cart-form {
display: contents;
}
.product-summary__button {
min-height: 38px;
border: 1px solid #63768d;
@@ -384,6 +388,17 @@ aside {
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
}
.product-summary__button--link {
display: flex;
align-items: center;
justify-content: center;
}
.product-summary__button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.product-summary__button--primary {
background: #63768d;
color: #ffffff;

View File

@@ -0,0 +1,242 @@
.contacts-page {
padding-bottom: 120px;
}
.contacts-page__hero {
padding-top: 20px;
}
.contacts-page__hero > h1 {
font-weight: 400;
font-size: 54px;
line-height: 1;
color: #36213e;
}
.contacts-grid {
margin-top: 58px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 80px;
}
.contacts-column {
display: flex;
flex-direction: column;
gap: 42px;
}
.contacts-card h2 {
font-family: 'Noah', sans-serif;
font-weight: 600;
font-size: 30px;
line-height: 1.1;
color: #36213e;
}
.contacts-card p,
.contacts-card a {
font-size: 24px;
line-height: 1.3;
color: #36213e;
}
.contacts-card p + p {
margin-top: 8px;
}
.contacts-card h2 + p,
.contacts-card h2 + a,
.contacts-card h2 + div,
.contacts-card h2 + ul {
margin-top: 18px;
}
.contacts-card a {
text-decoration: underline;
text-underline-offset: 3px;
}
.contacts-map-section {
margin-top: 88px;
}
.contacts-map-section__title {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.contacts-map {
width: 100%;
min-height: 520px;
border: 1px solid rgba(54, 33, 62, 0.16);
background: linear-gradient(180deg, rgba(99, 118, 141, 0.08) 0%, rgba(99, 118, 141, 0.03) 100%);
overflow: hidden;
}
.contacts-map .leaflet-control-attribution,
.contacts-map .leaflet-control-attribution a {
font-family: 'Noah', sans-serif;
color: #554971;
}
.contacts-map .leaflet-popup-content-wrapper {
border-radius: 16px;
background: rgba(54, 33, 62, 0.94);
color: #ffffff;
box-shadow: 0 16px 34px rgba(54, 33, 62, 0.22);
}
.contacts-map .leaflet-popup-content {
margin: 12px 16px;
font-family: 'Noah', sans-serif;
font-size: 16px;
font-weight: 700;
}
.contacts-map .leaflet-popup-tip {
background: rgba(54, 33, 62, 0.94);
}
.contacts-map .leaflet-container {
font-family: 'Noah', sans-serif;
}
.contacts-map--fallback {
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
}
.contacts-map-fallback {
max-width: 560px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
text-align: center;
}
.contacts-map-fallback h3 {
font-family: 'BerlinType', serif;
font-weight: 400;
font-size: 32px;
line-height: 1.1;
color: #36213e;
}
.contacts-map-fallback p,
.contacts-map-fallback a {
font-size: 22px;
line-height: 1.35;
color: #554971;
}
.contacts-map-fallback a {
text-decoration: underline;
text-underline-offset: 3px;
}
.contacts-map-marker {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 999px;
background: rgba(54, 33, 62, 0.94);
color: #ffffff;
box-shadow: 0 14px 30px rgba(54, 33, 62, 0.24);
transform: translate(-50%, -100%);
}
.contacts-map-marker span {
width: 12px;
height: 12px;
border-radius: 999px;
background: #b8f3ff;
box-shadow: 0 0 0 5px rgba(184, 243, 255, 0.16);
}
.contacts-map-marker strong {
font-family: 'Noah', sans-serif;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.02em;
}
@media (max-width: 900px) {
.contacts-page {
padding: 0 24px 96px;
}
.contacts-grid {
gap: 48px;
}
.contacts-card p,
.contacts-card a,
.contacts-map-fallback p,
.contacts-map-fallback a {
font-size: 21px;
}
}
@media (max-width: 700px) {
.contacts-page {
padding-bottom: 72px;
}
.contacts-page__hero > h1 {
font-size: 42px;
}
.contacts-grid {
margin-top: 40px;
grid-template-columns: 1fr;
gap: 36px;
}
.contacts-column {
gap: 28px;
}
.contacts-card h2 {
font-size: 26px;
}
.contacts-card p,
.contacts-card a,
.contacts-map-fallback p,
.contacts-map-fallback a {
font-size: 18px;
}
.contacts-map-section {
margin-top: 56px;
}
.contacts-map {
min-height: 360px;
}
.contacts-map-fallback h3 {
font-size: 26px;
}
.contacts-map-marker {
padding: 8px 12px;
}
footer .footer-container {
grid-template-columns: 1fr !important;
gap: 28px;
}
}

View File

@@ -550,7 +550,7 @@ footer {
padding-right: 24px;
}
footer {
.footer-container {
grid-template-columns: 1fr 1fr;
padding: 40px 24px;
}
@@ -614,4 +614,9 @@ footer {
width: auto;
flex: 0 0 auto;
}
.footer-container {
grid-template-columns: 1fr;
gap: 28px;
}
}

20
static/js/cart.js Normal file
View File

@@ -0,0 +1,20 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-cart-decrement]').forEach(function (button) {
button.addEventListener('click', function () {
const quantity = Number(button.dataset.currentQuantity || '0');
const removeForm = document.getElementById(button.dataset.removeFormId);
const updateForm = document.getElementById(button.dataset.updateFormId);
if (quantity <= 1) {
if (window.confirm('Удалить товар из корзины?') && removeForm) {
removeForm.submit();
}
return;
}
if (updateForm) {
updateForm.submit();
}
});
});
});