diff --git a/04_browser-api/unterricht/tag30/01_website-refactored/backend/data/products.json b/04_browser-api/unterricht/tag30/01_website-refactored/backend/data/products.json index 7f443d1..83ff8c1 100644 --- a/04_browser-api/unterricht/tag30/01_website-refactored/backend/data/products.json +++ b/04_browser-api/unterricht/tag30/01_website-refactored/backend/data/products.json @@ -1,22 +1,22 @@ [ - { - "_id": "174a2a19-26eb-475e-af47-4f46d2ab62ec", - "position": 1, - "name": "3Doodler 3D Printing Pen", - "price": 29.99 - }, { "_id": "ba337ef0-fead-48c9-a210-16514fce54cd", - "position": 2, + "position": 1, "name": "Powerstation 5- E. Maximus Chargus", "price": 44.95 }, { "_id": "61c25758-5d24-4a1d-baf3-bb8e11d8c9a2", - "position": 3, + "position": 2, "name": "8-Bit Legendary Hero Heat-Change Mug", "price": 6.99 }, + { + "_id": "174a2a19-26eb-475e-af47-4f46d2ab62ec", + "position": 3, + "name": "3Doodler 3D Printing Pen", + "price": 29.99 + }, { "_id": "2b8eae22-50ad-43a6-91f5-5ca5d62976fa", "position": 4, diff --git a/04_browser-api/unterricht/tag30/01_website-refactored/backend/models/ProductModel.js b/04_browser-api/unterricht/tag30/01_website-refactored/backend/models/ProductModel.js index 798e30e..1b6a56e 100644 --- a/04_browser-api/unterricht/tag30/01_website-refactored/backend/models/ProductModel.js +++ b/04_browser-api/unterricht/tag30/01_website-refactored/backend/models/ProductModel.js @@ -86,6 +86,29 @@ const ProductModel = () => { return true; }; + // [{_id, position},{_id, position} ] + + const updatePosition = (positions = []) => { + // Guard + if (!Array.isArray(positions) || positions.length === 0) return false; + + // Schnellzugriff über Map erstellen: id -> position + const posMap = new Map(positions.map((item) => [item._id, Number(item.position)])); + + // Positionen in products aktualisieren + products.forEach((product) => { + if (posMap.has(product._id)) { + product.position = posMap.get(product._id); + } + }); + + // Liste anhand der neuen Positionen sortieren + products.sort((a, b) => (a.position || 0) - (b.position || 0)); + + return true; + }; + + // Validate ===== const hasAllProps = (product) => { // return Object.hasOwn(product, 'name') && Object.hasOwn(product, 'price'); @@ -100,6 +123,7 @@ const ProductModel = () => { add, update, delete: remove, + updatePosition, reset, save, }; diff --git a/04_browser-api/unterricht/tag30/01_website-refactored/backend/package-lock.json b/04_browser-api/unterricht/tag30/01_website-refactored/backend/package-lock.json index 6205c89..e5d1e87 100644 --- a/04_browser-api/unterricht/tag30/01_website-refactored/backend/package-lock.json +++ b/04_browser-api/unterricht/tag30/01_website-refactored/backend/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "chalk": "^6.0.1", "cors": "^2.8.6", - "dotenv": "^18.0.7", + "dotenv": "^18.0.8", "express": "^5.3.0", "uuid": "^14.0.3" } @@ -190,9 +190,9 @@ } }, "node_modules/dotenv": { - "version": "18.0.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.7.tgz", - "integrity": "sha512-QEvMEF8ca3GNZJJnAbOgxpj5ULhBvnMcURp+XKrtvqFNPgjBvYoswQ0Jl2cO7JzCjxZjJhr9ZuIEhlrRxMft/Q==", + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.8.tgz", + "integrity": "sha512-2KDeoFoK/zRtcnbunCDlLG5VWVMcyqeQc6iK65o231nfNAcuGrKoqUQfJFUtBI+QlShk5HKIHxwebBtUl3rEfA==", "license": "BSD-2-Clause", "bin": { "dotenv": "dist/index.cjs" diff --git a/04_browser-api/unterricht/tag30/01_website-refactored/backend/package.json b/04_browser-api/unterricht/tag30/01_website-refactored/backend/package.json index d310a57..8cfa4b1 100644 --- a/04_browser-api/unterricht/tag30/01_website-refactored/backend/package.json +++ b/04_browser-api/unterricht/tag30/01_website-refactored/backend/package.json @@ -11,7 +11,7 @@ "dependencies": { "chalk": "^6.0.1", "cors": "^2.8.6", - "dotenv": "^18.0.7", + "dotenv": "^18.0.8", "express": "^5.3.0", "uuid": "^14.0.3" } diff --git a/04_browser-api/unterricht/tag30/01_website-refactored/backend/server.js b/04_browser-api/unterricht/tag30/01_website-refactored/backend/server.js index 7853e67..6d3e714 100644 --- a/04_browser-api/unterricht/tag30/01_website-refactored/backend/server.js +++ b/04_browser-api/unterricht/tag30/01_website-refactored/backend/server.js @@ -116,6 +116,27 @@ app.patch('/api/products/reset', (req, res) => { return res.send({ msg: 'Products reseted', status: 200, success: true, data: [] }); }); +// HTTP - Methode - PUT +// Positionsaktualisierung aller Produkte +app.put('/api/products/positions', (req, res) => { + const positions = req.body; + + if (!products.updatePosition(positions)) { + return res.status(400).send({ + msg: 'Could not update positions', + status: 400, + success: false, + }); + } + + return res.send({ + msg: 'Positions updated', + status: 200, + success: true, + data: positions, + }); +}); + // HTTP - Methode - PATCH // speichern von Produkten app.patch('/api/products/save', (req, res) => { diff --git a/04_browser-api/unterricht/tag30/01_website-refactored/frontend-gulp/TODO.md b/04_browser-api/unterricht/tag30/01_website-refactored/frontend-gulp/TODO.md new file mode 100644 index 0000000..0f55a6b --- /dev/null +++ b/04_browser-api/unterricht/tag30/01_website-refactored/frontend-gulp/TODO.md @@ -0,0 +1,6 @@ +## Refactoring ProductManager + +- [x] Aktualisierung der Produktpositionen nach Veränderung durch Drag n' Drop +- [x] fetch Anfragen durch `api.request()` Methode zusammenfassen (DRY-Prinzip) +- [x] Anzahl an EventListener auf Zeilenebene durch Eventlistener auf Tabellenebene reduzieren. +- [ ] HTML Elemente im Produktmanager sollen über JS eine einmalige ID über `uuid` erhalten. \ No newline at end of file diff --git a/04_browser-api/unterricht/tag30/01_website-refactored/frontend-gulp/assets/js/build.js b/04_browser-api/unterricht/tag30/01_website-refactored/frontend-gulp/assets/js/build.js index ed59d1a..4950699 100644 --- a/04_browser-api/unterricht/tag30/01_website-refactored/frontend-gulp/assets/js/build.js +++ b/04_browser-api/unterricht/tag30/01_website-refactored/frontend-gulp/assets/js/build.js @@ -1,9 +1,6 @@ (function () { 'use strict'; - // Helper - function parents(el,selector){const parents=[];while((el=el.parentNode)&&el!==document){if(!selector||el.matches(selector))parents.push(el);}return parents;} - var top='top';var bottom='bottom';var right='right';var left='left';var auto='auto';var basePlacements=[top,bottom,right,left];var start='start';var end='end';var clippingParents='clippingParents';var viewport='viewport';var popper='popper';var reference='reference';var variationPlacements=/*#__PURE__*/basePlacements.reduce(function(acc,placement){return acc.concat([placement+"-"+start,placement+"-"+end]);},[]);var placements=/*#__PURE__*/[].concat(basePlacements,[auto]).reduce(function(acc,placement){return acc.concat([placement,placement+"-"+start,placement+"-"+end]);},[]);// modifiers that need to read the DOM var beforeRead='beforeRead';var read='read';var afterRead='afterRead';// pure-logic modifiers var beforeMain='beforeMain';var main='main';var afterMain='afterMain';// modifier with the purpose to write to the DOM (or write into a framework state) @@ -890,114 +887,31 @@ function v4(options,buf,offset){if(crypto.randomUUID){return crypto.randomUUID();}return _v4(options);}function _v4(options,buf,offset){options=options||{};const rnds=options.random??options.rng?.()??rng();if(rnds.length<16){throw new Error('Random bytes length must be >= 16');}rnds[6]=rnds[6]&0x0f|0x40;rnds[8]=rnds[8]&0x3f|0x80;return unsafeStringify(rnds);} - // import 'bootstrap'; - // Factory Function - const ProductManager=(target='.product-manager',options={})=>{// === Selektor or Element === - if(typeof target==='string'){const elements=document.querySelectorAll(target);if(elements.length===0){console.warn('Product Manager not found',target);return null;}if(elements.length>1){return [...elements].map(el=>ProductManager(el,options));// Rekursion - }target=elements[0];}else if(target instanceof NodeList||Array.isArray(target)){if(target.length===0)return null;if(target.length===1){target=target[0];}else {return [...target].map(el=>ProductManager(el,options));// Rekursion - }}// === DOM & VARS ======= - const uId=v4();// 'b18794e8-5d0d-417c-b361-ba38e78411b4' - // console.log('uId: ', uId); - const module=target;if(!module)return;const DOM={module,table:module.querySelector('.table-products'),tbody:module.querySelector('tbody'),inputName:module.querySelector('.input-product-name'),inputPrice:module.querySelector('.input-product-price'),btnAdd:module.querySelector('.button-product-add'),templateRow:module.querySelector('.template-row'),// Nav Actions - navActions:module.querySelector('.nav-products-actions'),btnReset:module.querySelector('.button-products-reset'),btnSave:module.querySelector('.button-products-save'),// Modal Edit + const ProductManager=(target='.product-manager',options={})=>{// === AUTO MULTI-INSTANCE === + if(typeof target==='string'){const elements=document.querySelectorAll(target);if(elements.length===0){console.error('Product Manager not found',target);return null;}if(elements.length>1){return [...elements].map(el=>ProductManager(el,options));}target=elements[0];}else if(target instanceof NodeList||Array.isArray(target)){if(target.length===0)return null;if(target.length===1){target=target[0];}else {return [...target].map(el=>ProductManager(el,options));}}// === DOM & VARS ======= + const SERVER_BASE_URL=options.baseUrl||'http://127.0.0.1:8000';const API_PRODUCTS=`${SERVER_BASE_URL}/api/products`;const module=target;v4();if(!module||!(module instanceof HTMLElement)){console.error('Product Manager: Ungültiges Element übergeben',target);return null;}const DOM={module,table:module.querySelector('.table-products'),tbody:module.querySelector('tbody'),inputName:module.querySelector('.input-product-name'),inputPrice:module.querySelector('.input-product-price'),btnAdd:module.querySelector('.button-product-add'),templateRow:module.querySelector('.template-row'),// Nav Actions + btnSave:module.querySelector('.button-products-save'),btnReset:module.querySelector('.button-products-reset'),// Modal Edit modalEdit:module.querySelector('.modal-edit'),formEdit:module.querySelector('.form-product-edit'),inputEditName:module.querySelector('.input-edit-name'),inputEditPrice:module.querySelector('.input-edit-price'),inputEditId:module.querySelector('.input-edit-id'),inputEditPosition:module.querySelector('.input-edit-position'),btnUpdate:module.querySelector('.button-product-update'),// Modal Delete - modalDelete:module.querySelector('.modal-delete'),productName:module.querySelector('strong.product-name'),btnConfirmDelete:module.querySelector('.button-confirm-delete')};const SERVER_BASE_URL='http://127.0.0.1:8000';const bsModalEdit=new Modal(DOM.modalEdit,{backdrop:'static',keyboard:true});const bsModalDelete=new Modal(DOM.modalDelete,{backdrop:true,keyboard:true});// console.log(DOM); - // === INIT ============= - const init=()=>{// Funktionsaufrufe zu beginn der Anwendung - initProducts();DOM.btnAdd.disabled=true;setUIds();// Event-Lauscher zu beginn der Anwendung - DOM.btnReset.addEventListener('click',onClickReset);DOM.btnSave.addEventListener('click',onClickSave);DOM.btnAdd.addEventListener('click',onClickAdd);DOM.btnConfirmDelete.addEventListener('click',onClickConfirmDelete);DOM.formEdit.addEventListener('submit',onSubmitEdit);window.addEventListener('keyup',onKeyUp);};// === EVENTHANDLER ===== - const onKeyUp=e=>{DOM.btnAdd.disabled=!(DOM.inputName.value!==''&&DOM.inputPrice.value!=='');// if (DOM.inputName.value !== '' && DOM.inputPrice.value !== '') { - // DOM.btnAdd.disabled = false; - // } else { - // DOM.btnAdd.disabled = true; - // } - };const onClickReset=e=>{const btnEl=e.currentTarget;const icon=btnEl.querySelector('i');icon.classList.add('fa-jello');// async API request - resetProducts().then(data=>{if(data.success){Toast(data.msg,'success').show();// Toast(data.msg, { variant: 'primary', duration: 5000, stopOnFocus: false }).show(); - // Toast(data.msg, { - // variant: 'success', - // duration: 3000, - // stopOnFocus: false, - // style: { borderRadius: '1rem' }, - // }).show(); - loadProducts();}else {Toast(data.msg,'error').show();}setTimeout(()=>{icon.classList.remove('fa-jello');},500);});};const onClickSave=e=>{const btnEl=e.currentTarget;const icon=btnEl.querySelector('i');icon.classList.add('fa-jello');// async API request - saveProducts().then(data=>{if(data.success){Toast(data.msg,{variant:'success',// - style:{background:'#f3b562',borderRadius:'1rem',color:'black'}}).show();}else {Toast(data.msg,'error').show();}setTimeout(()=>{icon.classList.remove('fa-jello');},500);});};const onClickAdd=e=>{const product={name:DOM.inputName.value,// - price:Number(DOM.inputPrice.valueAsNumber)};// async API request - createProduct(product).then(data=>{console.log(data);if(data.success){Toast(data.msg,'success').show();loadProducts();resetInputFields();disableNonFunctionalButtons();}else {Toast(data.msg,'error').show();}});// addProduct(product); - };const onSubmitEdit=e=>{e.preventDefault();// Standardverhalten unterbinden (Modal Form nicht versenden) - const product={name:DOM.inputEditName.value,price:Number(Number(DOM.inputEditPrice.value).toFixed(2)),_id:DOM.inputEditId.value,position:parseInt(DOM.inputEditPosition.value)};// console.log(Object.fromEntries(new FormData(DOM.formEdit))); - console.log(product);const icon=DOM.btnUpdate.querySelector('i');icon.classList.add('fa-spin');// async Fetch Request - updateProduct(product).then(data=>{console.log(data);if(data.success){Toast(data.msg,'success').show();icon.classList.remove('fa-spin');resetInputFields();loadProducts();bsModalEdit.hide();}else {Toast(data.msg,'error').show();}});};const onClickEdit=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];// btn.closest('tr'); // btn.parentNode.parentNode; - const id=currentRow.dataset.id;// async API request - fetchProduct(id).then(data=>{// console.log(data); - showModalEdit(data);});};const onClickRemove=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];// btn.closest('tr'); // btn.parentNode.parentNode; - const id=currentRow.dataset.id;const productName=currentRow.querySelector('.td-name').textContent.trim();console.log({id,productName});DOM.btnConfirmDelete.dataset.id=id;DOM.productName.textContent=productName;// currentRow.remove(); - // currentRow.parentNode.removeChild(currentRow); // IE11 support - disableNonFunctionalButtons();};const onClickConfirmDelete=e=>{const btn=e.currentTarget;const id=btn.dataset.id;// async API request - deleteProduct(id).then(data=>{// console.log(data); - if(data.success){Toast(data.msg,'success').show();loadProducts();bsModalDelete.hide();}else {Toast(data.msg,'error').show();}});};const onClickMoveUp=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];currentRow.previousElementSibling.before(currentRow);// DOM.tbody.insertBefore(currentRow, currentRow.previousElementSibling); - disableNonFunctionalButtons();};const onClickMoveDown=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];currentRow.nextElementSibling.after(currentRow);// DOM.tbody.insertBefore(currentRow, currentRow.nextElementSibling.nextElementSibling); - disableNonFunctionalButtons();};const onMouseDownDrag=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];// btn.closest('tr'); // btn.parentNode.parentNode; - currentRow.draggable=true;// =>