This commit is contained in:
Philippe Torrel
2026-10-09 11:30:10 +02:00
parent 80c664c78a
commit 7fd7301202
17 changed files with 10798 additions and 616 deletions
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,54 @@
(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;}
const ProductManager=(el=null,options={})=>{// === DOM & VARS =======
const module=el||document.querySelector('.product-manager')||console.warn('Product Manager Element not found');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')};// console.log(DOM);
// === INIT =============
const init=()=>{// Funktionsaufrufe zu beginn der Anwendung
initProducts();DOM.btnAdd.disabled=true;// Event-Lauscher zu beginn der Anwendung
DOM.btnAdd.addEventListener('click',onClickAdd);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 onClickAdd=e=>{const product={name:DOM.inputName.value,//
price:Number(DOM.inputPrice.valueAsNumber)};addProduct(product);DOM.inputName.value=DOM.inputPrice.value='';DOM.btnAdd.disabled=true;disableNonFunctionalButtons();};const onClickRemove=e=>{const btn=e.currentTarget;const currentRow=parents(btn,'tr')[0];// btn.closest('tr'); // btn.parentNode.parentNode;
currentRow.remove();// currentRow.parentNode.removeChild(currentRow); // IE11 support
disableNonFunctionalButtons();};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 ========
const fetchProducts=async()=>{try{const response=await fetch('/data/products.json');// 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){console.error(error);throw error;}};// === FUNCTIONS ========
const initProducts=()=>{fetchProducts().then(products=>{products.forEach(addProduct);disableNonFunctionalButtons();});};const addProduct=product=>{const{name,price}=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 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);trEl.addEventListener('dragstart',onDragStart);trEl.addEventListener('dragover',onDragOver);trEl.addEventListener('drop',onDrop);tdNameEl.textContent=name;tdPriceEl.textContent=Number(price).toFixed(2);btnRemoveEl.addEventListener('click',onClickRemove);btnMoveUpEl.addEventListener('click',onClickMoveUp);btnMoveDownEl.addEventListener('click',onClickMoveDown);DOM.tbody.appendChild(trEl);};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;});};// 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};};
(()=>{// === DOM & VARS =======
const init=()=>{console.log('init!');};// === EVENTHANDLER =====
const init=()=>{console.log('init!');ProductManager();};// === EVENTHANDLER =====
// === XHR/FETCH ========
// === FUNCTIONS ========
init();})();
File diff suppressed because one or more lines are too long
@@ -38,7 +38,7 @@
<!-- ul.sublist>li*3>a[href="#"]{Project $} -->
<ul class="sublist">
<li><a href="./projects/01_jokes.html">01 Chuck Norris Jokes</a></li>
<li><a href="#">Project 2</a></li>
<li><a href="/projects/02_product-manager.html">02 Product Manager</a></li>
<li><a href="#">Project 3</a></li>
</ul>
</li>
@@ -7,7 +7,7 @@
<!-- ul.sublist>li*3>a[href="#"]{Project $} -->
<ul class="sublist">
<li><a href="@@webRoot/projects/01_jokes.html">01 Chuck Norris Jokes</a></li>
<li><a href="#">Project 2</a></li>
<li><a href="/projects/02_product-manager.html">02 Product Manager</a></li>
<li><a href="#">Project 3</a></li>
</ul>
</li>
@@ -0,0 +1,56 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chuck Norris Joke (API)</title>
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
<link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="JS Course" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<link rel="stylesheet" href="/assets/css/main.css" />
<script src="/assets/js/build.js" defer></script>
</head>
<body>
<!-- header.header-main>.content-box>figure.logo-main>img+figc -->
<!-- <header class="header-main">
<div class="content-box">
<figure class="logo-main">
<img src="/assets/img/logo-js.min.svg" alt="Logo - JS Course" />
<figcaption>JS course</figcaption>
</figure>
</div>
</header> -->
<!-- nav.menu-main>ul.list>li*4>a[href="#"]{Link $} -->
<nav class="menu-main">
<ul class="list">
<li><a href="/">Home</a></li>
<li><a href="/about.html">About</a></li>
<li>
<a href="#dropdown">Projects</a>
<!-- ul.sublist>li*3>a[href="#"]{Project $} -->
<ul class="sublist">
<li><a href="/projects/01_jokes.html">01 Chuck Norris Jokes</a></li>
<li><a href="#">Project 2</a></li>
<li><a href="#">Project 3</a></li>
</ul>
</li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</nav>
<main>
<section class="section-joke">
<!-- .chuck-norris-joke>blockquote.joke-text+button.btn.btn-dark.button-get-joke{Get Joke} -->
<div class="chuck-norris-joke">
<blockquote class="joke-text"></blockquote>
<button class="btn btn-dark button-get-joke">Get Joke</button>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,89 @@
<!doctype html>
<html lang="de">
@@include('head.html')
<body>
@@include('menu-main.html')
<main>
<!-- ▼ Product Manager ▼ -->
<div class="product-manager">
<div class="container py-5">
<h1>Productmanager</h1>
<!-- table.table.table-striped.table-products>thead.table-dark>tr>th*2 -->
<table class="table table-striped table-products">
<thead class="table-dark">
<tr>
<th class="th-dragger">#</th>
<th class="th-name">Name</th>
<th class="th-price">Price in &euro;</th>
<th class="th-actions">Actions</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="4">
<div class="row">
<div class="col-12 col-md-6 col-lg-6">
<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 col-md-6 col-lg-3">
<div class="input-group">
<span class="input-group-text">€</span>
<input
type="number"
name="prodcut-price"
class="form-control input-product-price"
placeholder="00.00"
aria-label="Product price"
min="0"
step="0.01" />
</div>
</div>
<div class="col-12 col-md-6 col-lg">
<button class="btn btn-dark button-product-add">
<i class="fas fa-plus"></i>
Add Product
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
<template class="template-row">
<tr>
<td class="td-dragger">
<button class="btn btn-default button-product-drag">
<i class="fas fa-grip-vertical"></i>
<span class="visually-hidden">Drag Product</span>
</button>
</td>
<td class="td-name">{PRODUCT_NAME}</td>
<td class="td-price">{PRODUCT_PRICE}</td>
<td class="td-actions">
<button class="btn btn-danger button-product-remove">
<i class="fas fa-trash-can"></i>
<span class="visually-hidden">Remove Product</span>
</button>
<button class="btn btn-secondary button-product-move-up">
<i class="fas fa-chevron-up"></i>
<span class="visually-hidden">Move up</span>
</button>
<button class="btn btn-secondary button-product-move-down">
<i class="fas fa-chevron-down"></i>
<span class="visually-hidden">Move down</span>
</button>
</td>
</tr>
</template>
</div>
<!-- ▲ /Product Manager ▲ -->
</main>
</body>
</html>
@@ -1,4 +1,4 @@
'use strict';
import ProductManager from './modules/ProductManager'; // Dateiendung nicht notwendig ?
(() => {
// === DOM & VARS =======
@@ -7,6 +7,7 @@
// === INIT =============
const init = () => {
console.log('init!');
ProductManager();
};
// === EVENTHANDLER =====
@@ -0,0 +1,27 @@
const CreateBtn = (options = {}) => {
const {
classNames = ['btn-primary'],
iconName = 'circle',
label,
clickHandler = (e) => {
console.log('no click action');
},
} = options;
const btn = document.createElement('button');
const icon = document.createElement('i');
const span = document.createElement('span');
btn.classList.add('btn', ...classNames);
icon.classList.add('fas', `fa-${iconName}`);
span.classList.add('visually-hidden');
span.textContent = label;
btn.addEventListener('click', clickHandler);
btn.appendChild(icon);
btn.appendChild(span);
return btn;
};
export default CreateBtn;
@@ -0,0 +1,204 @@
import { parents } from '../utils/helpers.js';
// Factory Function
const ProductManager = (el = null, options = {}) => {
// === DOM & VARS =======
const module = el || document.querySelector('.product-manager') || console.warn('Product Manager Element not found');
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'),
};
// console.log(DOM);
// === INIT =============
const init = () => {
// Funktionsaufrufe zu beginn der Anwendung
initProducts();
DOM.btnAdd.disabled = true;
// Event-Lauscher zu beginn der Anwendung
DOM.btnAdd.addEventListener('click', onClickAdd);
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 onClickAdd = (e) => {
const product = {
name: DOM.inputName.value, //
price: Number(DOM.inputPrice.valueAsNumber),
};
addProduct(product);
DOM.inputName.value = DOM.inputPrice.value = '';
DOM.btnAdd.disabled = true;
disableNonFunctionalButtons();
};
const onClickRemove = (e) => {
const btn = e.currentTarget;
const currentRow = parents(btn, 'tr')[0]; // btn.closest('tr'); // btn.parentNode.parentNode;
currentRow.remove();
// currentRow.parentNode.removeChild(currentRow); // IE11 support
disableNonFunctionalButtons();
};
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 ========
const fetchProducts = async () => {
try {
const response = await fetch('/data/products.json'); // 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) {
console.error(error);
throw error;
}
};
// === FUNCTIONS ========
const initProducts = () => {
fetchProducts().then((products) => {
products.forEach(addProduct);
disableNonFunctionalButtons();
});
};
const addProduct = (product) => {
const { name, price } = 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 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);
trEl.addEventListener('dragstart', onDragStart);
trEl.addEventListener('dragover', onDragOver);
trEl.addEventListener('drop', onDrop);
tdNameEl.textContent = name;
tdPriceEl.textContent = Number(price).toFixed(2);
btnRemoveEl.addEventListener('click', onClickRemove);
btnMoveUpEl.addEventListener('click', onClickMoveUp);
btnMoveDownEl.addEventListener('click', onClickMoveDown);
DOM.tbody.appendChild(trEl);
};
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;
});
};
// 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,
};
};
export default ProductManager;
@@ -0,0 +1,32 @@
// Helper
export function parents(el, selector) {
const parents = [];
while ((el = el.parentNode) && el !== document) {
if (!selector || el.matches(selector)) parents.push(el);
}
return parents;
}
export function addClass(el, className) {
if (Array.isArray(className)) {
el.classList.add(...className);
} else {
el.classList.add(className);
}
return el;
}
export function removeClass(el, className) {
if (Array.isArray(className)) {
el.classList.remove(...className);
} else {
el.classList.remove(className);
}
return el;
}
export default {
parents,
addClass,
removeClass,
};
@@ -2,3 +2,4 @@
@import 'header-main';
@import 'logo-main';
@import 'menu-main';
@import 'product-manager';
@@ -0,0 +1,31 @@
/* Mobile first */
.product-manager table tfoot .row > * {
margin-bottom: 0.5rem;
}
@media (min-width: 992px) {
.product-manager table tfoot .row > * {
margin-bottom: 0;
}
}
.product-manager table td {
vertical-align: middle;
}
.product-manager table tbody:empty::after {
content: 'loading products...';
color: #999;
padding: 1rem;
}
.product-manager table td button {
margin: 0 3px;
}
.product-manager table .th-actions {
width: 185px;
}
.product-manager table .th-dragger {
width: 50px;
}
@@ -38,7 +38,7 @@
<!-- ul.sublist>li*3>a[href="#"]{Project $} -->
<ul class="sublist">
<li><a href="./projects/01_jokes.html">01 Chuck Norris Jokes</a></li>
<li><a href="#">Project 2</a></li>
<li><a href="/projects/02_product-manager.html">02 Product Manager</a></li>
<li><a href="#">Project 3</a></li>
</ul>
</li>
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
{
"name": "website-gulp-js",
"name": "website-frontend",
"version": "0.0.1",
"description": "First Website with sass",
"description": "webseite mit sas, rollup, gulp, module",
"keywords": [],
"license": "ISC",
"author": "",
@@ -13,9 +13,9 @@
"dev": "npx gulp dev"
},
"devDependencies": {
"@babel/core": "^8.0.6",
"@babel/core": "^8.0.7",
"@babel/plugin-transform-runtime": "^8.0.6",
"@babel/preset-env": "^8.0.6",
"@babel/preset-env": "^8.0.7",
"@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
@@ -28,7 +28,7 @@
"gulp-sass": "^6.0.1",
"gulp-sourcemaps": "^3.0.0",
"npm-run-all": "^4.1.5",
"rollup": "^4.64.0",
"rollup": "^4.64.3",
"sass": "^1.105.1",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0"
@@ -0,0 +1,121 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Webseite mit HTML & CSS (SCSS)</title>
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" /> -->
<link rel="icon" type="image/png" href="../favicon/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="../favicon/favicon.svg" />
<link rel="shortcut icon" href="../favicon/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="../favicon/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="JS Course" />
<link rel="manifest" href="../favicon/site.webmanifest" />
<link rel="stylesheet" href="../assets/css/main.css" />
<script src="../assets/js/build.js" defer></script>
</head>
<body>
<nav class="menu-main">
<ul class="list">
<li><a href="../">Home</a></li>
<li><a href="../about.html">About</a></li>
<li>
<a href="#dropdown">Projects</a>
<!-- ul.sublist>li*3>a[href="#"]{Project $} -->
<ul class="sublist">
<li><a href="../projects/01_jokes.html">01 Chuck Norris Jokes</a></li>
<li><a href="/projects/02_product-manager.html">02 Product Manager</a></li>
<li><a href="#">Project 3</a></li>
</ul>
</li>
<li><a href="../contact.html">Contact</a></li>
</ul>
</nav>
<main>
<!-- ▼ Product Manager ▼ -->
<div class="product-manager">
<div class="container py-5">
<h1>Productmanager</h1>
<!-- table.table.table-striped.table-products>thead.table-dark>tr>th*2 -->
<table class="table table-striped table-products">
<thead class="table-dark">
<tr>
<th class="th-dragger">#</th>
<th class="th-name">Name</th>
<th class="th-price">Price in &euro;</th>
<th class="th-actions">Actions</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="4">
<div class="row">
<div class="col-12 col-md-6 col-lg-6">
<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 col-md-6 col-lg-3">
<div class="input-group">
<span class="input-group-text">€</span>
<input
type="number"
name="prodcut-price"
class="form-control input-product-price"
placeholder="00.00"
aria-label="Product price"
min="0"
step="0.01" />
</div>
</div>
<div class="col-12 col-md-6 col-lg">
<button class="btn btn-dark button-product-add">
<i class="fas fa-plus"></i>
Add Product
</button>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
<template class="template-row">
<tr>
<td class="td-dragger">
<button class="btn btn-default button-product-drag">
<i class="fas fa-grip-vertical"></i>
<span class="visually-hidden">Drag Product</span>
</button>
</td>
<td class="td-name">{PRODUCT_NAME}</td>
<td class="td-price">{PRODUCT_PRICE}</td>
<td class="td-actions">
<button class="btn btn-danger button-product-remove">
<i class="fas fa-trash-can"></i>
<span class="visually-hidden">Remove Product</span>
</button>
<button class="btn btn-secondary button-product-move-up">
<i class="fas fa-chevron-up"></i>
<span class="visually-hidden">Move up</span>
</button>
<button class="btn btn-secondary button-product-move-down">
<i class="fas fa-chevron-down"></i>
<span class="visually-hidden">Move down</span>
</button>
</td>
</tr>
</template>
</div>
<!-- ▲ /Product Manager ▲ -->
</main>
</body>
</html>