@@ -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')&¤tRow.previousElementSibling){currentRow.previousElementSibling.before(currentRow);disableNonFunctionalButtons();await updateRowPositions();}else if(btnEl.matches('.button-product-move-down')&¤tRow.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&¤t!==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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
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"
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 19 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 |
|
After Width: | Height: | Size: 849 KiB |
|
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 €</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">€</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">€</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 & 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"
|
||||
}
|
||||
}
|
||||
|
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 |
|
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 |
|
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);
|
||||
}
|
||||
}
|
||||