This commit is contained in:
Philippe Torrel
2026-10-16 14:35:19 +02:00
parent 42583441cc
commit 5a14dd3f10
95 changed files with 5235 additions and 681 deletions
@@ -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,
@@ -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,
};
@@ -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"
@@ -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"
}
@@ -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) => {
@@ -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.
@@ -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;// => <tr draggable="true" >...</tr>
};const onMouseUpDrag=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];// btn.closest('tr'); // btn.parentNode.parentNode;
currentRow.draggable=false;// => <tr draggable="false" >...</tr>
};const onDragStart=e=>{// console.log('DragEvent: ', e);
const currentRow=e.currentTarget;const idx=[...currentRow.parentNode.children].indexOf(currentRow);e.dataTransfer.setData('text/plain',String(idx));// console.log('tr reference index: ', idx);
};const onDragOver=e=>{e.preventDefault();// WICHTIG: sonst funktioniet drop-EventHandler nicht
};const onDrop=e=>{const currentRow=e.currentTarget;// <- Zielzeile, aud die gedroppt wird
const targetIdx=[...currentRow.parentNode.children].indexOf(currentRow);const sourceIdx=Number(e.dataTransfer.getData('text/plain'));console.log({targetIdx,sourceIdx});//const draggedRow = DOM.tbody.querySelector(`tr:nth-child(${sourceIdx + 1})`);
const draggedRow=DOM.tbody.children[sourceIdx];// Guard wenn Element auf sich selbst losgelassen wurde, abbruch
if(sourceIdx===targetIdx)return;// Verschiebe das Element im DOM
if(sourceIdx>targetIdx){currentRow.before(draggedRow);}else {currentRow.after(draggedRow);}disableNonFunctionalButtons();};// === XHR/FETCH ========
// HTTP - Methode - POST
// (C)RUD - Create
// BRE(A)D - Add - hinzufügen eines neuen Produkts
const createProduct=async product=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(product)});// HTTP-Request URL
if(!response.ok){throw new Error('Fetch went wrong.');}const data=await response.json();// HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;}catch(error){Toast(error.message||"Couldn't create product",'error').show();console.error(error);throw error;}};// HTTP - Methode - GET
// C(R)UD - Read
// (B)READ - Browse - auslesen aller Produkte
const fetchProducts=async()=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products`);// HTTP-Request URL
if(!response.ok){throw new Error('Fetch went wrong.');}const data=await response.json();// HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;}catch(error){Toast(error.message||"Couldn't get products",'error').show();console.error(error);throw error;}};// HTTP - Methode - GET
// C(R)UD - Read
// B(R)EAD - Read - auslesen eines Produkts
const fetchProduct=async id=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products/${id}`);// HTTP-Request URL
if(!response.ok){throw new Error('Fetch went wrong.');}const data=await response.json();// HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;}catch(error){Toast(error.message||"Couldn't get product",'error').show();console.error(error);throw error;}};// HTTP - Methode - PUT
// CR(U)D - Update
// BR(E)AD - Edit - aktualisieren eines Produkts
const updateProduct=async product=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(product)});// HTTP-Request URL
if(!response.ok){throw new Error('Fetch went wrong.');}const data=await response.json();// HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;}catch(error){Toast(error.message||"Couldn't update product",'error').show();console.error(error);throw error;}};// HTTP - Methode - DELETE
// CRU(D) - Delete
// BREA(D) - Delete - löschen eines Produkts
const deleteProduct=async id=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products/${id}`,{method:'DELETE'});// HTTP-Request URL
if(!response.ok){throw new Error('Fetch went wrong.');}const data=await response.json();// HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;}catch(error){Toast(error.message||"Couldn't delete product",'error').show();console.error(error);throw error;}};// HTTP - Methode - PATCH
// Reseten der Produkte
const resetProducts=async id=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products/reset`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({reset:true})});// HTTP-Request URL
if(!response.ok){throw new Error('Fetch went wrong.');}const data=await response.json();// HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;}catch(error){Toast(error.message||"Couldn't reset products",'error').show();console.error(error);throw error;}};// HTTP - Methode - PATCH
// Speichern der Produkte
const saveProducts=async id=>{try{const response=await fetch(`${SERVER_BASE_URL}/api/products/save`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({save:true})});// HTTP-Request URL
if(!response.ok){throw new Error('Fetch went wrong.');}const data=await response.json();// HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;}catch(error){Toast(error.message||"Couldn't save products",'error').show();console.error(error);throw error;}};// === FUNCTIONS ========
const initProducts=()=>{fetchProducts().then(products=>{products.forEach(addProduct);disableNonFunctionalButtons();});};const loadProducts=()=>{DOM.tbody.innerHTML='';fetchProducts().then(data=>{// TODO: add fade Animation
data.forEach(addProduct);disableNonFunctionalButtons();});};const addProduct=product=>{const{name,price,_id:id}=product;// https://developer.mozilla.org/de/docs/Web/HTML/Reference/Elements/template#anwendungshinweise
const trEl=DOM.templateRow.content.firstElementChild.cloneNode(true);const tdNameEl=trEl.querySelector('.td-name');const tdPriceEl=trEl.querySelector('.td-price');const btnDragEl=trEl.querySelector('.button-product-drag');const btnEditEl=trEl.querySelector('.button-product-edit');const btnRemoveEl=trEl.querySelector('.button-product-remove');const btnMoveUpEl=trEl.querySelector('.button-product-move-up');const btnMoveDownEl=trEl.querySelector('.button-product-move-down');// Drag 'n Drop hinzufügen.
btnDragEl.addEventListener('mousedown',onMouseDownDrag);btnDragEl.addEventListener('mouseup',onMouseUpDrag);// Id vom Produkt auf HTML data-Attribut ablegen
trEl.dataset.id=id;// <tr data-id="..."></tr>
trEl.addEventListener('dragstart',onDragStart);trEl.addEventListener('dragover',onDragOver);trEl.addEventListener('drop',onDrop);tdNameEl.textContent=name;tdPriceEl.textContent=Number(price).toFixed(2);btnEditEl.dataset.bsTarget+=`-${uId}`;btnEditEl.addEventListener('click',onClickEdit);btnRemoveEl.addEventListener('click',onClickRemove);btnRemoveEl.dataset.bsTarget+=`-${uId}`;btnMoveUpEl.addEventListener('click',onClickMoveUp);btnMoveDownEl.addEventListener('click',onClickMoveDown);DOM.tbody.appendChild(trEl);};const showModalEdit=product=>{const{name='',price=0,_id:id,position}=product;DOM.inputEditName.value=name;DOM.inputEditPrice.value=price;DOM.inputEditId.value=id;DOM.inputEditPosition.value=position;};const disableNonFunctionalButtons=()=>{DOM.tbody.querySelectorAll('tr').forEach(tr=>{tr.querySelector('.button-product-move-up').disabled=!tr.previousElementSibling;tr.querySelector('.button-product-move-down').disabled=!tr.nextElementSibling;});};const resetInputFields=()=>{DOM.inputName.value=DOM.inputPrice.value='';DOM.btnAdd.disabled=true;DOM.formEdit.reset();// Modal Formular leeren
};const setUIds=()=>{DOM.modalEdit.id+=`-${uId}`;const labedEditId=DOM.modalEdit.querySelector('.modal-title').id+=`-${uId}`;DOM.modalEdit.setAttribute('aria-labelledby',labedEditId);DOM.modalDelete.id+=`-${uId}`;const labedDeleteId=DOM.modalDelete.querySelector('.modal-title').id+=`-${uId}`;DOM.modalDelete.setAttribute('aria-labelledby',labedDeleteId);DOM.inputEditName.id+=`-${uId}`;DOM.inputEditPrice.id+=`-${uId}`;};// Helper
// function parents(el, selector) {
// const parents = [];
// while ((el = el.parentNode) && el !== document) {
// if (!selector || el.matches(selector)) parents.push(el);
// }
// return parents;
// }
init();return {element:DOM.module,init};};
modalDelete:module.querySelector('.modal-delete'),productName:module.querySelector('strong.product-name'),btnConfirmDelete:module.querySelector('.button-confirm-delete')};const bsModalEdit=new Modal(DOM.modalEdit,{backdrop:'static',keyboard:true});const bsModalDelete=new Modal(DOM.modalDelete,{backdrop:'static',keyboard:true});// State für Drag & Drop
let draggedRow=null;// === INIT =============
const init=async()=>{// Funktionsaufrufe zu Beginn der Anwendung
await initProducts();DOM.btnAdd.disabled=true;// 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);DOM.inputName.addEventListener('input',onInputValidate);DOM.inputPrice.addEventListener('input',onInputValidate);// Delegierte Tabellen- & Drag-Events
DOM.tbody.addEventListener('click',onTableClick);setupDragAndDrop();};// === EVENTHANDLER =====
const onInputValidate=()=>{DOM.btnAdd.disabled=DOM.inputName.value===''||DOM.inputPrice.value==='';};const onClickReset=async()=>{try{const data=await api.reset();if(data.success){Toast(data.msg,'success').show();loadProducts();}}catch(err){console.error('Reset fehlgeschlagen:',err);}};const onClickSave=async e=>{const btnEl=e.currentTarget;const icon=btnEl.querySelector('i');icon?.classList.add('fa-jello');try{const data=await api.save();if(data.success){Toast(data.msg,'success').show();}}catch(err){console.error('Speichern fehlgeschlagen:',err);}finally{setTimeout(()=>{icon?.classList.remove('fa-jello');},500);}};const onSubmitEdit=async e=>{e.preventDefault();const product={name:DOM.inputEditName.value.trim(),price:Number(DOM.inputEditPrice.value),_id:DOM.inputEditId.value,position:Number(DOM.inputEditPosition.value)};DOM.btnUpdate.querySelector('i')?.classList.add('fa-spin');try{const data=await api.edit(product);if(data.success){Toast(data.msg,'success').show();loadProducts();resetFields();bsModalEdit.hide();}}catch(err){console.error('Update fehlgeschlagen:',err);}finally{DOM.btnUpdate.querySelector('i')?.classList.remove('fa-spin');}};const onClickAdd=async()=>{const product={name:DOM.inputName.value.trim(),price:Number(DOM.inputPrice.value)};try{const data=await api.add(product);if(data.success){Toast(data.msg,'success').show();loadProducts();resetFields();}}catch(err){console.error('Erstellen fehlgeschlagen:',err);}};const onTableClick=async e=>{const btnEl=e.target.closest('button');if(!btnEl)return;const currentRow=parent(btnEl,'tr');if(!currentRow)return;const id=currentRow.dataset.id;if(btnEl.matches('.button-product-edit')){try{const data=await api.read(id);showModalEdit(data);}catch(err){console.error('Laden für Bearbeitung fehlgeschlagen:',err);}}else if(btnEl.matches('.button-product-remove')){const productName=currentRow.querySelector('.td-name')?.textContent.trim()||'';DOM.btnConfirmDelete.dataset.id=id;DOM.productName.textContent=productName;bsModalDelete.show();}else if(btnEl.matches('.button-product-move-up')&&currentRow.previousElementSibling){currentRow.previousElementSibling.before(currentRow);disableNonFunctionalButtons();await updateRowPositions();}else if(btnEl.matches('.button-product-move-down')&&currentRow.nextElementSibling){currentRow.nextElementSibling.after(currentRow);disableNonFunctionalButtons();await updateRowPositions();}};const onClickConfirmDelete=async()=>{const id=DOM.btnConfirmDelete.dataset.id;if(!id)return;try{const data=await api.destroy(id);if(data.success){Toast(data.msg,'success').show();bsModalDelete.hide();loadProducts();}}catch(err){console.error('Löschen fehlgeschlagen:',err);}};// Drag 'n Drop Event-Setup via Delegation
const setupDragAndDrop=()=>{let isHandleMouseDown=false;// 1. Nur wenn die Maus auf den Drag-Button drückt, erlauben wir das Ziehen der Zeile
DOM.tbody.addEventListener('mousedown',e=>{const handle=e.target.closest('.button-product-drag, .button-drag');if(!handle){isHandleMouseDown=false;return;}const currentRow=parent(handle,'tr');if(currentRow){isHandleMouseDown=true;currentRow.setAttribute('draggable','true');}});DOM.tbody.addEventListener('mouseup',()=>{isHandleMouseDown=false;});// 2. Drag Start: Startet den nativen Browser-Drag-Vorgang
DOM.tbody.addEventListener('dragstart',e=>{const currentRow=parent(e.target,'tr');if(!currentRow||!isHandleMouseDown){e.preventDefault();return;}draggedRow=currentRow;e.dataTransfer.effectAllowed='move';e.dataTransfer.setData('text/plain',currentRow.dataset.id||'drag');currentRow.classList.add('is-dragging');});// 3. Drag Over: Pflicht, um drop zu gestatten
DOM.tbody.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='move';});// 4. Drop: Verschieben im DOM und Server-Aktualisierung
DOM.tbody.addEventListener('drop',async e=>{e.preventDefault();const currentRow=parent(e.target,'tr');if(!currentRow||!draggedRow||currentRow===draggedRow)return;const rows=[...DOM.tbody.children];const sourceIdx=rows.indexOf(draggedRow);const targetIdx=rows.indexOf(currentRow);if(sourceIdx<targetIdx){currentRow.after(draggedRow);}else {currentRow.before(draggedRow);}disableNonFunctionalButtons();await updateRowPositions();});// 5. Drag End & Cleanup
DOM.tbody.addEventListener('dragend',()=>{isHandleMouseDown=false;if(draggedRow){draggedRow.removeAttribute('draggable');draggedRow.classList.remove('is-dragging');draggedRow=null;}});};// === XHR/FETCH (DRY BREAD API) ========
const api={async request(url,{data,...options}={}){const isJsonPayload=data!==undefined;try{const response=await fetch(url,{...options,headers:{...(isJsonPayload&&{'Content-Type':'application/json'}),...options.headers},body:isJsonPayload?JSON.stringify(data):options.body});if(!response.ok){const errorJson=await response.json().catch(()=>null);const errorMsg=errorJson?.msg||`${options.method??'GET'} ${url} -> ${response.status}`;throw new Error(errorMsg);}if(response.status===204)return null;return await response.json();}catch(error){// Zentraler Toast für alle API-/Netzwerkfehler
Toast(error.message||'Verbindungsfehler zum Server','error').show();throw error;}},// BREAD Endpunkte
browse:()=>api.request(API_PRODUCTS),read:id=>api.request(`${API_PRODUCTS}/${id}`),edit:product=>api.request(API_PRODUCTS,{method:'PUT',data:product}),add:product=>api.request(API_PRODUCTS,{method:'POST',data:product}),destroy:id=>api.request(`${API_PRODUCTS}/${id}`,{method:'DELETE'}),// Zusätzliche Endpunkte
updatePositions:positions=>api.request(`${API_PRODUCTS}/positions`,{method:'PUT',data:positions}),save:()=>api.request(`${API_PRODUCTS}/save`,{method:'PATCH',data:{save:true}}),reset:()=>api.request(`${API_PRODUCTS}/reset`,{method:'PATCH',data:{reset:true}})};// === FUNCTIONS ========
const initProducts=async()=>{try{const products=await api.browse();DOM.tbody.replaceChildren(...products.map(createRowElement));Toast('Init products').show();disableNonFunctionalButtons();}catch(err){console.error('Initialisierung fehlgeschlagen:',err);}};const loadProducts=async()=>{try{const products=await api.browse();DOM.tbody.replaceChildren(...products.map(createRowElement));disableNonFunctionalButtons();}catch(err){console.error('Neu laden fehlgeschlagen:',err);}};const createRowElement=product=>{const{name,price,_id:id,position=0}=product;const trEl=DOM.templateRow.content.firstElementChild.cloneNode(true);trEl.dataset.id=id;trEl.dataset.position=position;trEl.querySelector('.td-name').textContent=name;trEl.querySelector('.td-price').textContent=Number(price).toFixed(2);return trEl;};const showModalEdit=product=>{const{name='',price=0,_id:id,position}=product;DOM.inputEditName.value=name;DOM.inputEditPrice.value=price;DOM.inputEditId.value=id;DOM.inputEditPosition.value=position??0;bsModalEdit.show();};const resetFields=()=>{DOM.inputName.value='';DOM.inputPrice.value='';DOM.btnAdd.disabled=true;DOM.formEdit.reset();};const disableNonFunctionalButtons=()=>{const rows=[...DOM.tbody.children];rows.forEach((tr,index)=>{const btnUp=tr.querySelector('.button-product-move-up');const btnDown=tr.querySelector('.button-product-move-down');if(btnUp)btnUp.disabled=index===0;if(btnDown)btnDown.disabled=index===rows.length-1;});};const updateRowPositions=async()=>{const rows=[...DOM.tbody.children];const positions=rows.map((tr,index)=>{const newPos=index+1;tr.dataset.position=newPos;return {_id:tr.dataset.id,position:newPos};});try{const data=await api.updatePositions(positions);if(data.success){Toast(data.msg,'success').show();}}catch(err){console.error('Fehler beim Aktualisieren der Positionen:',err);}};// === HELPERS ==========
const parent=(el,selector,boundary=DOM.tbody)=>{let current=el;while(current&&current!==document){if(current.matches(selector)&&(!boundary||current.parentElement===boundary)){return current;}current=current.parentElement;}return null;};// Selbstanstoß & Interface
init();return {element:DOM.module,init,initProducts,loadProducts,resetFields};};
function _defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor);}}function _createClass(Constructor,protoProps,staticProps){if(protoProps)_defineProperties(Constructor.prototype,protoProps);Object.defineProperty(Constructor,"prototype",{writable:false});return Constructor;}/*!
* Splide.js
File diff suppressed because one or more lines are too long
@@ -1,24 +1,20 @@
import { parents } from '../utils/helpers.js';
import { Modal } from 'bootstrap';
import Toast from './Toast';
import { v4 as id } from 'uuid';
//import * as bootstrap from 'bootstrap';
// import 'bootstrap';
// Factory Function
const ProductManager = (target = '.product-manager', options = {}) => {
// === Selektor or Element ===
// === AUTO MULTI-INSTANCE ===
if (typeof target === 'string') {
const elements = document.querySelectorAll(target);
if (elements.length === 0) {
console.warn('Product Manager not found', target);
console.error('Product Manager not found', target);
return null;
}
if (elements.length > 1) {
return [...elements].map((el) => ProductManager(el, options)); // Rekursion
return [...elements].map((el) => ProductManager(el, options));
}
target = elements[0];
@@ -27,15 +23,21 @@ const ProductManager = (target = '.product-manager', options = {}) => {
if (target.length === 1) {
target = target[0];
} else {
return [...target].map((el) => ProductManager(el, options)); // Rekursion
return [...target].map((el) => ProductManager(el, options));
}
}
// === DOM & VARS =======
const uId = id(); // 'b18794e8-5d0d-417c-b361-ba38e78411b4'
// console.log('uId: ', uId);
const SERVER_BASE_URL = options.baseUrl || 'http://127.0.0.1:8000';
const API_PRODUCTS = `${SERVER_BASE_URL}/api/products`;
const module = target;
if (!module) return;
const uId = id();
if (!module || !(module instanceof HTMLElement)) {
console.error('Product Manager: Ungültiges Element übergeben', target);
return null;
}
const DOM = {
module,
@@ -47,9 +49,8 @@ const ProductManager = (target = '.product-manager', options = {}) => {
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'),
btnReset: module.querySelector('.button-products-reset'),
// Modal Edit
modalEdit: module.querySelector('.modal-edit'),
@@ -66,479 +67,323 @@ const ProductManager = (target = '.product-manager', options = {}) => {
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,
backdrop: 'static',
keyboard: true,
});
// console.log(DOM);
// State für Drag & Drop
let draggedRow = null;
// === INIT =============
const init = () => {
// Funktionsaufrufe zu beginn der Anwendung
initProducts();
const init = async () => {
// Funktionsaufrufe zu Beginn der Anwendung
await initProducts();
DOM.btnAdd.disabled = true;
setUIds();
// Event-Lauscher zu beginn der Anwendung
DOM.btnReset.addEventListener('click', onClickReset);
DOM.btnSave.addEventListener('click', onClickSave);
// 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.btnConfirmDelete?.addEventListener('click', onClickConfirmDelete);
DOM.formEdit.addEventListener('submit', onSubmitEdit);
window.addEventListener('keyup', onKeyUp);
DOM.inputName.addEventListener('input', onInputValidate);
DOM.inputPrice.addEventListener('input', onInputValidate);
// Delegierte Tabellen- & Drag-Events
DOM.tbody.addEventListener('click', onTableClick);
setupDragAndDrop();
};
// === 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 onInputValidate = () => {
DOM.btnAdd.disabled = DOM.inputName.value === '' || DOM.inputPrice.value === '';
};
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);
const onClickReset = async () => {
try {
const data = await api.reset();
if (data.success) {
Toast(data.msg, 'success').show();
loadProducts();
resetInputFields();
disableNonFunctionalButtons();
} else {
Toast(data.msg, 'error').show();
}
});
// addProduct(product);
} catch (err) {
console.error('Reset fehlgeschlagen:', err);
}
};
const onSubmitEdit = (e) => {
e.preventDefault(); // Standardverhalten unterbinden (Modal Form nicht versenden)
const onClickSave = async (e) => {
const btnEl = e.currentTarget;
const icon = btnEl.querySelector('i');
icon?.classList.add('fa-jello');
try {
const data = await api.save();
if (data.success) {
Toast(data.msg, 'success').show();
}
} catch (err) {
console.error('Speichern fehlgeschlagen:', err);
} finally {
setTimeout(() => {
icon?.classList.remove('fa-jello');
}, 500);
}
};
const onSubmitEdit = async (e) => {
e.preventDefault();
const product = {
name: DOM.inputEditName.value,
price: Number(Number(DOM.inputEditPrice.value).toFixed(2)),
name: DOM.inputEditName.value.trim(),
price: Number(DOM.inputEditPrice.value),
_id: DOM.inputEditId.value,
position: parseInt(DOM.inputEditPosition.value),
position: Number(DOM.inputEditPosition.value),
};
// console.log(Object.fromEntries(new FormData(DOM.formEdit)));
DOM.btnUpdate.querySelector('i')?.classList.add('fa-spin');
console.log(product);
const icon = DOM.btnUpdate.querySelector('i');
icon.classList.add('fa-spin');
// async Fetch Request
updateProduct(product).then((data) => {
console.log(data);
try {
const data = await api.edit(product);
if (data.success) {
Toast(data.msg, 'success').show();
icon.classList.remove('fa-spin');
resetInputFields();
loadProducts();
resetFields();
bsModalEdit.hide();
} else {
Toast(data.msg, 'error').show();
}
});
} catch (err) {
console.error('Update fehlgeschlagen:', err);
} finally {
DOM.btnUpdate.querySelector('i')?.classList.remove('fa-spin');
}
};
const onClickEdit = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0]; // btn.closest('tr'); // btn.parentNode.parentNode;
const id = currentRow.dataset.id;
const onClickAdd = async () => {
const product = {
name: DOM.inputName.value.trim(),
price: Number(DOM.inputPrice.value),
};
// 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);
try {
const data = await api.add(product);
if (data.success) {
Toast(data.msg, 'success').show();
loadProducts();
resetFields();
}
} catch (err) {
console.error('Erstellen fehlgeschlagen:', err);
}
};
const onTableClick = async (e) => {
const btnEl = e.target.closest('button');
if (!btnEl) return;
const currentRow = parent(btnEl, 'tr');
if (!currentRow) return;
const id = currentRow.dataset.id;
if (btnEl.matches('.button-product-edit')) {
try {
const data = await api.read(id);
showModalEdit(data);
} catch (err) {
console.error('Laden für Bearbeitung fehlgeschlagen:', err);
}
} else if (btnEl.matches('.button-product-remove')) {
const productName = currentRow.querySelector('.td-name')?.textContent.trim() || '';
DOM.btnConfirmDelete.dataset.id = id;
DOM.productName.textContent = productName;
bsModalDelete.show();
} else if (btnEl.matches('.button-product-move-up') && currentRow.previousElementSibling) {
currentRow.previousElementSibling.before(currentRow);
disableNonFunctionalButtons();
await updateRowPositions();
} else if (btnEl.matches('.button-product-move-down') && currentRow.nextElementSibling) {
currentRow.nextElementSibling.after(currentRow);
disableNonFunctionalButtons();
await updateRowPositions();
}
};
const onClickConfirmDelete = async () => {
const id = DOM.btnConfirmDelete.dataset.id;
if (!id) return;
try {
const data = await api.destroy(id);
if (data.success) {
Toast(data.msg, 'success').show();
bsModalDelete.hide();
loadProducts();
}
} catch (err) {
console.error('Löschen fehlgeschlagen:', err);
}
};
// Drag 'n Drop Event-Setup via Delegation
const setupDragAndDrop = () => {
let isHandleMouseDown = false;
// 1. Nur wenn die Maus auf den Drag-Button drückt, erlauben wir das Ziehen der Zeile
DOM.tbody.addEventListener('mousedown', (e) => {
const handle = e.target.closest('.button-product-drag, .button-drag');
if (!handle) {
isHandleMouseDown = false;
return;
}
const currentRow = parent(handle, 'tr');
if (currentRow) {
isHandleMouseDown = true;
currentRow.setAttribute('draggable', 'true');
}
});
DOM.tbody.addEventListener('mouseup', () => {
isHandleMouseDown = false;
});
// 2. Drag Start: Startet den nativen Browser-Drag-Vorgang
DOM.tbody.addEventListener('dragstart', (e) => {
const currentRow = parent(e.target, 'tr');
if (!currentRow || !isHandleMouseDown) {
e.preventDefault();
return;
}
draggedRow = currentRow;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', currentRow.dataset.id || 'drag');
currentRow.classList.add('is-dragging');
});
// 3. Drag Over: Pflicht, um drop zu gestatten
DOM.tbody.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
// 4. Drop: Verschieben im DOM und Server-Aktualisierung
DOM.tbody.addEventListener('drop', async (e) => {
e.preventDefault();
const currentRow = parent(e.target, 'tr');
if (!currentRow || !draggedRow || currentRow === draggedRow) return;
const rows = [...DOM.tbody.children];
const sourceIdx = rows.indexOf(draggedRow);
const targetIdx = rows.indexOf(currentRow);
if (sourceIdx < targetIdx) {
currentRow.after(draggedRow);
} else {
Toast(data.msg, 'error').show();
currentRow.before(draggedRow);
}
disableNonFunctionalButtons();
await updateRowPositions();
});
// 5. Drag End & Cleanup
DOM.tbody.addEventListener('dragend', () => {
isHandleMouseDown = false;
if (draggedRow) {
draggedRow.removeAttribute('draggable');
draggedRow.classList.remove('is-dragging');
draggedRow = null;
}
});
};
const onClickMoveUp = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0];
// === XHR/FETCH (DRY BREAD API) ========
const api = {
async request(url, { data, ...options } = {}) {
const isJsonPayload = data !== undefined;
currentRow.previousElementSibling.before(currentRow);
// DOM.tbody.insertBefore(currentRow, currentRow.previousElementSibling);
disableNonFunctionalButtons();
};
try {
const response = await fetch(url, {
...options,
headers: {
...(isJsonPayload && { 'Content-Type': 'application/json' }),
...options.headers,
},
body: isJsonPayload ? JSON.stringify(data) : options.body,
});
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();
};
if (!response.ok) {
const errorJson = await response.json().catch(() => null);
const errorMsg = errorJson?.msg || `${options.method ?? 'GET'} ${url} -> ${response.status}`;
throw new Error(errorMsg);
}
const onMouseDownDrag = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0]; // btn.closest('tr'); // btn.parentNode.parentNode;
currentRow.draggable = true; // => <tr draggable="true" >...</tr>
};
const onMouseUpDrag = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0]; // btn.closest('tr'); // btn.parentNode.parentNode;
currentRow.draggable = false; // => <tr draggable="false" >...</tr>
};
const onDragStart = (e) => {
// console.log('DragEvent: ', e);
const currentRow = e.currentTarget;
const idx = [...currentRow.parentNode.children].indexOf(currentRow);
e.dataTransfer.setData('text/plain', String(idx));
// console.log('tr reference index: ', idx);
};
const onDragOver = (e) => {
e.preventDefault(); // WICHTIG: sonst funktioniet drop-EventHandler nicht
};
const onDrop = (e) => {
const currentRow = e.currentTarget; // <- Zielzeile, aud die gedroppt wird
const targetIdx = [...currentRow.parentNode.children].indexOf(currentRow);
const sourceIdx = Number(e.dataTransfer.getData('text/plain'));
console.log({ targetIdx, sourceIdx });
//const draggedRow = DOM.tbody.querySelector(`tr:nth-child(${sourceIdx + 1})`);
const draggedRow = DOM.tbody.children[sourceIdx];
// Guard wenn Element auf sich selbst losgelassen wurde, abbruch
if (sourceIdx === targetIdx) return;
// Verschiebe das Element im DOM
if (sourceIdx > targetIdx) {
currentRow.before(draggedRow);
} else {
currentRow.after(draggedRow);
}
disableNonFunctionalButtons();
};
// === XHR/FETCH ========
// HTTP - Methode - POST
// (C)RUD - Create
// BRE(A)D - Add - hinzufügen eines neuen Produkts
const createProduct = async (product) => {
try {
const response = await fetch(`${SERVER_BASE_URL}/api/products`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(product),
}); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
if (response.status === 204) return null;
return await response.json();
} catch (error) {
// Zentraler Toast für alle API-/Netzwerkfehler
Toast(error.message || 'Verbindungsfehler zum Server', 'error').show();
throw error;
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
},
return data;
} catch (error) {
Toast(error.message || "Couldn't create product", 'error').show();
console.error(error);
throw error;
}
};
// BREAD Endpunkte
browse: () => api.request(API_PRODUCTS),
read: (id) => api.request(`${API_PRODUCTS}/${id}`),
edit: (product) => api.request(API_PRODUCTS, { method: 'PUT', data: product }),
add: (product) => api.request(API_PRODUCTS, { method: 'POST', data: product }),
destroy: (id) => api.request(`${API_PRODUCTS}/${id}`, { method: 'DELETE' }),
// HTTP - Methode - GET
// C(R)UD - Read
// (B)READ - Browse - auslesen aller Produkte
const fetchProducts = async () => {
try {
const response = await fetch(`${SERVER_BASE_URL}/api/products`); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;
} catch (error) {
Toast(error.message || "Couldn't get products", 'error').show();
console.error(error);
throw error;
}
};
// HTTP - Methode - GET
// C(R)UD - Read
// B(R)EAD - Read - auslesen eines Produkts
const fetchProduct = async (id) => {
try {
const response = await fetch(`${SERVER_BASE_URL}/api/products/${id}`); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;
} catch (error) {
Toast(error.message || "Couldn't get product", 'error').show();
console.error(error);
throw error;
}
};
// HTTP - Methode - PUT
// CR(U)D - Update
// BR(E)AD - Edit - aktualisieren eines Produkts
const updateProduct = async (product) => {
try {
const response = await fetch(`${SERVER_BASE_URL}/api/products`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(product),
}); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;
} catch (error) {
Toast(error.message || "Couldn't update product", 'error').show();
console.error(error);
throw error;
}
};
// HTTP - Methode - DELETE
// CRU(D) - Delete
// BREA(D) - Delete - löschen eines Produkts
const deleteProduct = async (id) => {
try {
const response = await fetch(`${SERVER_BASE_URL}/api/products/${id}`, {
method: 'DELETE',
}); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;
} catch (error) {
Toast(error.message || "Couldn't delete product", 'error').show();
console.error(error);
throw error;
}
};
// HTTP - Methode - PATCH
// Reseten der Produkte
const resetProducts = async (id) => {
try {
const response = await fetch(`${SERVER_BASE_URL}/api/products/reset`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ reset: true }),
}); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;
} catch (error) {
Toast(error.message || "Couldn't reset products", 'error').show();
console.error(error);
throw error;
}
};
// HTTP - Methode - PATCH
// Speichern der Produkte
const saveProducts = async (id) => {
try {
const response = await fetch(`${SERVER_BASE_URL}/api/products/save`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ save: true }),
}); // HTTP-Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response-Body in JS-Objekt geparsed -> Promise
return data;
} catch (error) {
Toast(error.message || "Couldn't save products", 'error').show();
console.error(error);
throw error;
}
// Zusätzliche Endpunkte
updatePositions: (positions) => api.request(`${API_PRODUCTS}/positions`, { method: 'PUT', data: positions }),
save: () => api.request(`${API_PRODUCTS}/save`, { method: 'PATCH', data: { save: true } }),
reset: () => api.request(`${API_PRODUCTS}/reset`, { method: 'PATCH', data: { reset: true } }),
};
// === FUNCTIONS ========
const initProducts = () => {
fetchProducts().then((products) => {
products.forEach(addProduct);
const initProducts = async () => {
try {
const products = await api.browse();
DOM.tbody.replaceChildren(...products.map(createRowElement));
Toast('Init products').show();
disableNonFunctionalButtons();
});
} catch (err) {
console.error('Initialisierung fehlgeschlagen:', err);
}
};
const loadProducts = () => {
DOM.tbody.innerHTML = '';
fetchProducts().then((data) => {
// TODO: add fade Animation
data.forEach(addProduct);
const loadProducts = async () => {
try {
const products = await api.browse();
DOM.tbody.replaceChildren(...products.map(createRowElement));
disableNonFunctionalButtons();
});
} catch (err) {
console.error('Neu laden fehlgeschlagen:', err);
}
};
const addProduct = (product) => {
const { name, price, _id: id } = product;
const createRowElement = (product) => {
const { name, price, _id: id, position = 0 } = product;
// https://developer.mozilla.org/de/docs/Web/HTML/Reference/Elements/template#anwendungshinweise
const trEl = DOM.templateRow.content.firstElementChild.cloneNode(true);
trEl.dataset.id = id;
trEl.dataset.position = position;
const tdNameEl = trEl.querySelector('.td-name');
const tdPriceEl = trEl.querySelector('.td-price');
const btnDragEl = trEl.querySelector('.button-product-drag');
trEl.querySelector('.td-name').textContent = name;
trEl.querySelector('.td-price').textContent = Number(price).toFixed(2);
const btnEditEl = trEl.querySelector('.button-product-edit');
const btnRemoveEl = trEl.querySelector('.button-product-remove');
const btnMoveUpEl = trEl.querySelector('.button-product-move-up');
const btnMoveDownEl = trEl.querySelector('.button-product-move-down');
// Drag 'n Drop hinzufügen.
btnDragEl.addEventListener('mousedown', onMouseDownDrag);
btnDragEl.addEventListener('mouseup', onMouseUpDrag);
// Id vom Produkt auf HTML data-Attribut ablegen
trEl.dataset.id = id; // <tr data-id="..."></tr>
trEl.addEventListener('dragstart', onDragStart);
trEl.addEventListener('dragover', onDragOver);
trEl.addEventListener('drop', onDrop);
tdNameEl.textContent = name;
tdPriceEl.textContent = Number(price).toFixed(2);
btnEditEl.dataset.bsTarget += `-${uId}`;
btnEditEl.addEventListener('click', onClickEdit);
btnRemoveEl.addEventListener('click', onClickRemove);
btnRemoveEl.dataset.bsTarget += `-${uId}`;
btnMoveUpEl.addEventListener('click', onClickMoveUp);
btnMoveDownEl.addEventListener('click', onClickMoveDown);
DOM.tbody.appendChild(trEl);
return trEl;
};
const showModalEdit = (product) => {
@@ -546,50 +391,71 @@ const ProductManager = (target = '.product-manager', options = {}) => {
DOM.inputEditName.value = name;
DOM.inputEditPrice.value = price;
DOM.inputEditId.value = id;
DOM.inputEditPosition.value = position;
DOM.inputEditPosition.value = position ?? 0;
bsModalEdit.show();
};
const resetFields = () => {
DOM.inputName.value = '';
DOM.inputPrice.value = '';
DOM.btnAdd.disabled = true;
DOM.formEdit.reset();
};
const disableNonFunctionalButtons = () => {
DOM.tbody.querySelectorAll('tr').forEach((tr) => {
tr.querySelector('.button-product-move-up').disabled = !tr.previousElementSibling;
tr.querySelector('.button-product-move-down').disabled = !tr.nextElementSibling;
const rows = [...DOM.tbody.children];
rows.forEach((tr, index) => {
const btnUp = tr.querySelector('.button-product-move-up');
const btnDown = tr.querySelector('.button-product-move-down');
if (btnUp) btnUp.disabled = index === 0;
if (btnDown) btnDown.disabled = index === rows.length - 1;
});
};
const resetInputFields = () => {
DOM.inputName.value = DOM.inputPrice.value = '';
DOM.btnAdd.disabled = true;
const updateRowPositions = async () => {
const rows = [...DOM.tbody.children];
DOM.formEdit.reset(); // Modal Formular leeren
const positions = rows.map((tr, index) => {
const newPos = index + 1;
tr.dataset.position = newPos;
return {
_id: tr.dataset.id,
position: newPos,
};
});
try {
const data = await api.updatePositions(positions);
if (data.success) {
Toast(data.msg, 'success').show();
}
} catch (err) {
console.error('Fehler beim Aktualisieren der Positionen:', err);
}
};
const setUIds = () => {
DOM.modalEdit.id += `-${uId}`;
const labedEditId = (DOM.modalEdit.querySelector('.modal-title').id += `-${uId}`);
DOM.modalEdit.setAttribute('aria-labelledby', labedEditId);
DOM.modalDelete.id += `-${uId}`;
const labedDeleteId = (DOM.modalDelete.querySelector('.modal-title').id += `-${uId}`);
DOM.modalDelete.setAttribute('aria-labelledby', labedDeleteId);
DOM.inputEditName.id += `-${uId}`;
DOM.inputEditPrice.id += `-${uId}`;
// === HELPERS ==========
const parent = (el, selector, boundary = DOM.tbody) => {
let current = el;
while (current && current !== document) {
if (current.matches(selector) && (!boundary || current.parentElement === boundary)) {
return current;
}
current = current.parentElement;
}
return null;
};
// Helper
// function parents(el, selector) {
// const parents = [];
// while ((el = el.parentNode) && el !== document) {
// if (!selector || el.matches(selector)) parents.push(el);
// }
// return parents;
// }
// Selbstanstoß & Interface
init();
return {
element: DOM.module,
init,
initProducts,
loadProducts,
resetFields,
};
};
@@ -1,11 +1,11 @@
{
"name": "website-frontend",
"name": "website-frontend-gulp",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "website-frontend",
"name": "website-frontend-gulp",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
@@ -37,8 +37,8 @@
"gulp-sizereport": "^1.2.1",
"gulp-terser": "^2.1.0",
"npm-run-all": "^4.1.5",
"rollup": "^4.64.6",
"sass": "^1.105.1",
"rollup": "^4.64.7",
"sass": "^1.105.2",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0"
}
@@ -2523,9 +2523,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.6.tgz",
"integrity": "sha512-FkO19Fowv/OQeJZPVfdetJaI3/VpbZmBsnmAlR/rQ+b3QwfiGQiOpVegpSRH331kelvHTVw9hXYG3DwjAaRzyQ==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.7.tgz",
"integrity": "sha512-GFF2tHk0NqYVObrK0+/ps/LEXsV/v2q6Lyiy7mZr+Ej24fpQScvqK2ziz9NHeEQYnKWKym38Ml7dZbPtC9tuAw==",
"cpu": [
"arm"
],
@@ -2537,9 +2537,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.6.tgz",
"integrity": "sha512-si5/Etm6MphPc2xYHnk455Shls/hoMjTy3mhXge4OPmvCw7TwC4KP4fKokAlKLV4XaQvLvgSHfafPcblLMYYdQ==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.7.tgz",
"integrity": "sha512-BOymcXJFYYXqluDidj6sLelNjXruC3u1BqVxLn2OPCXH9xaPSOTH+MS4uychsGI1NU7tJoiv9KvqijDHFe+hhQ==",
"cpu": [
"arm64"
],
@@ -2551,9 +2551,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.6.tgz",
"integrity": "sha512-PQCl3q7b0BgvVt72i2xrWM6I5KliwwVe4GpWW5QDGyUQawDS9aPZss/aGyFqBmxdRxlE8wDc9CaxiQ5DHRSp/Q==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.7.tgz",
"integrity": "sha512-jgVpGweIbls2H5jfBB70tnX52abMd5pJr3xmtoDILx6JdmGl+Un+tMb9R+MbHPVB46tTBpY8emtn7M71SnQhEQ==",
"cpu": [
"arm64"
],
@@ -2565,9 +2565,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.6.tgz",
"integrity": "sha512-l9DFl1Itb1YYdSrP1K3U8ih44AOHa/p+Q4WMMDfGvH5qVWp+ajfgS1DlYo4yCRh8wJ2ZJv0Fqccf+ULAmMFjmg==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.7.tgz",
"integrity": "sha512-AVMpt94mrXH7WjyWKmd9nT4RC5yM0AN3Lt6A89xVIsuLDi3dDX6dxMytLbW36FlppY9JsoqUoS3LUUpUrYxiMQ==",
"cpu": [
"x64"
],
@@ -2579,9 +2579,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.6.tgz",
"integrity": "sha512-bR4GR3205N9+od0+l5Dd9/iZTDpJkIe6d6Ydp2qbcQZJLmKBdYQQRp8gbWpbMt92twKJY66o1vK4NkoKQODIrA==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.7.tgz",
"integrity": "sha512-Nje29c/MyajxRNFGU05oyeTDS6SWdMg/wWMSlZSBTBju5BKz2NVc0kFiFSQGzRELNiB+CBk/ZvAVsWsENgjGag==",
"cpu": [
"arm64"
],
@@ -2593,9 +2593,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.6.tgz",
"integrity": "sha512-XTVNaDzcFl2TeBglvxdGgsCv9JGtAGRAoJENmGBR/XvDH2mA2K0IXc2aMfP0dR8mexSXSKifJ85FixoXX1wfTw==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.7.tgz",
"integrity": "sha512-xxgN5/kMzCNz/NYJ4t49B0/WS6InNlWv6xvgwUQqNnWM9WJMMwMUGWrAxdNEgYWdHL0sMnuh8I87StGrIMTA+w==",
"cpu": [
"x64"
],
@@ -2607,9 +2607,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.6.tgz",
"integrity": "sha512-qWc6qbc+25ugG9vqPR9P422KJBbVfZ05mjcMggoq8tsfWP+eRfC+/tiDdIlRF5VL2pDVZWN9zAhWsmu7R1+7Dw==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.7.tgz",
"integrity": "sha512-5U/9gVn/X657I8+NHoHO9Wnfyd0lEXwqbslobRoB9TC8SwYzOitcIr6TLE7531Q/NWs3L4mb+YOGzlf3vHIeYw==",
"cpu": [
"arm"
],
@@ -2624,9 +2624,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.6.tgz",
"integrity": "sha512-V4Nwkp8844X/Xnww3PI1LYS0YPei8XQEVG70R+5MPZGO7AhYoun4hCN7Svs4U3wbQLZbwiz+9xRFhUZZdRl8Ww==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.7.tgz",
"integrity": "sha512-hcDS0a/Y8ggM3dcOfzH7UpSaogCViWTtNDSl0EXpWIj8g8/r86ITqptjil2hayTXS0Umzb+Yt3mjgpQVFVf2+A==",
"cpu": [
"arm"
],
@@ -2641,9 +2641,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.6.tgz",
"integrity": "sha512-xb72A1aPaaIsp+a88VtRae/ovIEXislbUMyp7b9AQQU7ZkP2m/+ik6DW9TQcJyw5R8/RIailF9zExYwkjR++UA==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.7.tgz",
"integrity": "sha512-awbAr4N7Qd/yjZAQBEEPzgcPveYG2Gi5L6gq1/Bp+3SMRjn5F68Wv1zKsntzvvoioBR6yKvhLtWqu11am1TsxA==",
"cpu": [
"arm64"
],
@@ -2658,9 +2658,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.6.tgz",
"integrity": "sha512-pbcs1BVBwC43hZkjhjlF8Rx8m5G0KnhLZs9Azy/gqccP6UsNSzJr0hKGIAVPqzAH1O/NdPxxC9GVGb4msQg/aw==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.7.tgz",
"integrity": "sha512-Ut7xQJfeLhzu2yN3rHzVvL9ccyPJvN4aCdP1V7hJ5lGxX/eCzgB9SiMk/Md3J+rWWZgvoYsDW4BUFggJr32pBA==",
"cpu": [
"arm64"
],
@@ -2675,9 +2675,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.6.tgz",
"integrity": "sha512-EKOC8os6Vsg487ZLKUqzLF99Jsmnq6tu7O7mAELakNpcA+8KadktFf/JYdRuHe/DzCUXmN38pUKu4XBcKlqGbQ==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.7.tgz",
"integrity": "sha512-trqoni/vHkuM5oo5iQhoKPh6fjjD7E80pMgCXWgGxU8vSRPUlwvG8GeYvEEcB6eJAxNa99WH9iVHPHelgtKiHA==",
"cpu": [
"loong64"
],
@@ -2692,9 +2692,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.6.tgz",
"integrity": "sha512-v/SQ68GnSlr/HyjB79sNXesGDwkOPu9JjbHfmWaymCiM+nZsQ9TstG1KfA4MuEvTr2d3PoY8PhFxj9bt0e32JA==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.7.tgz",
"integrity": "sha512-gvlUy0XFIwtUWiQmhc+4GT2Knm3KosinpUVhr7CtTFA78gFDnE5NWwMPzHrJsYpKkh2LlY7yvBIboN1FAV6Xeg==",
"cpu": [
"loong64"
],
@@ -2709,9 +2709,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.6.tgz",
"integrity": "sha512-G3pmliMQ7r15zFFNwBlaT/xTdSLi4lfgI7CUrQueyKYheNVhrd8JrWCE6Ll8xf4AA1RZFqq+mNJYntiElsVOyA==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.7.tgz",
"integrity": "sha512-8yS/kSXTJ/47Z62JW+C2AJ3vd/8zZw016vFjtNpZwmZlwJumLua4s1i78dkYuSSqQ+0aIMxJcBp/4uIwp2ehjA==",
"cpu": [
"ppc64"
],
@@ -2726,9 +2726,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.6.tgz",
"integrity": "sha512-XQTB/IPoB/wG6x/wu9uomAt8YH61IAr5iZTubMiad/lu88sotq2ocz9hJd556+Jz2kIkF7ZQZIsUuWE3OWqtcA==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.7.tgz",
"integrity": "sha512-mxVSKvvty0hOP1AFo/l4Kpv3i3HK0zx4rYUrirZDToYlWhS3xzmIkq3vUJ7rtrM2cPyXveDyDT6M1FfXZ9hTRA==",
"cpu": [
"ppc64"
],
@@ -2743,9 +2743,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.6.tgz",
"integrity": "sha512-O5mpokth6+ydMuwiGtY8fIjvOBfH7u6SzmDNmZ3k6GK5C6iEAC2q2ZN/T5L9jYILx0IcCT0KnfMDihQ5yQFSBw==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.7.tgz",
"integrity": "sha512-lEse1zra1CagM+uHH2bl28GInDsZZ45NJzHei2OMaXQFzZWKBO1hFo07rS0xB6ayV4WU360UrSmPDSyCMpYsUQ==",
"cpu": [
"riscv64"
],
@@ -2760,9 +2760,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.6.tgz",
"integrity": "sha512-aIFxbAD9v3TvIaEDsKmukjcDg1JS4/tFXw8SHyQQgzOwzWCQ4cyDK7Ju1pQKqUXDFkEu/BZCFDCDATsKhQcB/Q==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.7.tgz",
"integrity": "sha512-d/cqCoiG9WMZ5VuJZ5Qdlv9g97QyoGwnmvHccvGhKSacTy7xFrAS+jMcJSBdMNOw1oacSkl6p7K6O/DRYTKL3g==",
"cpu": [
"riscv64"
],
@@ -2777,9 +2777,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.6.tgz",
"integrity": "sha512-fuNo1UwBc6BxnHDOxxguQL/InhheMN3RJcJMb0b4xP8r5Q3CNCrJbVg0eCIFuJ4e/zAfjR2gYbOfbzO/G7CIyw==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.7.tgz",
"integrity": "sha512-kVEA//CeDOZZP1XCH2JBgqXb9+ALlwuA/4civaazMVdgzaSQC9WM38w65zG/LLk0lA7F9Od19CSBZW7J9mCbNg==",
"cpu": [
"s390x"
],
@@ -2794,9 +2794,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.6.tgz",
"integrity": "sha512-05v6wFKDKJPZs9xlytVrrRu6IbRuoSvlH9xNwEOafXsKyjBFZPvAcZz2VIAB/M+lblYeKa9iOYC4RDVdfcAAaw==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.7.tgz",
"integrity": "sha512-14oLfDsOSr0TSntT/34jRg/V8B2XW/Jomh9zX9CjIM9gwwNlI1OCymS/GVn0DV32h67eRf7U0GGem0rvfXgNwQ==",
"cpu": [
"x64"
],
@@ -2811,9 +2811,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.6.tgz",
"integrity": "sha512-wJXKcdMjvkc/FaavZXNP1yW4i0pfOmhZAIWRSLG2yJ0PB0TWi7dkLErhX0u0vptDcXKKO/m1Y/YTeuyvb2MXwg==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.7.tgz",
"integrity": "sha512-dFVtPfjZqAnUPpPY4Bfg+87yo1IBW9HiY8iCFtg49Pn46MfZTebSHZh8PL6SOsgyt9LQP8Nj/sE6jm3aWpySmQ==",
"cpu": [
"x64"
],
@@ -2828,9 +2828,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.6.tgz",
"integrity": "sha512-cwg7/h0ThvJwy8CBkmACdiHAxRWpbsCmIsjOt+Zzx0hTnIcSUmKYEV03bGg+hD827OnwUsbKh1ZZDW754xWyBA==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.7.tgz",
"integrity": "sha512-Dm9CZ2SM3u2NgvBj4awW/gGqkOwzH8f6MSYIJE6+bKcKLI+T0+LhpD6vtetTfrIIx+rLXDf5RJqBP6v7V5O+wg==",
"cpu": [
"x64"
],
@@ -2842,9 +2842,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.6.tgz",
"integrity": "sha512-T0RkYIJbdYXJEQFw+tMBO09KDFdgmNxkHsof3wWAABrvnx15Yh8RytmrgYy9+q9cBeBNZF66MMQZC+5nzEz6vg==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.7.tgz",
"integrity": "sha512-oaxsvS2mL6+78741CT2HCORBvy1USg3Wqpt5qvZwy3aMXRMl4zxyDo8maeFRWJkF3dp0Wbi71fKH5Gr9YI3vYA==",
"cpu": [
"arm64"
],
@@ -2856,9 +2856,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.6.tgz",
"integrity": "sha512-KlfpmF1AAIdAWxt8QQNClbpLFlAUfYOzqIuJ7IXEStMHpFRuH78RG6FjqPb3Rqhh0K89F5npvG+/i3Fjl6IveQ==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.7.tgz",
"integrity": "sha512-UKZz1Y4mxUqkWh60uH7B1aZn6UcAimPk4ipRktpb3wfZq7JrzZYyAXTwiafkb3T+kd5P3Sf+vzcRdsDks4WtjQ==",
"cpu": [
"arm64"
],
@@ -2870,9 +2870,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.6.tgz",
"integrity": "sha512-MrTLjd7Jlt9/ZavJZYG61KQv9Iy9bzbY+yXy73Xvwoa2uP66wvDShkyBr5Chz/yrE6lbD/zHMuTmKQVNQqBApg==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.7.tgz",
"integrity": "sha512-WWkSuC1J+mZJnsDh0mRaCAFSWfQdl6y3fDjZeWElI6zHo8CcOSG5lY3yurKJjEq+FrX0WD60sFrVVpbHqzMmrw==",
"cpu": [
"ia32"
],
@@ -2884,9 +2884,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.6.tgz",
"integrity": "sha512-8fWDH1CkPI8GdBKAxzHDza7UmgficsKo2+MVbvUgOGhB0629w5sHMg3Qu5y7TZsrYKHHWpz8fla4KvoT7WsO3Q==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.7.tgz",
"integrity": "sha512-u9bp2yXCI3/BUjUxY6uWknAY32pfvvDCPysyUTySotx0dj3ydlFHrRQYfn/b/kZgnfjyEwySMtI7Ga054uiysQ==",
"cpu": [
"x64"
],
@@ -2898,9 +2898,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.6.tgz",
"integrity": "sha512-9/c9PWdr5GCIHkkxiHM1HjNBQMVdftP4O8QTcoUAaRza2FSqJ4axbf8QAnBuE6BIGQDl71SaRPeDdDYid5ecTw==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.7.tgz",
"integrity": "sha512-EhX6N3XPMEnVU5cRfNgzPOKcSucsbu3nNNWuoAaEuKIqmddBKHN5b9d4rPe65T1I+FaOT176JKiZNsUBp3rqbA==",
"cpu": [
"x64"
],
@@ -8447,9 +8447,9 @@
}
},
"node_modules/rollup": {
"version": "4.64.6",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.6.tgz",
"integrity": "sha512-fX1Vy+UNKdXn+xKDnu/HBXbHVr1AY8cSdQj9BU4kLv5aFCMqGpvrVbyUMmjN3DdzWhSAuTbcX8PCNEVaumthYA==",
"version": "4.64.7",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.7.tgz",
"integrity": "sha512-aCiJONoZp0lNGJ6sYqspLkh6UXaWEey/7Ko2TZyXQ4kgzAhDUxAgbt/oulXKmD+zFHXw1k+RF6ZZWOrjp7DZnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8464,31 +8464,31 @@
},
"optionalDependencies": {
"@napi-rs/lzma-linux-x64-gnu": "1.5.1",
"@rollup/rollup-android-arm-eabi": "4.64.6",
"@rollup/rollup-android-arm64": "4.64.6",
"@rollup/rollup-darwin-arm64": "4.64.6",
"@rollup/rollup-darwin-x64": "4.64.6",
"@rollup/rollup-freebsd-arm64": "4.64.6",
"@rollup/rollup-freebsd-x64": "4.64.6",
"@rollup/rollup-linux-arm-gnueabihf": "4.64.6",
"@rollup/rollup-linux-arm-musleabihf": "4.64.6",
"@rollup/rollup-linux-arm64-gnu": "4.64.6",
"@rollup/rollup-linux-arm64-musl": "4.64.6",
"@rollup/rollup-linux-loong64-gnu": "4.64.6",
"@rollup/rollup-linux-loong64-musl": "4.64.6",
"@rollup/rollup-linux-ppc64-gnu": "4.64.6",
"@rollup/rollup-linux-ppc64-musl": "4.64.6",
"@rollup/rollup-linux-riscv64-gnu": "4.64.6",
"@rollup/rollup-linux-riscv64-musl": "4.64.6",
"@rollup/rollup-linux-s390x-gnu": "4.64.6",
"@rollup/rollup-linux-x64-gnu": "4.64.6",
"@rollup/rollup-linux-x64-musl": "4.64.6",
"@rollup/rollup-openbsd-x64": "4.64.6",
"@rollup/rollup-openharmony-arm64": "4.64.6",
"@rollup/rollup-win32-arm64-msvc": "4.64.6",
"@rollup/rollup-win32-ia32-msvc": "4.64.6",
"@rollup/rollup-win32-x64-gnu": "4.64.6",
"@rollup/rollup-win32-x64-msvc": "4.64.6",
"@rollup/rollup-android-arm-eabi": "4.64.7",
"@rollup/rollup-android-arm64": "4.64.7",
"@rollup/rollup-darwin-arm64": "4.64.7",
"@rollup/rollup-darwin-x64": "4.64.7",
"@rollup/rollup-freebsd-arm64": "4.64.7",
"@rollup/rollup-freebsd-x64": "4.64.7",
"@rollup/rollup-linux-arm-gnueabihf": "4.64.7",
"@rollup/rollup-linux-arm-musleabihf": "4.64.7",
"@rollup/rollup-linux-arm64-gnu": "4.64.7",
"@rollup/rollup-linux-arm64-musl": "4.64.7",
"@rollup/rollup-linux-loong64-gnu": "4.64.7",
"@rollup/rollup-linux-loong64-musl": "4.64.7",
"@rollup/rollup-linux-ppc64-gnu": "4.64.7",
"@rollup/rollup-linux-ppc64-musl": "4.64.7",
"@rollup/rollup-linux-riscv64-gnu": "4.64.7",
"@rollup/rollup-linux-riscv64-musl": "4.64.7",
"@rollup/rollup-linux-s390x-gnu": "4.64.7",
"@rollup/rollup-linux-x64-gnu": "4.64.7",
"@rollup/rollup-linux-x64-musl": "4.64.7",
"@rollup/rollup-openbsd-x64": "4.64.7",
"@rollup/rollup-openharmony-arm64": "4.64.7",
"@rollup/rollup-win32-arm64-msvc": "4.64.7",
"@rollup/rollup-win32-ia32-msvc": "4.64.7",
"@rollup/rollup-win32-x64-gnu": "4.64.7",
"@rollup/rollup-win32-x64-msvc": "4.64.7",
"fsevents": "~2.3.2"
}
},
@@ -8607,9 +8607,9 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.105.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.105.1.tgz",
"integrity": "sha512-1CnAWPyrrnVQzXGB/DUep1vYCKCm1CIBr+YxjjiW6wk9qtzemnGP4qicH56T2F9S4Hjtv/Ne3jDz8JqfdDU4oA==",
"version": "1.105.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.105.2.tgz",
"integrity": "sha512-KxW6ZKZiBKWK3ziQ4YZ3cpqo8/1TlHXE8t9nBNvGDZFoBFbk2MD7RacxYFs8zy8hzeFwpFYkVyAhboi47uYPEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11,7 +11,8 @@
"css": "npx sass dev/scss/main.scss:assets/css/main.css -w -q",
"js": "npx esbuild dev/js/main.js --watch --bundle --sourcemap --outfile=assets/js/build.js",
"dev": "npx gulp dev",
"build": "npx gulp build"
"build": "npx gulp build",
"serve": "npx http-server ./dist -p 3000 -c-1 "
},
"devDependencies": {
"@babel/core": "^8.0.7",
@@ -36,8 +37,8 @@
"gulp-sizereport": "^1.2.1",
"gulp-terser": "^2.1.0",
"npm-run-all": "^4.1.5",
"rollup": "^4.64.6",
"sass": "^1.105.1",
"rollup": "^4.64.7",
"sass": "^1.105.2",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0"
},
@@ -0,0 +1,3 @@
node_modules
dist
.DS_Store
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
{
"name": "website-frontend-vite",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Website mit Vite, Nunjucks und SCSS — Nachfolger des gulp-Setups (frontend-gulp)",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@floating-ui/dom": "^1.8.0",
"bootstrap": "6.0.0-alpha.1",
"toastify-js": "^1.12.0",
"uuid": "^14.0.3"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^7.3.1",
"nunjucks": "^3.2.4",
"sass-embedded": "^1.105.2",
"vite": "^8.3.4"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,21 @@
{
"name": "JS Course",
"short_name": "JS",
"icons": [
{
"src": "/favicon/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/favicon/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 642 483">
<path d="M803.541 858.114h641.02v489.042h-641.02z" fill="none" transform="matrix(1 0 0 .98569 -803.541 -845.831)"/>
<path d="m200.993 79.566-1.106 1.106c-4.992 4.992-13.087 4.992-18.08 0L159.18 58.044c-4.992-4.993-4.992-13.087 0-18.08l1.106-1.105c4.993-4.993 13.087-4.993 18.08 0l22.627 22.627c4.992 4.993 4.992 13.088 0 18.08" fill="#75cbcc" fill-rule="nonzero" transform="translate(-222.359 -66.506)scale(3.5623)"/>
<path d="m178.918 101.6 21.522-21.522c5.298-5.298 5.298-13.887 0-19.185a13 13 0 0 0-.618-.577l-17.95-17.95c4.525 5.117 4.525 12.829.003 17.947q-.317.278-.621.58l-21.522 21.521c-5.298 5.298-5.298 13.888 0 19.186 5.299 5.298 13.888 5.298 19.186 0" fill="#ace0e0" fill-rule="nonzero" transform="translate(-222.359 -66.506)scale(3.5623)"/>
<path d="m99.555 61.446 1.106-1.106c4.992-4.993 13.087-4.993 18.08 0l22.628 22.628c4.992 4.993 4.992 13.087 0 18.08l-1.106 1.105c-4.993 4.993-13.087 4.993-18.08 0L99.555 79.525c-4.992-4.993-4.992-13.087 0-18.079" fill="#75cbcc" fill-rule="nonzero" transform="translate(-222.359 -66.506)scale(3.5623)"/>
<path d="m121.63 39.411-21.522 21.522c-5.298 5.298-5.298 13.888 0 19.185q.302.299.617.577l17.95 17.949c-4.524-5.117-4.525-12.829-.003-17.947a13 13 0 0 0 .621-.58l21.522-21.522c5.298-5.298 5.298-13.887 0-19.186-5.297-5.295-13.887-5.295-19.185.002" fill="#ace0e0" fill-rule="nonzero" transform="translate(-222.359 -66.506)scale(3.5623)"/>
<path d="M206.166 330.121q0 5-1.229 8.958t-3.625 6.688q-2.395 2.729-5.896 4.187t-8.041 1.458q-4.125 0-8.417-1.166.084-1.208.208-2.396.125-1.187.209-2.354.083-.708.52-1.146.438-.438 1.355-.438.75 0 1.979.375 1.228.375 3.312.375 2.751 0 4.917-.833a8.7 8.7 0 0 0 3.625-2.583q1.458-1.75 2.229-4.479t.771-6.48v-39.25h8.083zM251.333 300.371q-.375.625-.813.937-.436.313-1.062.313-.708 0-1.667-.709a29 29 0 0 0-2.396-1.562q-1.437-.853-3.458-1.563-2.02-.708-4.896-.708-2.708 0-4.771.729-2.062.73-3.458 1.979a8.1 8.1 0 0 0-2.104 2.938q-.708 1.687-.708 3.646 0 2.499 1.229 4.146 1.229 1.645 3.25 2.812t4.583 2.021a790 790 0 0 1 5.25 1.771 61 61 0 0 1 5.25 2.062 18.5 18.5 0 0 1 4.583 2.896q2.022 1.75 3.25 4.292 1.23 2.542 1.23 6.25 0 3.916-1.334 7.354a16.9 16.9 0 0 1-3.896 5.979q-2.562 2.542-6.291 4t-8.479 1.458q-5.834 0-10.584-2.104t-8.125-5.687l2.334-3.834a3.1 3.1 0 0 1 .812-.77q.48-.313 1.063-.313.541 0 1.229.438.687.436 1.562 1.104.876.666 2 1.458t2.563 1.458q1.437.667 3.271 1.105t4.125.437q2.875 0 5.125-.792 2.25-.79 3.812-2.229a9.66 9.66 0 0 0 2.396-3.437q.834-2 .833-4.459 0-2.707-1.229-4.437t-3.229-2.896-4.583-1.979a978 978 0 0 1-5.25-1.667 56 56 0 0 1-5.25-1.979 17 17 0 0 1-4.584-2.917q-2-1.791-3.229-4.479-1.229-2.687-1.229-6.646 0-3.166 1.229-6.125a15.7 15.7 0 0 1 3.583-5.25q2.355-2.291 5.792-3.666 3.438-1.375 7.896-1.375 5 0 9.104 1.583a21.5 21.5 0 0 1 7.229 4.583zM329.25 338.412q.666 0 1.166.542l3.209 3.458q-3.667 4.25-8.896 6.625t-12.646 2.375q-6.417 0-11.667-2.229t-8.958-6.25q-3.708-4.02-5.75-9.646-2.042-5.625-2.042-12.375t2.125-12.375 5.979-9.666q3.855-4.042 9.23-6.271 5.374-2.229 11.875-2.229 6.375 0 11.25 2.041 4.875 2.043 8.583 5.542l-2.667 3.708a2.2 2.2 0 0 1-.646.688q-.395.271-1.062.271-.75 0-1.833-.813-1.084-.812-2.834-1.812t-4.375-1.813-6.458-.812q-4.625 0-8.458 1.604a18.5 18.5 0 0 0-6.605 4.646q-2.77 3.042-4.312 7.416-1.542 4.376-1.542 9.875 0 5.584 1.604 9.959t4.375 7.396a18.5 18.5 0 0 0 6.542 4.604q3.77 1.583 8.146 1.583 2.667 0 4.812-.312a19.7 19.7 0 0 0 3.959-.98 16.6 16.6 0 0 0 3.375-1.687 24 24 0 0 0 3.104-2.438q.708-.625 1.417-.625M397 320.912q0 6.71-2.125 12.313t-6 9.646-9.313 6.271-12.021 2.229-12-2.229-9.291-6.271q-3.876-4.042-6-9.646t-2.125-12.313q0-6.708 2.125-12.312t6-9.667q3.875-4.062 9.291-6.312t12-2.25 12.021 2.25 9.313 6.312 6 9.667Q397 314.204 397 320.912m-8.292 0q0-5.5-1.5-9.875-1.5-4.374-4.25-7.395t-6.667-4.646q-3.916-1.625-8.75-1.625-4.791 0-8.708 1.625t-6.688 4.646q-2.77 3.021-4.27 7.395-1.5 4.376-1.5 9.875 0 5.5 1.5 9.855 1.5 4.353 4.27 7.375 2.772 3.02 6.688 4.625t8.708 1.604q4.834 0 8.75-1.604t6.667-4.625q2.75-3.022 4.25-7.375 1.5-4.355 1.5-9.855M431.583 344.329q3.708 0 6.625-1.25t4.937-3.5q2.021-2.25 3.084-5.375 1.062-3.125 1.062-6.875v-36.292h8.042v36.292q0 5.167-1.646 9.583-1.646 4.418-4.708 7.646-3.063 3.23-7.479 5.063t-9.917 1.833-9.917-1.833-7.5-5.063q-3.083-3.228-4.729-7.646-1.646-4.416-1.646-9.583v-36.292h8.042v36.25q0 3.75 1.062 6.875t3.084 5.375q2.02 2.25 4.958 3.521 2.938 1.272 6.646 1.271M486.375 319.954q3.5 0 6.187-.854t4.5-2.417a10 10 0 0 0 2.729-3.729q.917-2.166.917-4.792 0-5.333-3.5-8.041t-10.5-2.709h-8.833v22.542zm27.583 30.792h-7.167q-2.208 0-3.25-1.709l-15.5-21.333q-.708-1-1.521-1.437t-2.52-.438h-6.125v24.917h-8.042v-59.709h16.875q5.667 0 9.792 1.146t6.812 3.313 3.979 5.229 1.292 6.854q0 3.166-1 5.917a15.6 15.6 0 0 1-2.896 4.937q-1.896 2.188-4.625 3.729-2.729 1.542-6.187 2.334 1.54.875 2.666 2.541zM552.583 300.371q-.375.625-.813.937t-1.062.313q-.708 0-1.667-.709a29 29 0 0 0-2.396-1.562q-1.437-.853-3.458-1.563-2.02-.708-4.896-.708-2.708 0-4.771.729-2.062.73-3.458 1.979a8.1 8.1 0 0 0-2.104 2.938q-.708 1.687-.708 3.646 0 2.499 1.229 4.146 1.229 1.645 3.25 2.812t4.583 2.021a790 790 0 0 1 5.25 1.771 61 61 0 0 1 5.25 2.062 18.5 18.5 0 0 1 4.583 2.896q2.022 1.75 3.25 4.292 1.23 2.542 1.23 6.25 0 3.916-1.334 7.354a16.9 16.9 0 0 1-3.896 5.979q-2.562 2.542-6.291 4t-8.479 1.458q-5.834 0-10.584-2.104t-8.125-5.687l2.334-3.834a3.1 3.1 0 0 1 .812-.77q.48-.313 1.063-.313.541 0 1.229.438.687.436 1.562 1.104.875.666 2 1.458t2.563 1.458q1.437.667 3.271 1.105t4.125.437q2.875 0 5.125-.792 2.25-.79 3.812-2.229a9.66 9.66 0 0 0 2.396-3.437q.833-2 .833-4.459 0-2.707-1.229-4.437t-3.229-2.896-4.583-1.979a978 978 0 0 1-5.25-1.667 56 56 0 0 1-5.25-1.979 17 17 0 0 1-4.584-2.917q-2-1.791-3.229-4.479-1.229-2.687-1.229-6.646 0-3.166 1.229-6.125a15.7 15.7 0 0 1 3.583-5.25q2.355-2.291 5.792-3.666 3.438-1.375 7.896-1.375 5 0 9.104 1.583a21.5 21.5 0 0 1 7.229 4.583zM604.083 344.162l-.042 6.584H567.25v-59.709h36.791v6.584h-28.708v19.833h23.25v6.333h-23.25v20.375z" fill-rule="nonzero" transform="translate(-78.559 53)"/>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

@@ -0,0 +1,15 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title | default('Website') }}</title>
<link rel="icon" type="image/png" href="%BASE_URL%favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="%BASE_URL%favicon/favicon.svg" />
<link rel="shortcut icon" href="%BASE_URL%favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="%BASE_URL%favicon/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="JS Course" />
<link rel="manifest" href="%BASE_URL%favicon/site.webmanifest" />
<link rel="stylesheet" href="/src/scss/main.scss" />
<script type="module" src="/src/scripts/main.js"></script>
</head>
@@ -0,0 +1,8 @@
<header class="header-main">
<div class="content-box">
<figure class="logo-main">
<img src="%BASE_URL%img/logo-js.min.svg" alt="Logo - JS Course" />
<figcaption>JS course</figcaption>
</figure>
</div>
</header>
@@ -0,0 +1,44 @@
{#
Bootstrap 6 navbar. Each page sets `{% set active = '…' %}` before including this file;
the matching link gets .active + aria-current. Below md the links move into a drawer.
#}
<nav class="navbar md:navbar-expand bg-1 menu-main" data-bs-theme="dark" aria-label="Main navigation">
<div class="container-fluid">
<a class="navbar-brand" href="%BASE_URL%">JS course</a>
<button
class="btn-icon navbar-toggler"
type="button"
data-bs-toggle="drawer"
data-bs-target="#menu-main-drawer"
aria-controls="menu-main-drawer"
aria-expanded="false"
aria-label="Open navigation">
<span class="navbar-toggler-icon" aria-hidden="true"></span>
</button>
<dialog class="drawer drawer-end" tabindex="-1" id="menu-main-drawer" aria-labelledby="menu-main-drawer-label">
<div class="drawer-header">
<h2 class="drawer-title fs-lg" id="menu-main-drawer-label">Menu</h2>
<button type="button" class="btn-close" data-bs-dismiss="drawer" aria-label="Close"></button>
</div>
<div class="drawer-body">
<ul class="nav navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link{% if active == 'home' %} active" aria-current="page{% endif %}" href="%BASE_URL%">Home</a>
</li>
<li class="nav-item">
<button class="nav-link{% if active == 'projects' %} active{% endif %}" type="button" data-bs-toggle="menu" aria-expanded="false">
Projects
</button>
<div class="menu">
<a class="menu-item" href="%BASE_URL%projects/01_jokes.html">01 Chuck Norris Jokes</a>
<a class="menu-item" href="%BASE_URL%projects/02_product-manager.html">02 Product Manager</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link{% if active == 'contact' %} active" aria-current="page{% endif %}" href="%BASE_URL%contact.html">Contact</a>
</li>
</ul>
</div>
</dialog>
</div>
</nav>
@@ -0,0 +1,179 @@
<!-- ▼ Product Manager ▼ -->
<div class="product-manager">
<div class="container py-12">
<h1>Productmanager</h1>
<nav class="nav-products-actions">
<ul class="list">
<li>
<button class="btn btn-dark button-products-save">
<i class="fas fa-floppy-disk"></i>
<span>Save Products</span>
</button>
</li>
<li>
<button class="btn btn-danger button-products-reset">
<i class="fas fa-box-archive"></i>
<span>Reset Products</span>
</button>
</li>
</ul>
</nav>
<table class="table table-striped table-products">
<thead class="theme-inverse">
<tr>
<th class="th-dragger" scope="col">#</th>
<th class="th-name" scope="col">Name</th>
<th class="th-price" scope="col">Price in &euro;</th>
<th class="th-actions" scope="col">Actions</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="4">
<div class="row">
<div class="col-12 md:col-6 lg:col-7">
<input
type="text"
name="product-name"
class="form-control input-product-name"
placeholder="Insert product name"
aria-label="Product name" />
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="input-group">
<input
type="number"
name="product-price"
class="form-control input-product-price"
placeholder="00.00"
aria-label="Product price"
min="0"
step="0.01" />
<span class="input-group-text">&euro;</span>
</div>
</div>
<div class="col-12 md:col-6 lg:col">
<button type="button" class="btn-solid theme-inverse button-product-add" disabled>
<i class="fas fa-plus" aria-hidden="true"></i>
Add Product
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
<template class="template-row">
<tr>
<td class="td-dragger">
<button type="button" class="btn-text btn-sm button-product-drag">
<i class="fas fa-grip-vertical" aria-hidden="true"></i>
<span class="visually-hidden">Drag Product</span>
</button>
</td>
<td class="td-name"></td>
<td class="td-price"></td>
<td class="td-actions">
<button
type="button"
class="btn-solid theme-primary btn-sm button-product-edit"
data-bs-toggle="dialog"
data-bs-target="#dialog-product-edit">
<i class="fas fa-pen" aria-hidden="true"></i>
<span class="visually-hidden">Edit Product</span>
</button>
<button type="button" class="btn-solid theme-danger btn-sm button-product-remove">
<i class="fas fa-trash-can" aria-hidden="true"></i>
<span class="visually-hidden">Remove Product</span>
</button>
<button type="button" class="btn-solid theme-secondary btn-sm button-product-move-up">
<i class="fas fa-chevron-up" aria-hidden="true"></i>
<span class="visually-hidden">Move up</span>
</button>
<button type="button" class="btn-solid theme-secondary btn-sm button-product-move-down">
<i class="fas fa-chevron-down" aria-hidden="true"></i>
<span class="visually-hidden">Move down</span>
</button>
</td>
</tr>
</template>
<!-- {# Bootstrap 6 Dialog (native <dialog>) — replaces the Bootstrap 5 Modal from 01.
data-bs-backdrop="static": a click outside does not discard the edit. #} -->
<dialog
class="dialog dialog-edit"
id="dialog-product-edit"
aria-labelledby="dialog-product-edit-label"
data-bs-backdrop="static">
<form class="form-product-edit" method="post">
<div class="dialog-header">
<h2 class="dialog-title" id="dialog-product-edit-label">Edit Product</h2>
<button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
</div>
<div class="dialog-body">
<div class="row">
<div class="col-12 md:col-8">
<div class="mb-5">
<label for="input-edit-name" class="form-label">Product Name</label>
<input type="text" id="input-edit-name" class="form-control input-edit-name" name="name" />
</div>
</div>
<div class="col-12 md:col">
<div class="mb-5">
<label for="input-edit-price" class="form-label">Product price</label>
<div class="input-group">
<input
type="number"
step="0.01"
min="0"
id="input-edit-price"
class="form-control input-edit-price"
name="price" />
<span class="input-group-text">&euro;</span>
</div>
<input type="hidden" name="id" class="input-edit-id" />
<input type="hidden" name="position" class="input-edit-position" />
</div>
</div>
</div>
</div>
<div class="dialog-footer">
<button type="button" class="btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
<button type="submit" class="btn-solid theme-primary button-product-update">
<i class="fas fa-arrow-rotate-right" aria-hidden="true"></i>
Update Product
</button>
</div>
</form>
</dialog>
<!-- ▼ Dialog Delete ▼ -->
<dialog
class="dialog dialog-delete"
id="dialog-product-delete"
aria-labelledby="dialog-product-delete-label"
data-bs-backdrop="static">
<div class="dialog-header">
<h2 class="dialog-title" id="dialog-product-delete-label">Delete Product</h2>
<button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
</div>
<div class="dialog-body">
<p>
Do you really want to delete Product:<br />
<strong class="product-name">PRODUCT_NAME</strong>
</p>
</div>
<div class="dialog-footer">
<button type="button" class="btn-solid theme-secondary" data-bs-dismiss="dialog">Cancel</button>
<button type="button" class="btn-solid theme-danger button-confirm-delete">Confirm to delete</button>
</div>
</dialog>
<!-- ▲ /Dialog Delete ▲ -->
</div>
<!-- ▲ /Product Manager ▲ -->
@@ -0,0 +1,81 @@
<!doctype html>
<html lang="de">
{% set title = 'Contact' %}
{% set active = 'contact' %}
{% include "head.html" %}
<body>
{% include "header-main.html" %}
{% include "menu-main.html" %}
<main>
<div class="container py-12">
<h1>Kontakt</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, iste?</p>
<form class="form-contact">
<div class="row">
<!-- .col>.mb-3>label.form-label -->
<div class="col-12 md:col-6">
<div class="mb-5">
<label for="input-firstname" class="form-label">First name:</label>
<input
type="text"
name="firstname"
id="input-firstname"
class="form-control input-firstname"
required />
</div>
</div>
<!-- ▲ /col ▲ -->
<!-- .col>.mb-3>label.form-label -->
<div class="col-12 md:col-6">
<div class="mb-5">
<label for="input-lastname" class="form-label">Last name:</label>
<input type="text" name="lastname" id="input-lastname" class="form-control input-lastname" required />
</div>
</div>
<!-- ▲ /col ▲ -->
</div>
<!-- .row>.col>.mb-3>label.form-label+input:email.form-control -->
<div class="row">
<div class="col">
<div class="mb-5">
<label for="input-email" class="form-label">E-mail</label>
<input type="email" name="email" id="input-email" class="form-control input-email" required />
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="mb-5">
<label for="input-subject" class="form-label">Subject</label>
<input type="text" name="subject" id="input-subject" class="form-control input-subject" />
</div>
</div>
</div>
<!-- .row>.col>.mb-3>label.form-label+textarea.form-control -->
<div class="row">
<div class="col">
<div class="mb-5">
<label for="textarea-message" class="form-label">Message</label>
<textarea
name="message"
id="textarea-message"
cols="12"
rows="8"
class="form-control textarea-message"></textarea>
</div>
</div>
</div>
<!-- .row>.col>.mb-3>button:submit.btn.btn-dark.button-submit -->
<div class="row">
<div class="col">
<div class="mb-5">
<button type="submit" class="btn-solid theme-inverse button-submit">Send</button>
</div>
</div>
</div>
</form>
</div>
</main>
</body>
</html>
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="de">
{% set title = 'Website with Vite, Nunjucks, REST-Api' %} {% set active = 'home' %} {% include "head.html" %}
<body>
{% include "header-main.html" %} {% include "menu-main.html" %}
<main>
<div class="container py-12">
<h1>Webseite mit HTML &amp; CSS (SCSS)</h1>
<p>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Dolorem, earum? Harum fuga itaque a natus tempora
dolor vero officiis eos tempore! Impedit nobis ea eum labore, aut assumenda nostrum dicta.
</p>
<div class="card" style="width: 18rem">
<img src="https://picsum.photos/300/200" class="card-img-top" alt="" />
<div class="card-body">
<h5 class="card-title">Card title</h5>
<p class="card-text">
Some quick example text to build on the card title and make up the bulk of the card’s content.
</p>
<a href="#" class="btn-solid theme-primary">Go somewhere</a>
</div>
</div>
</div>
</main>
</body>
</html>
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="de">
{% set title = 'Chuck Norris Joke (API)' %}
{% set active = 'projects' %}
{% include "head.html" %}
<body>
{% include "menu-main.html" %}
<main>
<section class="section-joke">
<div class="chuck-norris-joke">
<blockquote class="joke-text" aria-live="polite"></blockquote>
<button type="button" class="btn-solid theme-inverse button-get-joke">Get Joke</button>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="de">
{% set title = 'Productmanager' %}
{% set active = 'projects' %}
{% include "head.html" %}
<body>
{% include "menu-main.html" %}
<main>{% include "product-manager.html" %}</main>
</body>
</html>
@@ -0,0 +1,14 @@
// Bootstrap 6 JS for the navbar: Drawer (mobile menu) and Menu (Projects dropdown).
// Importing them is enough — they register their data-bs-* handlers on import.
import 'bootstrap/js/dist/drawer.js';
import 'bootstrap/js/dist/menu.js';
// Page modules load only on the page that contains their markup
// (replaces the pathname switch/router from frontend-gulp).
if (document.querySelector('.chuck-norris-joke')) {
import('./modules/ChuckNorrisJoke.js').then(({ default: ChuckNorrisJoke }) => ChuckNorrisJoke());
}
if (document.querySelector('.product-manager')) {
import('./modules/ProductManager.js').then(({ default: ProductManager }) => ProductManager());
}
@@ -0,0 +1,38 @@
// Fetches a random joke from the public Chuck Norris API and shows it in the joke stage.
// The jokes page in frontend-gulp had the markup but no script yet.
const API_URL = 'https://api.chucknorris.io/jokes/random'
const ChuckNorrisJoke = (el = document.querySelector('.chuck-norris-joke')) => {
if (!el) return
const text = el.querySelector('.joke-text')
const button = el.querySelector('.button-get-joke')
if (!text || !button) return
const fetchJoke = async () => {
const response = await fetch(API_URL)
if (!response.ok) throw new Error(`Joke API answered ${response.status}`)
const data = await response.json()
return data.value
}
const showJoke = async () => {
button.disabled = true
try {
text.textContent = await fetchJoke()
} catch (error) {
console.error(error)
text.textContent = 'No joke right now — the API is not reachable.'
} finally {
button.disabled = false
}
}
button.addEventListener('click', showJoke)
showJoke()
return { element: el, showJoke }
}
export default ChuckNorrisJoke
@@ -0,0 +1,433 @@
import { Dialog } from 'bootstrap';
import Toast from './Toast';
// Factory Function
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;
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'),
// Dialog Edit (<dialog>)
dialogEdit: module.querySelector('.dialog-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'),
// Dialog Delete (<dialog>)
dialogDelete: module.querySelector('.dialog-delete'),
productName: module.querySelector('strong.product-name'),
btnConfirmDelete: module.querySelector('.button-confirm-delete'),
};
// Bootstrap 6 Dialog Plugin Instanzen
const bsDialogEdit = new Dialog(DOM.dialogEdit, {
backdrop: 'static',
keyboard: true,
});
const bsDialogDelete = DOM.dialogDelete
? new Dialog(DOM.dialogDelete, {
backdrop: 'static',
keyboard: true,
})
: null;
// State für Drag & Drop
let draggedRow = null;
// === INIT =============
const init = async () => {
// Funktionsaufrufe zu Beginn der Anwendung
await initProducts();
DOM.btnAdd.disabled = true;
// Event-Lauscher zu Beginn der Anwendung
DOM.btnAdd.addEventListener('click', onClickAdd);
DOM.formEdit.addEventListener('submit', onSubmitEdit);
DOM.btnConfirmDelete?.addEventListener('click', onClickConfirmDelete);
DOM.inputName.addEventListener('input', onInputValidate);
DOM.inputPrice.addEventListener('input', onInputValidate);
// Delegierte Tabellen- & Drag-Events
DOM.tbody.addEventListener('click', onTableClick);
setupDragAndDrop();
};
// === EVENTHANDLER =====
const onInputValidate = () => {
DOM.btnAdd.disabled = DOM.inputName.value.trim() === '' || DOM.inputPrice.value === '';
};
const onSubmitEdit = async (e) => {
e.preventDefault();
const product = {
name: DOM.inputEditName.value.trim(),
price: Number(DOM.inputEditPrice.value),
_id: DOM.inputEditId.value,
position: Number(DOM.inputEditPosition.value),
};
DOM.btnUpdate.querySelector('i')?.classList.add('fa-spin');
try {
const data = await api.edit(product);
if (data.success) {
Toast(data.msg, 'success').show();
loadProducts();
resetFields();
bsDialogEdit.hide();
}
} catch (err) {
console.error('Update fehlgeschlagen:', err);
} finally {
DOM.btnUpdate.querySelector('i')?.classList.remove('fa-spin');
}
};
const onClickAdd = async () => {
const product = {
name: DOM.inputName.value.trim(),
price: Number(DOM.inputPrice.value),
};
try {
const data = await api.add(product);
if (data.success) {
Toast(data.msg, 'success').show();
loadProducts();
resetFields();
}
} catch (err) {
console.error('Erstellen fehlgeschlagen:', err);
}
};
const onTableClick = async (e) => {
const btnEl = e.target.closest('button');
if (!btnEl) return;
const currentRow = parent(btnEl, 'tr');
if (!currentRow) return;
const id = currentRow.dataset.id;
if (btnEl.matches('.button-product-edit')) {
try {
const data = await api.read(id);
showDialogEdit(data);
} catch (err) {
console.error('Laden für Bearbeitung fehlgeschlagen:', err);
}
} else if (btnEl.matches('.button-product-remove')) {
const productName = currentRow.querySelector('.td-name')?.textContent.trim() || 'Produkt';
if (bsDialogDelete && DOM.btnConfirmDelete) {
DOM.btnConfirmDelete.dataset.id = id;
if (DOM.productName) DOM.productName.textContent = productName;
bsDialogDelete.show();
} else {
// Fallback falls kein Delete-Dialog im DOM existiert
if (!window.confirm(`Möchtest du "${productName}" wirklich löschen?`)) return;
try {
const data = await api.destroy(id);
if (data.success) {
Toast(data.msg, 'success').show();
loadProducts();
}
} catch (err) {
console.error('Löschen fehlgeschlagen:', err);
}
}
} else if (btnEl.matches('.button-product-move-up') && currentRow.previousElementSibling) {
currentRow.previousElementSibling.before(currentRow);
disableNonFunctionalButtons();
await updateRowPositions();
} else if (btnEl.matches('.button-product-move-down') && currentRow.nextElementSibling) {
currentRow.nextElementSibling.after(currentRow);
disableNonFunctionalButtons();
await updateRowPositions();
}
};
const onClickConfirmDelete = async () => {
const id = DOM.btnConfirmDelete.dataset.id;
if (!id) return;
try {
const data = await api.destroy(id);
if (data.success) {
Toast(data.msg, 'success').show();
bsDialogDelete?.hide();
loadProducts();
}
} catch (err) {
console.error('Löschen fehlgeschlagen:', err);
}
};
// Drag 'n Drop Event-Setup via Delegation
const setupDragAndDrop = () => {
let isHandleMouseDown = false;
// 1. Nur wenn die Maus auf den Drag-Button drückt, erlauben wir das Ziehen der Zeile
DOM.tbody.addEventListener('mousedown', (e) => {
const handle = e.target.closest('.button-product-drag, .button-drag');
if (!handle) {
isHandleMouseDown = false;
return;
}
const currentRow = parent(handle, 'tr');
if (currentRow) {
isHandleMouseDown = true;
currentRow.setAttribute('draggable', 'true');
}
});
DOM.tbody.addEventListener('mouseup', () => {
isHandleMouseDown = false;
});
// 2. Drag Start: Startet den nativen Browser-Drag-Vorgang
DOM.tbody.addEventListener('dragstart', (e) => {
const currentRow = parent(e.target, 'tr');
if (!currentRow || !isHandleMouseDown) {
e.preventDefault();
return;
}
draggedRow = currentRow;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', currentRow.dataset.id || 'drag');
currentRow.classList.add('is-dragging');
});
// 3. Drag Over: Pflicht, um drop zu gestatten
DOM.tbody.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
// 4. Drop: Verschieben im DOM und Server-Aktualisierung
DOM.tbody.addEventListener('drop', async (e) => {
e.preventDefault();
const currentRow = parent(e.target, 'tr');
if (!currentRow || !draggedRow || currentRow === draggedRow) return;
const rows = [...DOM.tbody.children];
const sourceIdx = rows.indexOf(draggedRow);
const targetIdx = rows.indexOf(currentRow);
if (sourceIdx < targetIdx) {
currentRow.after(draggedRow);
} else {
currentRow.before(draggedRow);
}
disableNonFunctionalButtons();
await updateRowPositions();
});
// 5. Drag End & Cleanup
DOM.tbody.addEventListener('dragend', () => {
isHandleMouseDown = false;
if (draggedRow) {
draggedRow.removeAttribute('draggable');
draggedRow.classList.remove('is-dragging');
draggedRow = null;
}
});
};
// === XHR/FETCH (DRY BREAD API) ========
const api = {
async request(url, { data, ...options } = {}) {
const isJsonPayload = data !== undefined;
try {
const response = await fetch(url, {
...options,
headers: {
...(isJsonPayload && { 'Content-Type': 'application/json' }),
...options.headers,
},
body: isJsonPayload ? JSON.stringify(data) : options.body,
});
if (!response.ok) {
const errorJson = await response.json().catch(() => null);
const errorMsg = errorJson?.msg || `${options.method ?? 'GET'} ${url} -> ${response.status}`;
throw new Error(errorMsg);
}
if (response.status === 204) return null;
return await response.json();
} catch (error) {
Toast(error.message || 'Verbindungsfehler zum Server', 'error').show();
throw error;
}
},
browse: () => api.request(API_PRODUCTS),
read: (id) => api.request(`${API_PRODUCTS}/${id}`),
edit: (product) => api.request(API_PRODUCTS, { method: 'PUT', data: product }),
add: (product) => api.request(API_PRODUCTS, { method: 'POST', data: product }),
destroy: (id) => api.request(`${API_PRODUCTS}/${id}`, { method: 'DELETE' }),
updatePositions: (positions) => api.request(`${API_PRODUCTS}/positions`, { method: 'PUT', data: positions }),
};
// === FUNCTIONS ========
const initProducts = async () => {
try {
const products = await api.browse();
DOM.tbody.replaceChildren(...products.map(createRowElement));
Toast('Init products').show();
disableNonFunctionalButtons();
} catch (err) {
console.error('Initialisierung fehlgeschlagen:', err);
}
};
const loadProducts = async () => {
try {
const products = await api.browse();
DOM.tbody.replaceChildren(...products.map(createRowElement));
disableNonFunctionalButtons();
} catch (err) {
console.error('Neu laden fehlgeschlagen:', err);
}
};
const createRowElement = (product) => {
const { name, price, _id: id, position = 0 } = product;
const trEl = DOM.templateRow.content.firstElementChild.cloneNode(true);
trEl.dataset.id = id;
trEl.dataset.position = position;
trEl.querySelector('.td-name').textContent = name;
trEl.querySelector('.td-price').textContent = Number(price).toFixed(2);
return trEl;
};
const showDialogEdit = (product) => {
const { name = '', price = 0, _id: id, position } = product;
DOM.inputEditName.value = name;
DOM.inputEditPrice.value = price;
DOM.inputEditId.value = id;
DOM.inputEditPosition.value = position ?? 0;
bsDialogEdit.show();
};
const resetFields = () => {
DOM.inputName.value = '';
DOM.inputPrice.value = '';
DOM.btnAdd.disabled = true;
DOM.formEdit.reset();
};
const disableNonFunctionalButtons = () => {
const rows = [...DOM.tbody.children];
rows.forEach((tr, index) => {
const btnUp = tr.querySelector('.button-product-move-up');
const btnDown = tr.querySelector('.button-product-move-down');
if (btnUp) btnUp.disabled = index === 0;
if (btnDown) btnDown.disabled = index === rows.length - 1;
});
};
const updateRowPositions = async () => {
const rows = [...DOM.tbody.children];
const positions = rows.map((tr, index) => {
const newPos = index + 1;
tr.dataset.position = newPos;
return {
_id: tr.dataset.id,
position: newPos,
};
});
try {
const data = await api.updatePositions(positions);
if (data.success) {
Toast(data.msg, 'success').show();
}
} catch (err) {
console.error('Fehler beim Aktualisieren der Positionen:', err);
}
};
// === HELPERS ==========
const parent = (el, selector, boundary = DOM.tbody) => {
let current = el;
while (current && current !== document) {
if (current.matches(selector) && (!boundary || current.parentElement === boundary)) {
return current;
}
current = current.parentElement;
}
return null;
};
// Selbstanstoß & Interface
init();
return {
element: DOM.module,
init,
initProducts,
loadProducts,
resetFields,
};
};
export default ProductManager;
@@ -0,0 +1,75 @@
import Toastify from 'toastify-js'; // das Modul wird vom node_modules Ordner ausgelesen und gebundlet. Funktioniert nur mit JS-Bundler (rollup/esbuild)
const Toast = (text = '', variantOrOptions = 'primary') => {
// Entweder string oder Objekt
const userOptions =
typeof variantOrOptions === 'string'
? { variant: variantOrOptions } //
: { ...variantOrOptions };
const defaultOptions = {
variant: 'primary',
duration: 2000,
gravity: 'top',
position: 'center',
stopOnFocus: true,
};
const options = { ...defaultOptions, ...userOptions };
const getColorByName = (variant = '') => {
let color;
switch (variant.trim().toLowerCase()) {
case 'primary':
color = '#0d6efd';
break;
case 'secondary':
color = '#6c757d';
break;
case 'warning':
color = '#ffc720';
break;
case 'success':
color = '#13653f';
break;
case 'info':
color = '#25cff2';
break;
case 'error':
case 'danger':
color = '#a52834';
break;
case 'light':
color = '#babbbc';
break;
case 'dark':
color = '#373b3e';
break;
default:
console.warn('variant not found');
color = 'white';
}
return color;
};
const show = () => {
const { variant, style, ...restProps } = options;
Toastify({
text,
className: variant,
...restProps,
style: {
background: getColorByName(variant),
...style, // Ermöglicht bei Bedarf auch individuelle CSS-Overrides
},
}).showToast();
};
return {
show,
};
};
export default Toast;
@@ -0,0 +1,3 @@
// Bootstrap 6 (6.0.0-alpha.1) straight from node_modules.
// @forward 'bootstrap/scss/bootstrap-grid'
@forward 'bootstrap/scss/bootstrap';
@@ -0,0 +1,8 @@
// gloria-hallelujah-regular - latin
@font-face {
font-display: swap;
font-family: 'Gloria Hallelujah';
font-style: normal;
font-weight: 400;
src: url('../../assets/fonts/gloria-hallelujah-v24-latin/gloria-hallelujah-v24-latin-regular.woff2') format('woff2');
}
@@ -0,0 +1,3 @@
@forward 'gloria-hallelujah-v24-latin';
@forward 'lato-v25-latin';
@forward 'source-sans-3-v19-latin';
@@ -0,0 +1,89 @@
// lato-100 - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: normal;
font-weight: 100;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-100.woff2') format('woff2');
}
// lato-100italic - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: italic;
font-weight: 100;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-100italic.woff2') format('woff2');
}
// lato-300 - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: normal;
font-weight: 300;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-300.woff2') format('woff2');
}
// lato-300italic - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: italic;
font-weight: 300;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-300italic.woff2') format('woff2');
}
// lato-regular - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-regular.woff2') format('woff2');
}
// lato-italic - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: italic;
font-weight: 400;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-italic.woff2') format('woff2');
}
// lato-700 - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-700.woff2') format('woff2');
}
// lato-700italic - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: italic;
font-weight: 700;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-700italic.woff2') format('woff2');
}
// lato-900 - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: normal;
font-weight: 900;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-900.woff2') format('woff2');
}
// lato-900italic - latin
@font-face {
font-display: swap;
font-family: 'Lato';
font-style: italic;
font-weight: 900;
src: url('../../assets/fonts/lato-v25-latin/lato-v25-latin-900italic.woff2') format('woff2');
}
@@ -0,0 +1,143 @@
// source-sans-3-200 - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 200;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-200.woff2') format('woff2');
}
// source-sans-3-200italic - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: italic;
font-weight: 200;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-200italic.woff2') format('woff2');
}
// source-sans-3-300 - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 300;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-300.woff2') format('woff2');
}
// source-sans-3-300italic - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: italic;
font-weight: 300;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-300italic.woff2') format('woff2');
}
// source-sans-3-regular - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 400;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-regular.woff2') format('woff2');
}
// source-sans-3-italic - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: italic;
font-weight: 400;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-italic.woff2') format('woff2');
}
// source-sans-3-500 - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 500;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-500.woff2') format('woff2');
}
// source-sans-3-500italic - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: italic;
font-weight: 500;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-500italic.woff2') format('woff2');
}
// source-sans-3-600 - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 600;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-600.woff2') format('woff2');
}
// source-sans-3-600italic - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: italic;
font-weight: 600;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-600italic.woff2') format('woff2');
}
// source-sans-3-700 - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 700;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-700.woff2') format('woff2');
}
// source-sans-3-700italic - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: italic;
font-weight: 700;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-700italic.woff2') format('woff2');
}
// source-sans-3-800 - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 800;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-800.woff2') format('woff2');
}
// source-sans-3-800italic - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: italic;
font-weight: 800;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-800italic.woff2') format('woff2');
}
// source-sans-3-900 - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 900;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-900.woff2') format('woff2');
}
// source-sans-3-900italic - latin
@font-face {
font-display: swap;
font-family: 'Source Sans 3';
font-style: italic;
font-weight: 900;
src: url('../../assets/fonts/source-sans-3-v19-latin/source-sans-3-v19-latin-900italic.woff2') format('woff2');
}
@@ -0,0 +1,13 @@
/* Color Theme Swatches in Hex */
$color-1: #5c4b51;
$color-2: #8cbeb2;
$color-3: #f2ebbf;
$color-4: #f3b562;
$color-5: #f06060;
$color-red: $color-5;
$color-white: white; // falls weiß geändert wird z.B.: #f8f8f8
$color-black: #101010;
$color-dark-blue: #283e4a;
$linear-gradient-dark: linear-gradient(180deg, rgba(40, 62, 74, 1) 0%, rgba(24, 32, 38, 1) 100%);
@@ -0,0 +1,2 @@
// Variables (colors, typography) are pulled in by the partials that need them via @use.
@forward 'theme';
@@ -0,0 +1,32 @@
@use 'typography' as *;
// Allgemeine Formatierung
*,
html {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
padding: 0;
font-family: $font-copy;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: $font-headline;
font-weight: 600;
}
blockquote {
font-family: $font-quote;
}
@@ -0,0 +1,9 @@
$fallback-serif: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
$fallback-sans-serif: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
$font-headline: 'Lato', $fallback-serif;
$font-subclaim: $fallback-sans-serif;
$font-menu: 'Source Sans 3', $fallback-sans-serif;
$font-copy: 'Source Sans 3', $fallback-sans-serif;
$font-quote: 'Gloria Hallelujah', $fallback-serif;
@@ -0,0 +1,9 @@
// Font Awesome 7 Free (solid icons), self-hosted from node_modules instead of the cdnjs import
// in frontend-gulp. The webfont path goes through the '@fa-webfonts' alias in vite.config.js.
@use '@fortawesome/fontawesome-free/scss/variables' with (
$font-path: '@fa-webfonts'
);
@use '@fortawesome/fontawesome-free/scss/fontawesome';
@use '@fortawesome/fontawesome-free/scss/solid';
@use 'toastify-1.12/toastify';
@@ -0,0 +1,88 @@
/*!
* Toastify js 1.12.0
* https://github.com/apvarun/toastify-js
* @license MIT licensed
*
* Copyright (C) 2018 Varun A P
*/
.toastify {
padding: 12px 20px;
color: #ffffff;
display: inline-block;
box-shadow:
0 3px 6px -1px rgba(0, 0, 0, 0.12),
0 10px 36px -4px rgba(77, 96, 232, 0.3);
background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5);
background: linear-gradient(135deg, #73a5ff, #5477f5);
position: fixed;
opacity: 0;
transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);
border-radius: 2px;
cursor: pointer;
text-decoration: none;
max-width: calc(50% - 20px);
z-index: 2147483647;
}
.toastify.on {
opacity: 1;
}
.toast-close {
background: transparent;
border: 0;
color: white;
cursor: pointer;
font-family: inherit;
font-size: 1em;
opacity: 0.4;
padding: 0 5px;
}
.toastify-right {
right: 15px;
}
.toastify-left {
left: 15px;
}
.toastify-top {
top: -150px;
}
.toastify-bottom {
bottom: -150px;
}
.toastify-rounded {
border-radius: 25px;
}
.toastify-avatar {
width: 1.5em;
height: 1.5em;
margin: -7px 5px;
border-radius: 2px;
}
.toastify-center {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: fit-content;
max-width: -moz-fit-content;
}
@media only screen and (max-width: 360px) {
.toastify-right,
.toastify-left {
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
max-width: fit-content;
}
}
@@ -0,0 +1,38 @@
@use '../03_base/colors' as *;
.chuck-norris-joke {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
min-height: calc(100vh - 3.5rem); // full screen below the navbar
background: #555 url('../../assets/img/concrete-bg.jpg') center no-repeat fixed;
background-size: cover;
&::after {
content: '';
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.8) 100%);
}
> * {
position: relative;
z-index: 2;
}
.joke-text {
color: $color-white;
text-align: center;
font-size: 3rem;
max-width: 80%;
padding: 2rem;
text-shadow: 2px 2px 5px rgba(black, 0.5);
}
}
@@ -0,0 +1,15 @@
.header-main {
width: 100%;
height: 400px;
// https://cssreference.io/property/background/
background: black url('../../assets/img/header-bg.webp') center no-repeat border-box;
background-size: cover;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border: 20px solid rgba(white, 0.25);
}
@@ -0,0 +1,5 @@
@forward 'chuck-norris-joke';
@forward 'header-main';
@forward 'logo-main';
@forward 'menu-main';
@forward 'product-manager';
@@ -0,0 +1,21 @@
.logo-main {
margin: 0;
img,
svg {
width: 320px;
height: auto;
max-width: 320px;
}
figcaption {
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
}
@@ -0,0 +1,20 @@
@use '../03_base/colors' as *;
@use '../03_base/typography' as *;
// Main navigation = Bootstrap 6 navbar (see includes/menu-main.html).
// Only the look from frontend-gulp is added here: black bar, uppercase links.
.menu-main {
background-color: $color-black;
font-family: $font-menu;
.nav-link {
text-transform: uppercase;
}
}
// Mobile drawer: Bootstrap 6 alpha keeps it white although the navbar is dark
// (data-bs-theme="dark"), which leaves the light link text invisible. Give it the bar's colour.
.menu-main .drawer {
background-color: $color-black;
color: $color-white;
}
@@ -0,0 +1,47 @@
.product-manager {
.nav-products-actions {
ul {
padding: 0;
margin: 0;
list-style: none;
display: flex;
justify-content: flex-end;
li {
button {
margin: 0.5rem 0 0.5rem 0.5rem;
}
}
}
}
table {
.th-actions {
width: 200px;
}
.th-dragger {
width: 50px;
}
td {
vertical-align: middle;
button {
margin: 0 2px;
}
}
tbody:empty::after {
content: 'loading products...';
width: max-content;
display: inline-block;
color: #999;
padding: 1rem;
}
tfoot .row > * {
margin-bottom: 0.5rem;
@media (min-width: 992px) {
margin-bottom: 0;
}
}
}
}
@@ -0,0 +1,13 @@
// Single stylesheet entry, linked in src/html/includes/head.html.
// Same folders as in frontend-gulp; @use instead of the deprecated @import.
// 01 Frameworks
@use '01_frameworks' as *;
// 02 Fonts
@use '02_fonts' as *;
// 03 Base
@use '03_base' as *;
// 04 Plugins
@use '04_plugins' as *;
// 05 Modules
@use '05_modules' as *;
@@ -0,0 +1,77 @@
import { readdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import nunjucks from 'nunjucks';
import { defineConfig } from 'vite';
const root = path.dirname(fileURLToPath(import.meta.url));
const srcDir = path.join(root, 'src');
const pagesDir = path.join(srcDir, 'html/pages');
const includesDir = path.join(srcDir, 'html/includes');
// Every .html in src/html/pages (including subfolders) becomes a page in dist/.
const collectPages = (dir, pages = {}) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) collectPages(full, pages);
else if (entry.name.endsWith('.html')) {
pages[path.relative(pagesDir, full).replace(/\.html$/, '')] = full;
}
}
return pages;
};
// The gulp-file-include replacement: renders each page through Nunjucks before Vite reads it.
// {% include "head.html" %} looks in src/html/includes/.
const nunjucksPlugin = () => {
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(includesDir, { noCache: true }));
return {
name: 'nunjucks',
transformIndexHtml: {
order: 'pre', // before Vite resolves <link>/<script>/<img> — so includes get processed too
handler: (html) => env.renderString(html, {}),
},
configureServer(server) {
// Includes live outside the Vite root: watch them and reload the page on change.
server.watcher.add(includesDir);
server.watcher.on('change', (file) => {
if (file.startsWith(includesDir)) server.hot.send({ type: 'full-reload' });
});
},
};
};
export default defineConfig({
root: pagesDir,
publicDir: path.join(root, 'public'),
plugins: [nunjucksPlugin()],
resolve: {
alias: {
// head.html links /src/scss/main.scss and /src/scripts/main.js. Both live above the Vite
// root (src/html/pages), which a plain URL cannot reach — this alias makes it work in dev
// and build alike.
'/src': srcDir,
// Font Awesome's webfonts folder, used by src/scss/04_plugins/_index.scss
'@fa-webfonts': path.join(root, 'node_modules/@fortawesome/fontawesome-free/webfonts'),
},
},
server: {
port: 3000,
fs: { allow: [root] },
// The product manager talks to the backend (01_website/backend, `node server.js`).
// Proxying /api keeps the browser on one origin, so CORS never comes into play.
proxy: { '/api': 'http://127.0.0.1:8000' },
},
preview: {
port: 3000,
proxy: { '/api': 'http://127.0.0.1:8000' },
},
build: {
outDir: path.join(root, 'dist'),
emptyOutDir: true,
rollupOptions: { input: collectPages(pagesDir) },
},
});
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>02_vite-app</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
@@ -0,0 +1,828 @@
{
"name": "02_vite-app",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "02_vite-app",
"version": "0.0.0",
"devDependencies": {
"vite": "^8.3.0"
}
},
"node_modules/@oxc-project/types": {
"version": "0.154.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz",
"integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/oxc-project"
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz",
"integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz",
"integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz",
"integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz",
"integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz",
"integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz",
"integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz",
"integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz",
"integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz",
"integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz",
"integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz",
"integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz",
"integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz",
"integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz",
"integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz",
"integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/lightningcss": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.33.0",
"lightningcss-darwin-arm64": "1.33.0",
"lightningcss-darwin-x64": "1.33.0",
"lightningcss-freebsd-x64": "1.33.0",
"lightningcss-linux-arm-gnueabihf": "1.33.0",
"lightningcss-linux-arm64-gnu": "1.33.0",
"lightningcss-linux-arm64-musl": "1.33.0",
"lightningcss-linux-x64-gnu": "1.33.0",
"lightningcss-linux-x64-musl": "1.33.0",
"lightningcss-win32-arm64-msvc": "1.33.0",
"lightningcss-win32-x64-msvc": "1.33.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/nanoid": {
"version": "3.3.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
"integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz",
"integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.20",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rolldown": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz",
"integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.154.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.14",
"@rolldown/binding-android-arm64": "1.2.14",
"@rolldown/binding-darwin-arm64": "1.2.14",
"@rolldown/binding-darwin-x64": "1.2.14",
"@rolldown/binding-freebsd-x64": "1.2.14",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.14",
"@rolldown/binding-linux-arm64-gnu": "1.2.14",
"@rolldown/binding-linux-arm64-musl": "1.2.14",
"@rolldown/binding-linux-ppc64-gnu": "1.2.14",
"@rolldown/binding-linux-s390x-gnu": "1.2.14",
"@rolldown/binding-linux-x64-gnu": "1.2.14",
"@rolldown/binding-linux-x64-musl": "1.2.14",
"@rolldown/binding-openharmony-arm64": "1.2.14",
"@rolldown/binding-win32-arm64-msvc": "1.2.14",
"@rolldown/binding-win32-x64-msvc": "1.2.14"
}
},
"node_modules/source-map-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/vite": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz",
"integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.7",
"postcss": "^8.5.29",
"rolldown": "~1.2.12",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.7.1",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"@vitejs/devtools": {
"optional": true
},
"esbuild": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
}
}
}
@@ -0,0 +1,14 @@
{
"name": "02_vite-app",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^8.3.0"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="32" height="32" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"/><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"/></svg>

After

Width:  |  Height:  |  Size: 863 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

@@ -0,0 +1,9 @@
export function setupCounter(element) {
let counter = 0
const setCounter = (count) => {
counter = count
element.innerHTML = `Count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}
@@ -0,0 +1,60 @@
import './style.css';
import heroImg from './assets/hero.png';
import javascriptLogo from './assets/javascript.svg';
import viteLogo from './assets/vite.svg';
import { setupCounter } from './counter.js';
document.querySelector('#app').innerHTML = `
<section id="center">
<div class="hero">
<img src="${heroImg}" class="base" width="170" height="179">
<img src="${javascriptLogo}" class="framework" alt="JavaScript logo"/>
<img src="${viteLogo}" class="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>Edit <code>src/main.js</code> and save to test <code>HMR</code></p>
</div>
<button id="counter" type="button" class="counter"></button>
</section>
<div class="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg class="icon" role="presentation" aria-hidden="true"><use href="/icons.svg#documentation-icon"></use></svg>
<h2>Documentation!!!</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img class="logo" src="${viteLogo}" alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
<img class="button-icon" src="${javascriptLogo}" alt="">
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg class="icon" role="presentation" aria-hidden="true"><use href="/icons.svg#social-icon"></use></svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li><a href="https://github.com/vitejs/vite" target="_blank"><svg class="button-icon" role="presentation" aria-hidden="true"><use href="/icons.svg#github-icon"></use></svg>GitHub</a></li>
<li><a href="https://chat.vite.dev/" target="_blank"><svg class="button-icon" role="presentation" aria-hidden="true"><use href="/icons.svg#discord-icon"></use></svg>Discord</a></li>
<li><a href="https://x.com/vite_js" target="_blank"><svg class="button-icon" role="presentation" aria-hidden="true"><use href="/icons.svg#x-icon"></use></svg>X.com</a></li>
<li><a href="https://bsky.app/profile/vite.dev" target="_blank"><svg class="button-icon" role="presentation" aria-hidden="true"><use href="/icons.svg#bluesky-icon"></use></svg>Bluesky</a></li>
</ul>
</div>
</section>
<div class="ticks"></div>
<section id="spacer"></section>
`;
setupCounter(document.querySelector('#counter'));
@@ -0,0 +1,296 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#app {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}