feat: add favorites feature for users
All checks were successful
Build and Push Docker Image / build (push) Successful in 51s

- Implemented `Favorite` model with unique user-item constraint and timestamped records
- Added `favorites:index` and `favorites:toggle` views with AJAX support for toggling favorites
- Built templates and front-end assets for the favorites page and integration with item grids
- Extended header with favorites link and dynamic counter
- Added tests covering models, views, and templates for the favorites functionality
- Updated admin to enable management of favorites via Django admin interface
This commit is contained in:
2026-05-10 20:06:12 +02:00
parent 812065a34b
commit 29d8302a4f
30 changed files with 1042 additions and 65 deletions

View File

@@ -74,9 +74,8 @@ aside {
transform: translateY(-2px);
}
.item-card-link {
.item-card-media-link {
display: block;
height: 100%;
}
.item-card-image {
@@ -106,11 +105,60 @@ aside {
padding: 18px;
}
.item-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.item-card-category {
color: #63768d;
font-size: 15px;
line-height: 1.2;
margin-bottom: 8px;
min-width: 0;
}
.item-card-favorite-form {
flex: 0 0 auto;
}
.item-card-favorite-button {
width: 42px;
height: 42px;
border: 0;
background: transparent;
padding: 0;
color: #63768d;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: color 0.2s, opacity 0.2s, transform 0.2s;
}
.item-card-favorite-button:hover {
color: #36213e;
opacity: 0.82;
transform: translateY(-1px);
}
.item-card-favorite-button svg {
width: 18px;
height: 18px;
}
.item-card-favorite-button.is-active {
color: #c54c5d;
}
.item-card-favorite-button--login {
text-decoration: none;
}
.item-card-title-link {
display: block;
}
.item-card-title {
@@ -397,6 +445,15 @@ aside {
flex-direction: column;
}
.item-card-header {
gap: 10px;
}
.item-card-favorite-button {
width: 38px;
height: 38px;
}
.product-gallery__thumbs {
gap: 12px;
}

0
static/css/contacts.css Normal file
View File

115
static/css/favorites.css Normal file
View File

@@ -0,0 +1,115 @@
.favorites-page {
margin-top: 18px;
margin-bottom: 80px;
}
.favorites-hero {
margin-bottom: 28px;
padding: 30px 32px;
border: 1px solid rgba(54, 33, 62, 0.12);
background:
/*radial-gradient(circle at top right, rgba(184, 243, 255, 0.55) 0, rgba(184, 243, 255, 0) 34%),*/
linear-gradient(180deg, rgba(99, 118, 141, 0.07) 0%, rgba(99, 118, 141, 0.12) 100%);
}
.favorites-kicker {
font-size: 16px;
line-height: 1.2;
color: #63768d;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 12px;
}
.favorites-hero__row {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
}
.favorites-title {
font-size: 50px;
line-height: 1.05;
color: #36213e;
font-weight: 400;
}
.favorites-description {
max-width: 680px;
margin-top: 14px;
color: #554971;
font-size: 18px;
line-height: 1.45;
}
.favorites-counter {
flex: 0 0 auto;
min-width: 130px;
padding: 16px 20px;
border: 1px solid rgba(54, 33, 62, 0.12);
background: rgba(255, 255, 255, 0.72);
text-align: center;
}
.favorites-counter__value {
display: block;
color: #36213e;
font-family: "BerlinType", serif;
font-size: 44px;
line-height: 1;
}
.favorites-counter__label {
display: block;
margin-top: 6px;
color: #63768d;
font-size: 15px;
line-height: 1.2;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.favorites-list .items-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
@media (max-width: 900px) {
.favorites-page {
padding-left: 24px;
padding-right: 24px;
margin-bottom: 64px;
}
.favorites-hero {
padding: 24px 20px;
}
.favorites-title {
font-size: 38px;
}
.favorites-description {
font-size: 16px;
}
.favorites-hero__row {
align-items: flex-start;
flex-direction: column;
}
}
@media (max-width: 520px) {
.favorites-page {
margin-top: 12px;
}
.favorites-title {
font-size: 30px;
}
.favorites-counter {
width: 100%;
}
}

View File

@@ -627,7 +627,7 @@
line-height: 24px;
text-align: left;
color: #b8f3ff;
color: #ffffff;
}
.faq-body {

View File

@@ -10,7 +10,6 @@
padding-top: 60px;
padding-bottom: 60px;
background:
radial-gradient(circle at top right, rgba(184, 243, 255, 0.45) 0, rgba(184, 243, 255, 0) 26%),
linear-gradient(180deg, rgba(99, 118, 141, 0.06) 0%, rgba(99, 118, 141, 0.12) 100%);
}
@@ -59,7 +58,7 @@
margin-bottom: 18px;
font-size: 20px;
line-height: 1.2;
color: #b8f3ff;
color: #ffffff;
}
.auth-title {

View File

@@ -240,16 +240,134 @@ header {
margin: 0 24px;
}
.search-wrap select {
border: none;
.search-select-wrap {
position: relative;
flex: 0 0 154px;
width: 154px;
border-right: 1px solid var(--dark);
padding: 10px 14px;
min-width: 0;
box-sizing: border-box;
}
.search-category-dropdown {
position: relative;
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.search-category-trigger {
width: 100%;
min-width: 0;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
padding: 10px 34px 10px 14px;
border: 0;
background: transparent;
color: var(--mid);
font-family: 'Inter', sans-serif;
font-size: 13px;
color: var(--mid);
background: transparent;
outline: none;
line-height: 1.2;
text-align: left;
cursor: pointer;
box-sizing: border-box;
}
.search-category-trigger:focus-visible {
outline: none;
}
.search-category-label {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.search-select-arrow {
position: absolute;
top: 50%;
right: 12px;
transform: translateY(-50%);
color: var(--mid);
pointer-events: none;
transition: transform .2s ease;
}
.search-category-dropdown.is-open .search-select-arrow {
transform: translateY(-50%) rotate(180deg);
}
.search-category-menu {
display: none;
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 30;
width: max(100%, 290px);
max-height: 340px;
overflow: auto;
padding: 8px 0 10px;
border: 1px solid rgba(54, 33, 62, .16);
background: #ffffff;
box-shadow: 0 16px 36px rgba(54, 33, 62, .14);
}
.search-category-dropdown.is-open .search-category-menu {
display: block;
}
.search-category-option {
display: block;
width: 100%;
border: 0;
background: transparent;
padding: 0;
cursor: pointer;
text-align: left;
font-family: 'Inter', sans-serif;
transition: background-color .15s ease, color .15s ease;
}
.search-category-option:hover,
.search-category-option:focus-visible {
background: rgba(99, 118, 141, 0.08);
outline: none;
}
.search-category-option--all {
padding: 10px 14px 11px;
font-size: 13px;
line-height: 1.25;
font-weight: 600;
color: var(--mid);
}
.search-category-group {
padding-top: 6px;
}
.search-category-sublist {
display: flex;
flex-direction: column;
}
.search-category-option--category {
padding: 9px 14px 9px 22px;
font-size: 13px;
line-height: 1.25;
font-weight: 600;
color: var(--dark);
}
.search-category-option--subcategory {
padding: 7px 14px 7px 34px;
font-size: 12px;
line-height: 1.25;
color: var(--mid);
}
.search-wrap input {
@@ -393,7 +511,7 @@ footer {
}
.footer-logo {
font-family: 'Playfair Display', serif;
font-family: 'BerlinType', serif;
font-size: 30px;
color: rgba(184, 243, 255, .85);
margin-bottom: 8px;
@@ -459,11 +577,15 @@ footer {
margin: 0;
}
.search-wrap select {
.search-select-wrap {
flex: 0 0 132px;
max-width: none;
width: 132px;
}
.search-category-trigger {
padding-left: 8px;
padding-right: 8px;
padding-right: 28px;
}
.header-icons {