mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 15:51:19 +00:00
[Eboutic] Fix double quote issue & improved user experience on small screen (#522)
* Fix #511 Regex issue with escaped double quotes * Fix basket being when reloading the page (when cookie != "") + Added JSDoc + Cleaned some code * Fix #509 Improved user experience on small screens * Fix css class not being added back when reloading page * CSS Fixes (see description) + Fixed overlaping item title with the cart emoji on small screen + Fixed minimal size of the basket on small screen (full width) * Added darkened background circle to items with no image * Fix issue were the basket could be None * Edited CSS to have bette img ratio & the 🛒 icon Adapt, Improve, Overcome * Moved basket down on small screen size
This commit is contained in:
parent
639197f4c8
commit
26c94c9ec6
@ -99,8 +99,11 @@ class BasketForm:
|
|||||||
- all the ids refer to products the user is allowed to buy
|
- all the ids refer to products the user is allowed to buy
|
||||||
- all the quantities are positive integers
|
- all the quantities are positive integers
|
||||||
"""
|
"""
|
||||||
basket = unquote(self.cookies.get("basket_items", None))
|
# replace escaped double quotes by single quotes, as the RegEx used to check the json
|
||||||
if basket is None or basket in ("[]", ""):
|
# does not support escaped double quotes
|
||||||
|
basket = unquote(self.cookies.get("basket_items", "")).replace('\\"', "'")
|
||||||
|
|
||||||
|
if basket in ("[]", ""):
|
||||||
self.error_messages.add(_("You have no basket."))
|
self.error_messages.add(_("You have no basket."))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
@media screen and (max-width: 765px) {
|
@media screen and (max-width: 765px) {
|
||||||
#eboutic {
|
#eboutic {
|
||||||
flex-direction: column;
|
flex-direction: column-reverse;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
row-gap: 20px;
|
row-gap: 20px;
|
||||||
@ -38,6 +38,7 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
#basket {
|
#basket {
|
||||||
|
width: -webkit-fill-available;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,23 +53,39 @@
|
|||||||
margin-bottom: 10px
|
margin-bottom: 10px
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#eboutic .item-row {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
#eboutic .item-name {
|
#eboutic .item-name {
|
||||||
flex: 1;
|
word-break: break-word;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#eboutic .item-price, #eboutic .item-quantity {
|
#eboutic .fa-plus,
|
||||||
width: 65px;
|
#eboutic .fa-minus {
|
||||||
}
|
cursor: pointer;
|
||||||
|
background-color: #354a5f;
|
||||||
#eboutic .item-quantity {
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 10px;
|
||||||
|
width: 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#eboutic .fa-plus, #eboutic .fa-minus {
|
#eboutic .item-quantity {
|
||||||
cursor: pointer;
|
min-width: 65px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#eboutic .item-price {
|
#eboutic .item-price {
|
||||||
|
min-width: 65px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,44 +108,65 @@
|
|||||||
column-gap: 15px;
|
column-gap: 15px;
|
||||||
row-gap: 15px;
|
row-gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 765px) {
|
|
||||||
#eboutic #catalog {
|
|
||||||
row-gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#eboutic section {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#eboutic .product-group {
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#eboutic .product-button {
|
#eboutic .product-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 120px;
|
min-height: 180px;
|
||||||
max-width: 150px;
|
height: fit-content;
|
||||||
padding: 10px;
|
width: 150px;
|
||||||
|
padding: 15px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px;
|
box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
row-gap: 5px;
|
row-gap: 5px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-button.selected {
|
||||||
|
animation: bg-in-out 1s ease;
|
||||||
|
background-color: rgb(216, 236, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-button.selected::after {
|
||||||
|
content: "🛒";
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0px 0px 12px 2px rgb(0 0 0 / 14%);
|
||||||
|
background-color: white;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#eboutic .product-button:active {
|
#eboutic .product-button:active {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#eboutic .product-button img, #eboutic .product-button .fa {
|
#eboutic .product-image {
|
||||||
margin: 0;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 70px;
|
||||||
|
max-height: 70px;
|
||||||
|
object-fit: contain;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 40px;
|
line-height: 70px;
|
||||||
line-height: 40px;
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic i.product-image {
|
||||||
|
background-color: rgba(173, 173, 173, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-description h4 {
|
||||||
|
font-size: .75em;
|
||||||
|
word-break: break-word;
|
||||||
|
margin: 0 0 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#eboutic .product-button p {
|
#eboutic .product-button p {
|
||||||
@ -152,8 +190,9 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: white;
|
color: white;
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
padding: 5px 10px;
|
padding: 10px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#eboutic .catalog-buttons .validate {
|
#eboutic .catalog-buttons .validate {
|
||||||
background-color: #354a5f;
|
background-color: #354a5f;
|
||||||
}
|
}
|
||||||
@ -174,4 +213,51 @@
|
|||||||
|
|
||||||
#eboutic .catalog-buttons form {
|
#eboutic .catalog-buttons form {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 765px) {
|
||||||
|
#eboutic #catalog {
|
||||||
|
row-gap: 15px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic section {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-group {
|
||||||
|
justify-content: space-around;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-group .product-button {
|
||||||
|
min-height: 100px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-group .product-description {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-description h4 {
|
||||||
|
text-align: left;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#eboutic .product-image {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
max-width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bg-in-out {
|
||||||
|
0% { background-color: white; }
|
||||||
|
100% { background-color: rgb(216, 236, 255); }
|
||||||
}
|
}
|
@ -1,76 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {Object} BasketItem An item in the basket
|
||||||
|
* @property {number} id The id of the product
|
||||||
|
* @property {string} name The name of the product
|
||||||
|
* @property {number} quantity The quantity of the product
|
||||||
|
* @property {number} unit_price The unit price of the product
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASKET_ITEMS_COOKIE_NAME = "basket_items";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for a cookie by name
|
||||||
|
* @param {string} name Name of the cookie to get
|
||||||
|
* @returns {string|null|undefined} the value of the cookie or null if it does not exist, undefined if not found
|
||||||
|
*/
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
let cookieValue = null;
|
if (!document.cookie || document.cookie.length === 0) return null;
|
||||||
if (document.cookie && document.cookie !== '') {
|
|
||||||
const cookies = document.cookie.split(';');
|
let found = document.cookie
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
.split(';')
|
||||||
const cookie = cookies[i].trim();
|
.map(c => c.trim())
|
||||||
// Does this cookie string begin with the name we want?
|
.find(c => c.startsWith(name + '='));
|
||||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
return found === undefined ? undefined : decodeURIComponent(found.split('=')[1]);
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cookieValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the basket items from the associated cookie
|
||||||
|
* @returns {BasketItem[]|[]} the items in the basket
|
||||||
|
*/
|
||||||
function get_starting_items() {
|
function get_starting_items() {
|
||||||
const cookie = getCookie("basket_items")
|
const cookie = getCookie(BASKET_ITEMS_COOKIE_NAME);
|
||||||
|
let output = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// django cookie backend does an utter mess on non-trivial data types
|
// Django cookie backend does an utter mess on non-trivial data types
|
||||||
// so we must perform a conversion of our own
|
// so we must perform a conversion of our own
|
||||||
const biscuit = JSON.parse(JSON.parse(cookie.replace(/\\054/g, ',')));
|
const biscuit = JSON.parse(cookie.replace(/\\054/g, ','));
|
||||||
if (Array.isArray(biscuit)) {
|
output = Array.isArray(biscuit) ? biscuit : [];
|
||||||
return biscuit;
|
|
||||||
}
|
} catch (e) {}
|
||||||
return [];
|
|
||||||
} catch (e) {
|
output.forEach(item => {
|
||||||
return [];
|
let el = document.getElementById(item.id);
|
||||||
}
|
el.classList.add("selected");
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
Alpine.data('basket', () => ({
|
Alpine.data('basket', () => ({
|
||||||
items: get_starting_items(),
|
items: get_starting_items(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total price of the basket
|
||||||
|
* @returns {number} The total price of the basket
|
||||||
|
*/
|
||||||
get_total() {
|
get_total() {
|
||||||
let total = 0;
|
return this.items
|
||||||
for (const item of this.items) {
|
.reduce((acc, item) => acc + item["quantity"] * item["unit_price"], 0);
|
||||||
total += item["quantity"] * item["unit_price"];
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add 1 to the quantity of an item in the basket
|
||||||
|
* @param {BasketItem} item
|
||||||
|
*/
|
||||||
add(item) {
|
add(item) {
|
||||||
item.quantity++;
|
item.quantity++;
|
||||||
this.edit_cookies()
|
this.set_cookies();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove 1 to the quantity of an item in the basket
|
||||||
|
* @param {BasketItem} item_id
|
||||||
|
*/
|
||||||
remove(item_id) {
|
remove(item_id) {
|
||||||
const index = this.items.findIndex(e => e.id === item_id);
|
const index = this.items.findIndex(e => e.id === item_id);
|
||||||
|
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
this.items[index].quantity -= 1;
|
this.items[index].quantity -= 1;
|
||||||
|
|
||||||
if (this.items[index].quantity === 0) {
|
if (this.items[index].quantity === 0) {
|
||||||
|
let el = document.getElementById(this.items[index].id);
|
||||||
|
el.classList.remove("selected");
|
||||||
|
|
||||||
this.items = this.items.filter((e) => e.id !== this.items[index].id);
|
this.items = this.items.filter((e) => e.id !== this.items[index].id);
|
||||||
}
|
}
|
||||||
this.edit_cookies();
|
this.set_cookies();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all the items from the basket & cleans the catalog CSS classes
|
||||||
|
*/
|
||||||
clear_basket() {
|
clear_basket() {
|
||||||
this.items = []
|
// We remove the class "selected" from all the items in the catalog
|
||||||
this.edit_cookies();
|
this.items.forEach(item => {
|
||||||
|
let el = document.getElementById(item.id);
|
||||||
|
el.classList.remove("selected");
|
||||||
|
})
|
||||||
|
|
||||||
|
this.items = [];
|
||||||
|
this.set_cookies();
|
||||||
},
|
},
|
||||||
|
|
||||||
edit_cookies() {
|
/**
|
||||||
// a cookie survives an hour
|
* Set the cookie in the browser with the basket items
|
||||||
document.cookie = "basket_items=" + encodeURIComponent(JSON.stringify(this.items)) + ";Max-Age=3600";
|
* ! the cookie survives an hour
|
||||||
|
*/
|
||||||
|
set_cookies() {
|
||||||
|
if (this.items.length === 0) document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=;Max-Age=0`;
|
||||||
|
else document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(this.items))};Max-Age=3600`;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an item in the basket if it was not already in
|
* Create an item in the basket if it was not already in
|
||||||
* @param id : int the id of the product to add
|
* @param {number} id The id of the product to add
|
||||||
* @param name : String the name of the product
|
* @param {string} name The name of the product
|
||||||
* @param price : number the unit price of the product
|
* @param {number} price The unit price of the product
|
||||||
|
* @returns {BasketItem} The created item
|
||||||
*/
|
*/
|
||||||
create_item(id, name, price) {
|
create_item(id, name, price) {
|
||||||
let new_item = {
|
let new_item = {
|
||||||
@ -79,25 +126,31 @@ document.addEventListener('alpine:init', () => {
|
|||||||
quantity: 0,
|
quantity: 0,
|
||||||
unit_price: price
|
unit_price: price
|
||||||
};
|
};
|
||||||
|
|
||||||
this.items.push(new_item);
|
this.items.push(new_item);
|
||||||
this.add(new_item);
|
this.add(new_item);
|
||||||
|
|
||||||
|
return new_item;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add an item to the basket.
|
* Add an item to the basket.
|
||||||
* This is called when the user click
|
* This is called when the user click on a button in the catalog
|
||||||
* on a button in the catalog (left side of the page)
|
* @param {number} id The id of the product to add
|
||||||
* @param id : int the id of the product to add
|
* @param {string} name The name of the product
|
||||||
* @param name : String the name of the product
|
* @param {number} price The unit price of the product
|
||||||
* @param price : number the unit price of the product
|
|
||||||
*/
|
*/
|
||||||
add_from_catalog(id, name, price) {
|
add_from_catalog(id, name, price) {
|
||||||
const item = this.items.find(e => e.id === id)
|
let item = this.items.find(e => e.id === id)
|
||||||
if (item === undefined) {
|
|
||||||
this.create_item(id, name, price);
|
// if the item is not in the basket, we create it
|
||||||
} else {
|
// else we add + 1 to it
|
||||||
// the user clicked on an item which is already in the basket
|
if (item === undefined) item = this.create_item(id, name, price);
|
||||||
this.add(item);
|
else this.add(item);
|
||||||
|
|
||||||
|
if (item.quantity > 0) {
|
||||||
|
let el = document.getElementById(item.id);
|
||||||
|
el.classList.add("selected");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
@ -46,12 +46,12 @@
|
|||||||
</li>
|
</li>
|
||||||
<template x-for="item in items" :key="item.id">
|
<template x-for="item in items" :key="item.id">
|
||||||
<li class="item-row" x-show="item.quantity > 0">
|
<li class="item-row" x-show="item.quantity > 0">
|
||||||
<span class="item-name" x-text="item.name"></span>
|
|
||||||
<div class="item-quantity">
|
<div class="item-quantity">
|
||||||
<i class="fa fa-minus fa-xs" @click="remove(item.id)"></i>
|
<i class="fa fa-minus fa-xs" @click="remove(item.id)"></i>
|
||||||
<span x-text="item.quantity"></span>
|
<span x-text="item.quantity"></span>
|
||||||
<i class="fa fa-plus" @click="add(item)"></i>
|
<i class="fa fa-plus" @click="add(item)"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="item-name" x-text="item.name"></span>
|
||||||
<span class="item-price" x-text="(item.unit_price * item.quantity).toFixed(2) + ' €'"></span>
|
<span class="item-price" x-text="(item.unit_price * item.quantity).toFixed(2) + ' €'"></span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@ -104,16 +104,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="product-group">
|
<div class="product-group">
|
||||||
{% for p in items %}
|
{% for p in items %}
|
||||||
<button class="product-button"
|
<button id="{{ p.id }}" class="product-button" @click='add_from_catalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'>
|
||||||
@click='add_from_catalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'>
|
|
||||||
{% if p.icon %}
|
{% if p.icon %}
|
||||||
<img src="{{ p.icon.url }}" alt="image de {{ p.name }}"
|
<img class="product-image" src="{{ p.icon.url }}" alt="image de {{ p.name }}">
|
||||||
width="40px" height="40px">
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="fa fa-2x fa-picture-o"></i>
|
<i class="fa fa-2x fa-picture-o product-image" ></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><strong>{{ p.name }}</strong></p>
|
<div class="product-description">
|
||||||
<p>{{ p.selling_price }} €</p>
|
<h4>{{ p.name }}</strong></h4>
|
||||||
|
<p>{{ p.selling_price }} €</p>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user