Native js realizes the functions of adding and deleting goods and calculating prices in shopping cart

Shopping Cart Functional requirements: ...
Shopping Cart

Functional requirements:

  1. Create shopping cart content based on data;
  2. Realize the function of adding goods to shopping cart;
  3. Increase and decrease the number of goods in shopping cart;
  4. Realize the function of deleting goods in shopping cart;
  5. Shopping cart full selection function;
  6. Calculate the total number of pieces and total price at the bottom;

Take a look at the effect:


Analysis:

  • It is divided into three files: GoodsList.js, which is used to create commodity list; ShoppingCart.js, which is used to create shopping cart content, StepNumber.js, which is used to create components in shopping cart that operate commodity quantity.
  • To distinguish communication between pages.
  • In the case, the shopping cart data is shopCartArr, so any operation related to the data is better to be in the same file to ensure the data synchronization. No matter which file is used for operation, the data will be uniformly distributed to the Html file for operation.
  • Class can be instantiated. It is not recommended to add id attribute to the element in the class file, because when multiple classes are instantiated, multiple identical IDS will appear on the page, which violates the uniqueness of id attribute.
  • When setting properties for input, if the innerHTML method is used, some properties of input may not function, and DOM elements can be used for operation.

The code is attached below:

html file, simulate data, listen to the sending events of each file, and operate on the data:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>shoppingCart</title> </head> <body> <script type="module"> import GoodsList from './js/GoodsList.js'; import ShoppingCart from './js/ShoppingCart.js'; let ADD_GOODS_EVENT="add_goods_event"; let DELETE_EVENT="delete_event"; let STEP_CHANGE_EVENT="step_change_event"; let CHECKED_EVENT="checked_event"; //Listen to add product event document.addEventListener(ADD_GOODS_EVENT,addGoodsEvent); //Listening for deleting product events document.addEventListener(DELETE_EVENT,deleteEvent); //Events triggered by listening for changing the quantity of goods document.addEventListener(STEP_CHANGE_EVENT,stepChangeEvent); //Listen to the click multiple box event document.addEventListener(CHECKED_EVENT,checkedEvent); //Simulated product list data var arr=[ {id:"0001",img:"img01.webp",title:"I don't know fire, ugly orange, ugly orange, 5 jin",desc:"Second pieces 9.9 element",price_desc:"1 Annual lowest price",price_now:"19.9",price_pre:"29.9",sale:"511",href:"https://item.jd.com/21562085787.html",weight:"2.5",state:"1"}, {id:"0006",img:"img06.webp",title:"Mother's Feast (2.0-2.3 Two) 10",desc:"Catch up now",price_desc:"82 Lowest price in history",price_now:"98",price_pre:"189",sale:"35",href:"https://item.jd.com/60387540668.html",weight:"0.3",state:"0"}, {id:"0007",img:"img07.webp",title:"Good taste 100%Pure cocoa butter dark chocolate",desc:"Buy one for free",price_desc:"181 Lowest price in history",price_now:"17.9",price_pre:"29.9",sale:"150",href:"https://item.jd.com/50895183185.html",weight:"0.12",state:"1"}, {id:"0010",img:"img10.webp",title:"Buy one and get one for free g/box",desc:"12 pieces for free",price_desc:"1 Annual lowest price",price_now:"27.8",price_pre:"39.8",sale:"292",href:"https://item.jd.com/25665431802.html",weight:"0.33",state:"1"}, {id:"0011",img:"img11.webp",title:"Wuchang organic rice fragrance 500 g(1 Jin)",desc:"Authentic Wuchang rice",price_desc:"138 Lowest price in history",price_now:"19.9",price_pre:"29.9",sale:"10",href:"https://item.jd.com/56090150750.html",weight:"0.5",state:"1"}, {id:"0012",img:"img12.webp",title:"Hunan hairy fish and dried fish 20 bags of sugar and vinegar",desc:"2 20% off 3 pieces 70% off",price_desc:"230 Lowest price in history",price_now:"13.9",price_pre:"19.9",sale:"103",href:"https://item.jd.com/49202317748.html",weight:"0.15",state:"1"}, ] //Cart list data var shopCartArr=[]; //Create item list setGoodsList(); function setGoodsList(){ let div=document.createElement("div"); div.className="clearfix"; for(let i=0;i<arr.length;i++){ let main = new GoodsList(arr[i],"./img/"); main.appendTo(div); } document.body.appendChild(div); } //Add merchandise function addGoodsEvent(e){ let arr=shopCartArr.filter(item=>item.id===e.list.id); if(arr.length===0){ //If the product is added for the first time, do the following let data={ id:e.list.id, //If the product is not available for sale, checked is set to false checked:e.list.state==1?true:false, img:e.list.img, title:e.list.title, price:Number(e.list.price_now).toFixed(2), num:1, weight:e.list.weight, subWeight:e.list.weight, //When state is 1, it means the goods are available for sale; when state is 0, it means they are not available for sale state:e.list.state, subPrice:Number(e.list.price_now).toFixed(2), href:"https://item.jd.com/21562085787.html" } shopCartArr.push(data); }else{ //If the item is not added for the first time, change the quantity of the item arr[0].num++; arr[0].subWeight=(arr[0].num*arr[0].weight).toFixed(2); arr[0].subPrice=(arr[0].num*arr[0].price).toFixed(2); } //Retrieve cart data setshoppingCart(); } function deleteEvent(e){ //Delete merchandise shopCartArr=shopCartArr.filter(item=>item.id!==e.data.id); //Retrieve cart data setshoppingCart(); } function stepChangeEvent(e){ //Change quantity, subtotal and weight of goods shopCartArr.forEach(item=>{ if(item.id===e.data.id){ item.num=e.num; item.subPrice=(item.num*item.price).toFixed(2); item.subWeight=(item.num*item.weight).toFixed(2); } }) //Retrieve cart data setshoppingCart(); } function checkedEvent(e){ if(e.data==="all"){ //If you click the select all button shopCartArr.forEach(item=>{ //Change the checkbox status in front of the saleable products if(item.state==1) item.checked=e.tag.checked; }) }else{ //If you click the checkbox of a product, only change its checked value shopCartArr.forEach(item=>{ if(item.id===e.data.id) item.checked=e.tag.checked; }) } //Retrieve cart data setshoppingCart(); } var shoppingCart; //Create cart content function setshoppingCart(){ if(shoppingCart){ //Delete the contents of the previous shopping cart and recreate it shoppingCart.elem.remove(); shoppingCart.elem=null; } shoppingCart=new ShoppingCart(shopCartArr); shoppingCart.appendTo("body"); } </script> </body> </html>

ShoppingCart.js file, create shopping cart content, and send events when the shopping cart DOM element is operated:

import Utils from "./Utils.js"; import StepNumber from "./StepNumber.js"; export default class ShoppingCart{ static styles=false; static DELETE_EVENT="delete_event"; static CHECKED_EVENT="checked_event"; constructor(_list){ this.list=_list; this.elem=this.createE(); } createE(){ if(this.elem) return this.elem; //Create outer container let div=Utils.createE("div"); div.className="cart_main"; //Create header content let cartTh=Utils.createE("div",{},{ className:"cart_table_th" }) this.createCartTableTh(cartTh); Utils.appendTo(cartTh,div); //Create cart body content let cartCont=Utils.createE("div",{},{ className:"cart_content" }) this.createShoppingCartCont(cartCont); Utils.appendTo(cartCont,div); //Create bottom total let cartBottom=Utils.createE("div",{},{ className:"cart_bottom" }) this.createCartBottom(cartBottom); Utils.appendTo(cartBottom,div); //Insert style ShoppingCart.setStyle(); //Calculate the total displayed this.totalCount(); return div; } appendTo(parent){ Utils.appendTo(this.elem,parent); } createCartTableTh(parent){ //Create header content let str=`<div><label><span>All election<span></label></div> <div>Commodity information</div> <div>Amount of money</div> <div>Number</div> <div>weight</div> <div>&nbsp;</div> <div>Subtotal</div> <div>operation</div>`; parent.innerHTML=str; let input=Utils.createE("input",{},{ type:"checkbox", //The default value of the check box is set according to the product with state 1 checked:this.list.filter(item=>item.state==1).every(item=>item.checked) }) //Listen to click events for multiple check boxes input.addEventListener("click",e=>this.checkboxClickHandler(e,"all")); Utils.insertBefore(input,parent.firstElementChild.firstElementChild); } createShoppingCartCont(parent){ //Create cart body content this.list.forEach(item => { let ul=Utils.createE("ul",{ //If the item is not available for sale, add a gray background backgroundColor:item.state==0?`#f5f5f5`:"none" },{ className:"item_content clearfix" }); ul.innerHTML=`<li></li> <li> <div><a href="${item.href}" target="_blank"><img src="${item.img}"></a></div> <div><a href="${item.href}" target="_blank">${item.title}</a></div> </li> <li>${item.price}</li> <li></li> <li>${item.subWeight}kg</li> <li>${item.state==1?"In stock":"No goods"}</li> <li>${item.subPrice}</li> <li>delete</li>`; let input=Utils.createE("input",{},{ type:"checkbox", checked:item.checked, //If the product is not available for sale, set the multi selection box to be non clickable disabled:item.state==0?true:false }); Utils.appendTo(input,ul.firstElementChild); //Create step let step=this.createStepNumber(item); Utils.appendTo(step,ul.firstElementChild.nextElementSibling.nextElementSibling.nextElementSibling); //Listen for delete events ul.lastElementChild.addEventListener("click",()=>this.deleGoodsInfo(item)); Utils.appendTo(ul,parent); //Listen to click events for multiple boxes of sellable goods if(item.state==1) input.addEventListener("click",e=>this.checkboxClickHandler(e,item)); }); } createStepNumber(obj){ let stepNumber=new StepNumber(obj); return stepNumber.elem; } createCartBottom(parent){ //Create bottom content let label=Utils.createE("label"); let input=Utils.createE("input",{},{ type:"checkbox", //The default value of the check box is set according to the product with state 1 checked:this.list.filter(item=>item.state==1).every(item=>item.checked) }) //Select all to listen to click events input.addEventListener("click",e=>this.checkboxClickHandler(e,"all")); Utils.appendTo(input,label); Utils.appendTo(Utils.createE("span",{margin:"0px"},{textContent:"All election"}),label); Utils.appendTo(label,parent); //Create delete button let delBtn=Utils.createE("a",{},{ className:"cart_bottom_del", textContent:"delete" }); //Listen for delete events delBtn.addEventListener("click",()=>this.deleGoodsInfo("all")); Utils.appendTo(delBtn,parent); //Create text content this.spanNum=Utils.createE("span"); Utils.appendTo(this.spanNum,parent); this.spanWeight=Utils.createE("span"); Utils.appendTo(this.spanWeight,parent); this.spanPrice=Utils.createE("span"); Utils.appendTo(this.spanPrice,parent); } totalCount(){ //Total, initially set to 0 this.totalPrices=0; this.totalWeight=0; this.totalNum=0; this.list.forEach(item=>{ if(item.checked){ //Calculate the total for the selected item this.totalPrices+=Number(item.subPrice); this.totalWeight+=Number(item.subWeight); this.totalNum+=Number(item.num); } }) this.spanPrice.innerHTML=`Total commodity price<em>¥${this.totalPrices.toFixed(2)}</em>`; this.spanWeight.innerHTML=`Total weight of goods:${this.totalWeight.toFixed(2)}kg`; this.spanNum.innerHTML=`In total:${this.totalNum}Commodity`; } checkboxClickHandler(e,type){ //Multiple selection box throwing event let evt=new Event(ShoppingCart.CHECKED_EVENT); evt.data=type; evt.tag=e.target; document.dispatchEvent(evt); } deleGoodsInfo(type){ //Delete merchandise if(type==="all"){ this.list.forEach(item=>{ //Using recursive loops if(item.checked) this.deleGoodsInfo(item); }) }else{ //Delete goods distribution event let evt=new Event(ShoppingCart.DELETE_EVENT); evt.data=type; document.dispatchEvent(evt); } } static setStyle(){ if(ShoppingCart.styles) return; ShoppingCart.styles=true; Utils.insertCss(".cart_main",{ width:"980px", fontSize:"12px", color: "#696969", background:"#fff", margin:"20px auto 0px", textAlign:"center", userSelect:"none" }) Utils.insertCss(".cart_table_th",{ height: "34px", border: "1px solid #dbdbdb", borderBottom:"none", height: "34px", lineHeight: "34px", background: "#f3f3f3", color: "#666" }) Utils.insertCss(".cart_table_th .th",{ float: "left", textAlign: "center" }) Utils.insertCss(".cart_table_th .th_chk",{ width:"58px" }) Utils.insertCss(".cart_checkbox",{ width: "13px", height: "13px", }) Utils.insertCss(".th_item",{ width:"290px" }) Utils.insertCss(".th_price,.td_price",{ width:"100px" }) Utils.insertCss(".th_amount,.td_amount",{ width:"115px" }) Utils.insertCss(".th_weight,.td_weight",{ width:"70px" }) Utils.insertCss(".th_location,.td_location",{ width:"137px" }) Utils.insertCss(".th_sum,.td_sum",{ width:"115px" }) Utils.insertCss(".th_op,.td_op",{ width:"89px", cursor:"pointer" }) Utils.insertCss(".cart_content",{ border: "1px solid #dbdbdb", backgroundColor: "#f3f3f3" }) Utils.insertCss(".item_content",{ listStyle:"none", padding:"0px", margin:"0px", height:"82px", backgroundColor:"#fff", borderBottom:"1px solid #dbdbdb" }) Utils.insertCss(".item_content li",{ paddingTop:"16px", float:"left" }) Utils.insertCss(".item_content .td_chk",{ width: "44px", paddingTop:"32px", }) Utils.insertCss(".td_item",{ width:"308px", textAlign:"left" }) Utils.insertCss(".item_pic",{ width:"60px", height:"60px", float:"left" }) Utils.insertCss(".item_pic img",{ width:"60px", height:"60px" }) Utils.insertCss(".item_info",{ float:"left", padding:"12px 15px 0 11px", width: "220px" }) Utils.insertCss(".item_info a",{ color:"#666", textDecoration:"none" }) Utils.insertCss(".cart_bottom",{ height:"62px", lineHeight:"62px", backgroundColor:"#e6e6e6", marginTop:"20px", textAlign:"left", fontSize:"14px" }) Utils.insertCss(".cart_bottom label,.cart_bottom_del,.cart_bottom span",{ margin:"0px 20px" }) Utils.insertCss(".cart_bottom_del",{ color:"#696969", textDecoration:"none", cursor:"pointer" }) Utils.insertCss(".cart_bottom em",{ color:"#e71a0f", fontStyle:"normal", fontSize:"18px" }) } }

StepNumber.js file, click the left and right buttons to change the quantity:

import Utils from './Utils.js'; export default class StepNumber{ static styles=false; static STEP_CHANGE_EVENT="step_change_event"; ids; constructor(_data){ this.data=_data; this.num=_data.num; this.elem=this.createE(); } createE(){ if(this.elem) return this.elem; //Create outer container let div=Utils.createE("div"); div.className="item_amount clearfix"; //Left button let reduceBtn=Utils.createE("a",{},{ className:"btn_reduce", textContent:"-" }) //Input box let input=Utils.createE("input",{},{ className:"amount_inp", type:"text", value:this.num, //If state is 0, set input not to be inputted disabled:this.data.state==1?false:true }) //Right button let addBtn=Utils.createE("a",{},{ className:"btn_add", textContent:"+" }) Utils.appendTo(reduceBtn,div); Utils.appendTo(input,div); Utils.appendTo(addBtn,div); //Set style StepNumber.setStyles(); //Monitor click events for products with state 1 if(this.data.state==1) div.addEventListener("click",e=>this.clickHandler(e)); return div; } appendTo(parent){ Utils.appendTo(this.elem,parent); } clickHandler(e){ e.preventDefault(); let target=e.target; //If the clicked element is not a or input, directly jump out if(target.nodeName!=="A"&&target.nodeName!=="INPUT") return; if(Utils.hasClass(target,"btn_reduce")){ //Click the button on the left this.num--; this.setStep(target.nextElementSibling); }else if(Utils.hasClass(target,"btn_add")){ //Click the button on the right this.num++; this.setStep(target.previousElementSibling); }else if(target.nodeName==="INPUT"){ //Click input to listen for input events target.addEventListener("input",e=>this.inputHandler(e)); } } inputHandler(e){ //Use throttling to improve efficiency if(this.ids) return; this.ids=setTimeout(()=>{ clearTimeout(this.ids); this.ids=0; this.setInputsValue(e.target); },500); } setInputsValue(target){ let value=target.value; //Disable non numeric input value=value.replace(/\D/g,""); //If the value of input is null, make it equal to 1 if(value.length===0) value="1"; this.num=value; this.setStep(target); } setStep(obj){ //Set value if(this.num<=1) this.num=1; if(this.num>=999) this.num=999; obj.value=this.num; //Throwing incident let evt=new Event(StepNumber.STEP_CHANGE_EVENT); evt.num=this.num; evt.data=this.data; document.dispatchEvent(evt); } static setStyles(){ if(StepNumber.styles) return; StepNumber.styles=true; Utils.insertCss(".item_amount",{ width:"90px", height:"26px" }) Utils.insertCss(".btn_reduce,.btn_add",{ display:"block", float:"left", width:"21px", height:"21px", lineHeight:"21px", textAlign:"center", border:"1px solid #ccc", color:"#696969", textDecoration:"none" }) Utils.insertCss(".amount_inp",{ float:"left", width:"32px", height:"19px", lineHeight:"20px", textAlign:"center", margin:"0px 4px", padding:"0px" }) } }

GoodsList.js file to create a product list:

import Utils from './Utils.js'; export default class GoodsList { static styles=false; static ADD_GOODS_EVENT="add_goods_event"; constructor(_list, basePath) { //The path of splicing pictures if (basePath) _list.img = basePath + _list.img; this.list=_list; this.elem = this.createElem(_list); } createElem(_list) { if(this.elem) return this.elem; let div = Utils.createE("div"); div.className = "goodsContainer"; //Page structure div.innerHTML = `<a href="${_list.href}" target="_blank"> <img src="${_list.img}" title="${_list.title}"> <span>${_list.title}</span> <span>${_list.desc}</span></a> <div><div> <div>${_list.price_desc}</div><div><span>¥</span>${_list.price_now}</div> <del>¥${_list.price_pre}</del></div> <div><p>Already sold<span>${_list.sale}</span>piece</p></div></div>`; let a=Utils.createE("a",{},{ className:"saleBtn", textContent:"add to cart" }) //Listen to add product event a.addEventListener("click",e=>this.addShoppingCartHandler(e)) Utils.appendTo(a,div.lastElementChild.lastElementChild) //Writing style GoodsList.setStyles(); return div; } appendTo(parent) { Utils.appendTo(this.elem, parent); } addShoppingCartHandler(e){ //Throwing incident let evt=new Event(GoodsList.ADD_GOODS_EVENT); evt.list=this.list; document.dispatchEvent(evt); } static setStyles() { if(GoodsList.styles) return; GoodsList.styles=true; Utils.insertCss("body", { background: "#f5f5f5" }) Utils.insertCss("body,div,p", { margin: "0px", padding: "0px" }) Utils.insertCss(".goodsContainer", { width: "226px", height: "350px", margin: "10px 0px 0px 10px", background: "#fff", float: "left" }) Utils.insertCss(".goodsInfo", { width:"220px", paddingLeft:"16px", cursor:"pointer", textDecoration:"none" }) Utils.insertCss(".goodsImg", { width: "150px", height: "150px", margin: "20px 0px 0px 25px" }) Utils.insertCss(".goodsImg:hover", { position:"relative", top:"-7px", }) Utils.insertCss(".goodsTit", { fontSize: "14px", color: "#333333", margin: "0px 16px 10px", fontWeight: "bold", textAlign: "left", lineHeight: "20px", height: "20px", whiteSpace: "nowrap", textOverflow: "ellipsis", overflow: "hidden", display:"block" }) Utils.insertCss(".goodsDesc", { margin: "0px 0px 10px 16px", fontSize: "14px", color: "#e01222", display:"block" }) Utils.insertCss(".goodsPrice", { paddingLeft:"16px", paddingTop:"13px", borderTop: "1px solid #f3f3f3" }) Utils.insertCss(".leftCon", { float: "left" }) Utils.insertCss(".priceDesc", { padding: "0 8px", height: "16px", lineHeight: "16px", backgroundColor: "#e6e6e6", fontSize: "10px", color: "#999", borderRadius: "2px", position: "relative", marginBottom:"10px" }) Utils.insertCss(".priceDesc:after", { content: "\".\"", position: "absolute", left: "0", right: "0", bottom: "-6px", margin: "0 auto", width: "0", height: "0", borderWidth: "3px", borderStyle: "solid dashed dashed", borderColor: "#e6e6e6 transparent transparent", pointerEvents: "none", }) Utils.insertCss(".priceNow", { fontSize: "24px", color: "#e01222", marginRight: "2px", lineHeight: "1", minWidth: "50px", marginBottom:"3px" }) Utils.insertCss(".priceNow span", { fontSize: "14px" }) Utils.insertCss(".pricePre", { lineHeight: "1.2", color: "#999", fontSize: "18px", }) Utils.insertCss(".rightCon", { float: "right" }) Utils.insertCss(".sale", { height: "18px", fontSize: "12px", color: "#999999", textAlign: "right", paddingRight: "10px", }) Utils.insertCss(".sale span", { fontSize: "18px", color: "#df0021" }) Utils.insertCss(".saleBtn", { marginTop: "15px", color: "#fff", height: "38px", lineHeight: "38px", fontSize: "16px", display: "block", width: "90px", textAlign: "center", background: "#df0021", textDecoration:"none", cursor:"pointer" }) Utils.insertCss(".clearfix::after", { content: "\".\"", display: "block", overflow: "hidden", visibility: "hidden", height: "0px", clear: "both" }) } }

Utils.js is a toolkit file:

export default class Utils{ static createE(elem,style,prep){ elem=document.createElement(elem); if(style) for(let prop in style) elem.style[prop]=style[prop]; if(prep) for(let prop in prep) elem[prop]=prep[prop]; return elem; } static appendTo(elem,parent){ if (parent.constructor === String) parent = document.querySelector(parent); parent.appendChild(elem); } static insertBefore(elem,parent){ if(parent.constructor === String) parent=document.querySelector(parent); parent.insertBefore(elem,parent.firstElementChild); } static randomNum(min,max){ return Math.floor(Math.random*(max-min)+min); } static randomColor(alpha){ alpha=alpha||Math.random().toFixed(1); if(isNaN(alpha)) alpha=1; if(alpha>1) alpha=1; if(alpha<0) alpha=0; let col="rgba("; for(let i=0;i<3;i++){ col+=Utils.randomNum(0,256)+","; } col+=alpha+")"; return col; } static insertCss(select,styles){ if(document.styleSheets.length===0){ let styleS=Utils.createE("style"); Utils.appendTo(styleS,document.head); } let styleSheet=document.styleSheets[document.styleSheets.length-1]; let str=select+"{"; for(var prop in styles){ str+=prop.replace(/[A-Z]/g,function(item){ return "-"+item.toLocaleLowerCase(); })+":"+styles[prop]+";"; } str+="}" styleSheet.insertRule(str,styleSheet.cssRules.length); } static addClass(elem,className){ let arr=(elem.className+" "+className).match(/\S+/g); arr=arr.filter((item,index)=>arr.indexOf(item,index+1)<0) elem.className=arr.join(" "); } static removeClass(elem,className){ if(!elem.className) return; let arr=elem.className.match(/\S+/g); let arr1=className.match(/\S+/g); arr1.forEach(item=>{ arr=arr.filter(t=>t!==item) }) elem.className=arr.join(" "); } static hasClass(elem,className){ if(!elem.className) return false; let arr=elem.className.match(/\S+/g); let arr1=className.match(/\S+/g); let res; arr1.forEach(item=>{ res= arr.some(it=>it===item) }) return res; } static loadImg({list,basePath,callback}){ if(!list || list.length===0) return; if(basePath) list=list.map(item=>basePath+item); let img=Utils.createE("img"); img.data={ list:list, callback:callback, resultList:[], num:0 } img.addEventListener("load",Utils.loadImgHandler); img.src=list[img.data.num]; } static loadImgHandler(e){ let data=e.currentTarget.data; data.resultList.push(e.currentTarget.cloneNode(false)); data.num++; if(data.num>data.list.length-1){ e.currentTarget.removeEventListener("load",Utils.loadImgHandler); data.callback(data.resultList); data=null; return; } e.currentTarget.src=data.list[data.num]; } }
Dandelion sprouts 66 original articles published, 36 praised, 20000 visitors+ Private letter follow

5 February 2020, 10:13 | Views: 2372

Add new comment

For adding a comment, please log in
or create account

0 comments