This commit is contained in:
Philippe Torrel
2026-07-24 13:31:30 +02:00
parent c118892017
commit e5c2f47063
19 changed files with 1380 additions and 659 deletions

View File

@@ -0,0 +1 @@
**Exkurs: Finalisierung Webseite mit Express + ProduktManager nach CRUD + Verwendung von Modulen**

View File

@@ -38,9 +38,29 @@ Content-Type: application/json
# DELETE Product # DELETE Product
DELETE http://127.0.0.1:8000/api/products/940c316b-518c-4a18-b1f6-328e010eef4d DELETE http://127.0.0.1:8000/api/products/6cbe4783-cfb5-4ed7-8196-9d6b55217de0
Content-Type: application/json Content-Type: application/json
###
# RESET Products
PATCH http://127.0.0.1:8000/api/products/reset
Content-Type: application/json
{
"reset": true
}
###
# SAVE Products
PATCH http://127.0.0.1:8000/api/products/save
Content-Type: application/json
{
"save": true
}

View File

@@ -0,0 +1,38 @@
[
{
"_id": "6cbe4783-cfb5-4ed7-8196-9d6b55217de0",
"position": 1,
"name": "3Doodler 3D Printing Pen",
"price": 29.99
},
{
"_id": "c38913d4-9524-461f-85d2-525241166e8e",
"position": 2,
"name": "Powerstation 5- E. Maximus Chargus",
"price": 44.95
},
{
"_id": "cdc887bd-fb0e-4485-8516-0a3720c3bb72",
"position": 3,
"name": "8-Bit Legendary Hero Heat-Change Mug",
"price": 6.99
},
{
"_id": "bded2710-8db8-41a5-87af-269195eab449",
"position": 4,
"name": "16-Bit Legendary Hero Heat-Change Mug",
"price": 12.99
},
{
"_id": "3b3352c1-789d-4b4f-8a93-f05f44ece5a5",
"position": 5,
"name": "32-Bit Legendary Hero Heat-Change Mug",
"price": 18.99
},
{
"_id": "5af39d33-cbcd-42c7-a160-50084faa91ba",
"position": 6,
"name": "64-Bit Legendary Hero Heat-Change Mug",
"price": 21.99
}
]

View File

@@ -14,6 +14,15 @@ const ProductModel = () => {
} }
}; };
const reset = () => {
try {
products = JSON.parse(fs.readFileSync('./data/products.bakup.json', 'utf-8'));
} catch (error) {
console.log('Something went wrong: ', error);
products = [];
}
};
const save = () => { const save = () => {
try { try {
fs.writeFileSync('./data/products.json', JSON.stringify(products, null, 2), 'utf-8'); fs.writeFileSync('./data/products.json', JSON.stringify(products, null, 2), 'utf-8');
@@ -53,7 +62,6 @@ const ProductModel = () => {
}; };
const update = (product) => { const update = (product) => {
console.log(product);
const foundIdx = products.findIndex((obj) => { const foundIdx = products.findIndex((obj) => {
return obj._id === product._id; return obj._id === product._id;
}); });
@@ -98,6 +106,7 @@ const ProductModel = () => {
get, get,
update, update,
delete: remove, delete: remove,
reset,
}; };
}; };

View File

@@ -4,6 +4,7 @@
"description": "", "description": "",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "npx nodemon server.js",
"server": "npx nodemon server.js" "server": "npx nodemon server.js"
}, },
"keywords": [], "keywords": [],

View File

@@ -13,7 +13,7 @@ const app = express();
const products = ProductModel(); const products = ProductModel();
const corsOptions = { const corsOptions = {
origin: 'http://localhost:3000', origin: ['http://localhost:3000', 'http://127.0.0.1:3000'],
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204 optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
}; };
@@ -22,7 +22,12 @@ products.load();
// Middleware // Middleware
app.use(cors(corsOptions)); // Fängt alle HTTP-Requests vor den Routenabfragen ab. app.use(cors(corsOptions)); // Fängt alle HTTP-Requests vor den Routenabfragen ab.
app.use(express.json()); // HTTP-Request Body wird als JSON Objekt erwartet und geparsed wird. app.use(express.json()); // HTTP-Request Body wird als JSON Objekt erwartet und geparsed wird. // app.use(bodyParser.json()) - Modul body-parser war in express 4 notwendig
app.use((req, res, next) => {
console.log(color.yellow('HTTP-Method: '), color.magenta(req.method));
next();
});
// Routes // Routes
app.get('/', (req, res) => { app.get('/', (req, res) => {
@@ -101,6 +106,34 @@ app.put('/api/products', (req, res) => {
return res.send({ msg: 'Product updated', status: 200, success: true, data: product }); return res.send({ msg: 'Product updated', status: 200, success: true, data: product });
}); });
// HTTP - Methode - PATCH
// reseten von Produkten
app.patch('/api/products/reset', (req, res) => {
const reset = req.body.reset;
if (!reset) {
return res.status(400).send({ msg: 'Could not reset products', status: 400, success: false });
}
products.reset();
return res.send({ msg: 'Products reseted', status: 200, success: true, data: reset });
});
// HTTP - Methode - PATCH
// speichern von Produkten
app.patch('/api/products/save', (req, res) => {
const save = req.body.save;
if (!save) {
return res.status(400).send({ msg: 'Could not save products', status: 400, success: false });
}
products.save();
return res.send({ msg: 'Products saved', status: 200, success: true, data: save });
});
// HTTP - Methode - DELETE // HTTP - Methode - DELETE
// CRU(D) - delete // CRU(D) - delete
// BREA(D) - delete // BREA(D) - delete

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,344 @@
(() => { (() => {
var __create = Object.create;
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
try {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
} catch (e) {
throw mod = 0, e;
}
};
var __export = (target, all) => { var __export = (target, all) => {
for (var name in all) for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true }); __defProp(target, name, { get: all[name], enumerable: true });
}; };
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// node_modules/toastify-js/src/toastify.js
var require_toastify = __commonJS({
"node_modules/toastify-js/src/toastify.js"(exports, module) {
(function(root, factory) {
if (typeof module === "object" && module.exports) {
module.exports = factory();
} else {
root.Toastify = factory();
}
})(exports, function(global) {
var Toastify2 = function(options) {
return new Toastify2.lib.init(options);
}, version = "1.12.0";
Toastify2.defaults = {
oldestFirst: true,
text: "Toastify is awesome!",
node: void 0,
duration: 3e3,
selector: void 0,
callback: function() {
},
destination: void 0,
newWindow: false,
close: false,
gravity: "toastify-top",
positionLeft: false,
position: "",
backgroundColor: "",
avatar: "",
className: "",
stopOnFocus: true,
onClick: function() {
},
offset: { x: 0, y: 0 },
escapeMarkup: true,
ariaLive: "polite",
style: { background: "" }
};
Toastify2.lib = Toastify2.prototype = {
toastify: version,
constructor: Toastify2,
// Initializing the object with required parameters
init: function(options) {
if (!options) {
options = {};
}
this.options = {};
this.toastElement = null;
this.options.text = options.text || Toastify2.defaults.text;
this.options.node = options.node || Toastify2.defaults.node;
this.options.duration = options.duration === 0 ? 0 : options.duration || Toastify2.defaults.duration;
this.options.selector = options.selector || Toastify2.defaults.selector;
this.options.callback = options.callback || Toastify2.defaults.callback;
this.options.destination = options.destination || Toastify2.defaults.destination;
this.options.newWindow = options.newWindow || Toastify2.defaults.newWindow;
this.options.close = options.close || Toastify2.defaults.close;
this.options.gravity = options.gravity === "bottom" ? "toastify-bottom" : Toastify2.defaults.gravity;
this.options.positionLeft = options.positionLeft || Toastify2.defaults.positionLeft;
this.options.position = options.position || Toastify2.defaults.position;
this.options.backgroundColor = options.backgroundColor || Toastify2.defaults.backgroundColor;
this.options.avatar = options.avatar || Toastify2.defaults.avatar;
this.options.className = options.className || Toastify2.defaults.className;
this.options.stopOnFocus = options.stopOnFocus === void 0 ? Toastify2.defaults.stopOnFocus : options.stopOnFocus;
this.options.onClick = options.onClick || Toastify2.defaults.onClick;
this.options.offset = options.offset || Toastify2.defaults.offset;
this.options.escapeMarkup = options.escapeMarkup !== void 0 ? options.escapeMarkup : Toastify2.defaults.escapeMarkup;
this.options.ariaLive = options.ariaLive || Toastify2.defaults.ariaLive;
this.options.style = options.style || Toastify2.defaults.style;
if (options.backgroundColor) {
this.options.style.background = options.backgroundColor;
}
return this;
},
// Building the DOM element
buildToast: function() {
if (!this.options) {
throw "Toastify is not initialized";
}
var divElement = document.createElement("div");
divElement.className = "toastify on " + this.options.className;
if (!!this.options.position) {
divElement.className += " toastify-" + this.options.position;
} else {
if (this.options.positionLeft === true) {
divElement.className += " toastify-left";
console.warn("Property `positionLeft` will be depreciated in further versions. Please use `position` instead.");
} else {
divElement.className += " toastify-right";
}
}
divElement.className += " " + this.options.gravity;
if (this.options.backgroundColor) {
console.warn('DEPRECATION NOTICE: "backgroundColor" is being deprecated. Please use the "style.background" property.');
}
for (var property in this.options.style) {
divElement.style[property] = this.options.style[property];
}
if (this.options.ariaLive) {
divElement.setAttribute("aria-live", this.options.ariaLive);
}
if (this.options.node && this.options.node.nodeType === Node.ELEMENT_NODE) {
divElement.appendChild(this.options.node);
} else {
if (this.options.escapeMarkup) {
divElement.innerText = this.options.text;
} else {
divElement.innerHTML = this.options.text;
}
if (this.options.avatar !== "") {
var avatarElement = document.createElement("img");
avatarElement.src = this.options.avatar;
avatarElement.className = "toastify-avatar";
if (this.options.position == "left" || this.options.positionLeft === true) {
divElement.appendChild(avatarElement);
} else {
divElement.insertAdjacentElement("afterbegin", avatarElement);
}
}
}
if (this.options.close === true) {
var closeElement = document.createElement("button");
closeElement.type = "button";
closeElement.setAttribute("aria-label", "Close");
closeElement.className = "toast-close";
closeElement.innerHTML = "✖";
closeElement.addEventListener(
"click",
function(event) {
event.stopPropagation();
this.removeElement(this.toastElement);
window.clearTimeout(this.toastElement.timeOutValue);
}.bind(this)
);
var width = window.innerWidth > 0 ? window.innerWidth : screen.width;
if ((this.options.position == "left" || this.options.positionLeft === true) && width > 360) {
divElement.insertAdjacentElement("afterbegin", closeElement);
} else {
divElement.appendChild(closeElement);
}
}
if (this.options.stopOnFocus && this.options.duration > 0) {
var self = this;
divElement.addEventListener(
"mouseover",
function(event) {
window.clearTimeout(divElement.timeOutValue);
}
);
divElement.addEventListener(
"mouseleave",
function() {
divElement.timeOutValue = window.setTimeout(
function() {
self.removeElement(divElement);
},
self.options.duration
);
}
);
}
if (typeof this.options.destination !== "undefined") {
divElement.addEventListener(
"click",
function(event) {
event.stopPropagation();
if (this.options.newWindow === true) {
window.open(this.options.destination, "_blank");
} else {
window.location = this.options.destination;
}
}.bind(this)
);
}
if (typeof this.options.onClick === "function" && typeof this.options.destination === "undefined") {
divElement.addEventListener(
"click",
function(event) {
event.stopPropagation();
this.options.onClick();
}.bind(this)
);
}
if (typeof this.options.offset === "object") {
var x = getAxisOffsetAValue("x", this.options);
var y = getAxisOffsetAValue("y", this.options);
var xOffset = this.options.position == "left" ? x : "-" + x;
var yOffset = this.options.gravity == "toastify-top" ? y : "-" + y;
divElement.style.transform = "translate(" + xOffset + "," + yOffset + ")";
}
return divElement;
},
// Displaying the toast
showToast: function() {
this.toastElement = this.buildToast();
var rootElement;
if (typeof this.options.selector === "string") {
rootElement = document.getElementById(this.options.selector);
} else if (this.options.selector instanceof HTMLElement || typeof ShadowRoot !== "undefined" && this.options.selector instanceof ShadowRoot) {
rootElement = this.options.selector;
} else {
rootElement = document.body;
}
if (!rootElement) {
throw "Root element is not defined";
}
var elementToInsert = Toastify2.defaults.oldestFirst ? rootElement.firstChild : rootElement.lastChild;
rootElement.insertBefore(this.toastElement, elementToInsert);
Toastify2.reposition();
if (this.options.duration > 0) {
this.toastElement.timeOutValue = window.setTimeout(
function() {
this.removeElement(this.toastElement);
}.bind(this),
this.options.duration
);
}
return this;
},
hideToast: function() {
if (this.toastElement.timeOutValue) {
clearTimeout(this.toastElement.timeOutValue);
}
this.removeElement(this.toastElement);
},
// Removing the element from the DOM
removeElement: function(toastElement) {
toastElement.className = toastElement.className.replace(" on", "");
window.setTimeout(
function() {
if (this.options.node && this.options.node.parentNode) {
this.options.node.parentNode.removeChild(this.options.node);
}
if (toastElement.parentNode) {
toastElement.parentNode.removeChild(toastElement);
}
this.options.callback.call(toastElement);
Toastify2.reposition();
}.bind(this),
400
);
}
};
Toastify2.reposition = function() {
var topLeftOffsetSize = {
top: 15,
bottom: 15
};
var topRightOffsetSize = {
top: 15,
bottom: 15
};
var offsetSize = {
top: 15,
bottom: 15
};
var allToasts = document.getElementsByClassName("toastify");
var classUsed;
for (var i = 0; i < allToasts.length; i++) {
if (containsClass(allToasts[i], "toastify-top") === true) {
classUsed = "toastify-top";
} else {
classUsed = "toastify-bottom";
}
var height = allToasts[i].offsetHeight;
classUsed = classUsed.substr(9, classUsed.length - 1);
var offset2 = 15;
var width = window.innerWidth > 0 ? window.innerWidth : screen.width;
if (width <= 360) {
allToasts[i].style[classUsed] = offsetSize[classUsed] + "px";
offsetSize[classUsed] += height + offset2;
} else {
if (containsClass(allToasts[i], "toastify-left") === true) {
allToasts[i].style[classUsed] = topLeftOffsetSize[classUsed] + "px";
topLeftOffsetSize[classUsed] += height + offset2;
} else {
allToasts[i].style[classUsed] = topRightOffsetSize[classUsed] + "px";
topRightOffsetSize[classUsed] += height + offset2;
}
}
}
return this;
};
function getAxisOffsetAValue(axis, options) {
if (options.offset[axis]) {
if (isNaN(options.offset[axis])) {
return options.offset[axis];
} else {
return options.offset[axis] + "px";
}
}
return "0px";
}
function containsClass(elem, yourClass) {
if (!elem || typeof yourClass !== "string") {
return false;
} else if (elem.className && elem.className.trim().split(/\s+/gi).indexOf(yourClass) > -1) {
return true;
} else {
return false;
}
}
Toastify2.lib.init.prototype = Toastify2.lib;
return Toastify2;
});
}
});
// node_modules/@popperjs/core/lib/index.js // node_modules/@popperjs/core/lib/index.js
var lib_exports = {}; var lib_exports = {};
@@ -5164,6 +5499,65 @@
enableDismissTrigger(Toast); enableDismissTrigger(Toast);
defineJQueryPlugin(Toast); defineJQueryPlugin(Toast);
// dev/scripts/modules/Toast.js
var import_toastify_js = __toESM(require_toastify(), 1);
var Toast2 = (text = "", variant = "primary") => {
const getColorByName = (variant2 = "") => {
let color;
switch (variant2.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 = () => {
(0, import_toastify_js.default)({
text,
duration: 2e3,
className: variant,
gravity: "top",
// `top` or `bottom`
position: "center",
// `left`, `center` or `right`
stopOnFocus: true,
// Prevents dismissing of toast on hover
style: {
background: getColorByName(variant)
}
}).showToast();
};
return {
show
};
};
var Toast_default = Toast2;
// dev/scripts/modules/ProductManager.js // dev/scripts/modules/ProductManager.js
var ProductManager = (el = null) => { var ProductManager = (el = null) => {
const BASE_URL = "http://127.0.0.1:8000"; const BASE_URL = "http://127.0.0.1:8000";
@@ -5177,25 +5571,41 @@
inputPrice: module.querySelector(".input-product-price"), inputPrice: module.querySelector(".input-product-price"),
btnAdd: module.querySelector(".button-product-add"), btnAdd: module.querySelector(".button-product-add"),
templateRow: module.querySelector(".template-row"), templateRow: module.querySelector(".template-row"),
// Modal // Modal Edit
modalEdit: module.querySelector(".modal-edit"), modalEdit: module.querySelector(".modal-edit"),
formEdit: module.querySelector(".form-product-edit"), formEdit: module.querySelector(".form-product-edit"),
inputEditName: module.querySelector(".input-edit-name"), inputEditName: module.querySelector(".input-edit-name"),
inputEditPrice: module.querySelector(".input-edit-price"), inputEditPrice: module.querySelector(".input-edit-price"),
inputEditId: module.querySelector(".input-edit-id"), inputEditId: module.querySelector(".input-edit-id"),
inputEditPosition: module.querySelector(".input-edit-position") 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")
}; };
console.log(DOM);
const bsModalEdit = new Modal(DOM.modalEdit, { const bsModalEdit = new Modal(DOM.modalEdit, {
backdrop: "static", backdrop: "static",
// true // true
keyboard: true keyboard: true
}); });
console.log(DOM); const bsModalDelete = new Modal(DOM.modalDelete, {
backdrop: "static",
// true
keyboard: true
});
const init = () => { const init = () => {
console.log("init"); console.log("init");
initProducts(); initProducts();
DOM.btnAdd.addEventListener("click", onClickAdd); DOM.btnAdd.addEventListener("click", onClickAdd);
DOM.btnAdd.disabled = true;
DOM.btnConfirmDelete.addEventListener("click", onClickConfirmDelete);
DOM.formEdit.addEventListener("submit", onSubmitEdit); DOM.formEdit.addEventListener("submit", onSubmitEdit);
window.addEventListener("keyup", onKeyUp);
};
const onKeyUp = (e) => {
DOM.btnAdd.disabled = DOM.inputName.value === "" || DOM.inputPrice.value === "";
}; };
const onSubmitEdit = (e) => { const onSubmitEdit = (e) => {
e.preventDefault(); e.preventDefault();
@@ -5205,11 +5615,17 @@
_id: DOM.inputEditId.value, _id: DOM.inputEditId.value,
position: DOM.inputEditPosition.value position: DOM.inputEditPosition.value
}; };
DOM.btnUpdate.querySelector("i").classList.add("fa-spin");
updateProduct(product).then((data) => { updateProduct(product).then((data) => {
console.log(data); console.log(data);
if (data.success) { if (data.success) {
Toast_default(data.msg, "success").show();
DOM.btnUpdate.querySelector("i").classList.remove("fa-spin");
loadProducts(); loadProducts();
resetFields();
bsModalEdit.hide(); bsModalEdit.hide();
} else {
Toast_default(data.msg, "error").show();
} }
}); });
}; };
@@ -5223,7 +5639,9 @@
console.log(data); console.log(data);
if (data.success) { if (data.success) {
console.log("add"); console.log("add");
Toast_default(data.msg, "success").show();
loadProducts(); loadProducts();
resetFields();
} }
}); });
disableNonFunctionalButtons(); disableNonFunctionalButtons();
@@ -5238,9 +5656,25 @@
}; };
const onClickRemove = (e) => { const onClickRemove = (e) => {
const btnEl = e.currentTarget; const btnEl = e.currentTarget;
const currentRowEl = parents(btnEl, "tr")[0]; const currentRow = parents(btnEl, "tr")[0];
currentRowEl.remove(); const id = currentRow.dataset.id;
disableNonFunctionalButtons(); const productName = currentRow.querySelector(".td-name").textContent.trim();
DOM.btnConfirmDelete.dataset.id = id;
DOM.productName.textContent = productName;
};
const onClickConfirmDelete = (e) => {
const btnEl = e.currentTarget;
const id = btnEl.dataset.id;
deleteProduct(id).then((data) => {
if (data.success) {
Toast_default(data.msg, "success").show();
bsModalDelete.hide();
loadProducts();
} else {
Toast_default(data.msg, "error").show();
console.error(data);
}
});
}; };
const onClickMoveUp = (e) => { const onClickMoveUp = (e) => {
const btnEl = e.currentTarget; const btnEl = e.currentTarget;
@@ -5307,6 +5741,7 @@
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
Toast_default(error, "error").show();
console.error(error); console.error(error);
return error; return error;
} }
@@ -5356,11 +5791,30 @@
return error; return error;
} }
}; };
const deleteProduct = async (id) => {
try {
const response = await fetch(`${BASE_URL}/api/products/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
}
});
if (!response.ok) {
throw new Error("Fetch went wrong.");
}
const data = await response.json();
return data;
} catch (error) {
console.error(error);
return error;
}
};
const initProducts = () => { const initProducts = () => {
getProducts().then((products) => { getProducts().then((products) => {
products.forEach((product) => { products.forEach((product) => {
addProduct(product); addProduct(product);
}); });
Toast_default("Init products").show();
disableNonFunctionalButtons(); disableNonFunctionalButtons();
}); });
}; };
@@ -5405,6 +5859,12 @@
DOM.inputEditId.value = id; DOM.inputEditId.value = id;
DOM.inputEditPosition.value = position; DOM.inputEditPosition.value = position;
}; };
const resetFields = () => {
DOM.btnAdd.disabled = true;
DOM.inputPrice.value = "";
DOM.inputName.value = "";
DOM.formEdit.reset();
};
const disableNonFunctionalButtons = () => { const disableNonFunctionalButtons = () => {
Array.from(DOM.tBody.querySelectorAll("tr")).forEach((tr) => { Array.from(DOM.tBody.querySelectorAll("tr")).forEach((tr) => {
const btnMoveUp = tr.querySelector(".button-product-move-up"); const btnMoveUp = tr.querySelector(".button-product-move-up");
@@ -5423,7 +5883,8 @@
init(); init();
return { return {
init, init,
initProducts initProducts,
resetFields
}; };
}; };
var ProductManager_default = ProductManager; var ProductManager_default = ProductManager;
@@ -5456,6 +5917,15 @@
})(); })();
/*! Bundled license information: /*! Bundled license information:
toastify-js/src/toastify.js:
(*!
* Toastify js 1.12.0
* https://github.com/apvarun/toastify-js
* @license MIT licensed
*
* Copyright (C) 2018 Varun A P
*)
bootstrap/dist/js/bootstrap.esm.js: bootstrap/dist/js/bootstrap.esm.js:
(*! (*!
* Bootstrap v5.3.8 (https://getbootstrap.com/) * Bootstrap v5.3.8 (https://getbootstrap.com/)

File diff suppressed because one or more lines are too long

View File

@@ -30,7 +30,6 @@ import ProductManager from './modules/ProductManager'; // Dateiendung kann wegge
break; break;
case '/projects/product-manager.html': case '/projects/product-manager.html':
console.log('PRODUCT_MANAGER'); console.log('PRODUCT_MANAGER');
const pm = ProductManager(); const pm = ProductManager();
break; break;
default: default:

View File

@@ -1,4 +1,8 @@
import { Modal } from 'bootstrap'; // ohne Pfadangabe (Modul auslesen aus node_modules) funktioniert nur mit JS-Bundler (esbuild, rollup & co.) import { Modal } from 'bootstrap'; // ohne Pfadangabe (Modul auslesen aus node_modules) funktioniert nur mit JS-Bundler (esbuild, rollup & co.)
import Toast from './Toast';
// import * as bootstrap from 'bootstrap';
// import 'bootstrap';
// import 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js'; // würde auch ohne Bundler funktionieren // import 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js'; // würde auch ohne Bundler funktionieren
// import '../../../node_modules/bootstrap/dist/js/bootstrap.esm.js'; // würde auch ohne Bundler funktionieren // import '../../../node_modules/bootstrap/dist/js/bootstrap.esm.js'; // würde auch ohne Bundler funktionieren
@@ -22,21 +26,31 @@ const ProductManager = (el = null) => {
btnAdd: module.querySelector('.button-product-add'), btnAdd: module.querySelector('.button-product-add'),
templateRow: module.querySelector('.template-row'), templateRow: module.querySelector('.template-row'),
// Modal // Modal Edit
modalEdit: module.querySelector('.modal-edit'), modalEdit: module.querySelector('.modal-edit'),
formEdit: module.querySelector('.form-product-edit'), formEdit: module.querySelector('.form-product-edit'),
inputEditName: module.querySelector('.input-edit-name'), inputEditName: module.querySelector('.input-edit-name'),
inputEditPrice: module.querySelector('.input-edit-price'), inputEditPrice: module.querySelector('.input-edit-price'),
inputEditId: module.querySelector('.input-edit-id'), inputEditId: module.querySelector('.input-edit-id'),
inputEditPosition: module.querySelector('.input-edit-position'), 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'),
}; };
console.log(DOM);
const bsModalEdit = new Modal(DOM.modalEdit, { const bsModalEdit = new Modal(DOM.modalEdit, {
backdrop: 'static', // true backdrop: 'static', // true
keyboard: true, keyboard: true,
}); });
console.log(DOM); const bsModalDelete = new Modal(DOM.modalDelete, {
backdrop: 'static', // true
keyboard: true,
});
// === INIT ============= // === INIT =============
const init = () => { const init = () => {
@@ -46,13 +60,27 @@ const ProductManager = (el = null) => {
// Event-Lauscher zu beginn der Anwendung // Event-Lauscher zu beginn der Anwendung
DOM.btnAdd.addEventListener('click', onClickAdd); DOM.btnAdd.addEventListener('click', onClickAdd);
DOM.btnAdd.disabled = true;
DOM.btnConfirmDelete.addEventListener('click', onClickConfirmDelete);
DOM.formEdit.addEventListener('submit', onSubmitEdit); DOM.formEdit.addEventListener('submit', onSubmitEdit);
window.addEventListener('keyup', onKeyUp);
}; };
// === EVENTHANDLER ===== // === EVENTHANDLER =====
const onKeyUp = (e) => {
// if (DOM.inputName.value === '' && DOM.inputPrice.value === '') {
// DOM.btnAdd.disabled = true;
// } else {
// DOM.btnAdd.disabled = false;
// }
DOM.btnAdd.disabled = (DOM.inputName.value === '' || DOM.inputPrice.value === ''); // prettier-ignore
};
const onSubmitEdit = (e) => { const onSubmitEdit = (e) => {
e.preventDefault(); // Standardverhalten unterbinden (Formular nicht an action versenden) e.preventDefault(); // Standardverhalten unterbinden (Formular nicht an action versenden)
// console.log(Object.entries(new FormData(DOM.formEdit)));
const product = { const product = {
name: DOM.inputEditName.value, name: DOM.inputEditName.value,
price: Number(DOM.inputEditPrice.value), price: Number(DOM.inputEditPrice.value),
@@ -61,11 +89,17 @@ const ProductManager = (el = null) => {
}; };
// update async process // update async process
DOM.btnUpdate.querySelector('i').classList.add('fa-spin');
updateProduct(product).then((data) => { updateProduct(product).then((data) => {
console.log(data); console.log(data);
if (data.success) { if (data.success) {
Toast(data.msg, 'success').show();
DOM.btnUpdate.querySelector('i').classList.remove('fa-spin');
loadProducts(); // All Produkte auslesen loadProducts(); // All Produkte auslesen
resetFields();
bsModalEdit.hide(); bsModalEdit.hide();
} else {
Toast(data.msg, 'error').show();
} }
}); });
}; };
@@ -82,7 +116,9 @@ const ProductManager = (el = null) => {
console.log(data); console.log(data);
if (data.success) { if (data.success) {
console.log('add'); console.log('add');
Toast(data.msg, 'success').show();
loadProducts(); loadProducts();
resetFields();
} }
}); });
@@ -105,15 +141,42 @@ const ProductManager = (el = null) => {
const onClickRemove = (e) => { const onClickRemove = (e) => {
const btnEl = e.currentTarget; const btnEl = e.currentTarget;
//const currentRowEl = btnEl.parentNode.parentNode; // parentNode -> td - parentNode -> tr const currentRow = parents(btnEl, 'tr')[0];
// MDN https://developer.mozilla.org/de/docs/Web/API/Element/closest const id = currentRow.dataset.id;
//const currentRowEl = btnEl.closest('tr');
const currentRowEl = parents(btnEl, 'tr')[0]; const productName = currentRow.querySelector('.td-name').textContent.trim();
currentRowEl.remove(); // kein IE11 support DOM.btnConfirmDelete.dataset.id = id;
// DOM.tBody.removeChild(currentRowEl); DOM.productName.textContent = productName;
disableNonFunctionalButtons();
// async fetch
// deleteProduct(id).then((data) => {
// if (data.success) {
// Toast(data.msg, 'success').show();
// console.log('delete: success', data);
// loadProducts();
// } else {
// Toast(data.msg, 'error').show();
// console.error(data);
// }
// });
};
const onClickConfirmDelete = (e) => {
const btnEl = e.currentTarget;
const id = btnEl.dataset.id;
// async fetch
deleteProduct(id).then((data) => {
if (data.success) {
Toast(data.msg, 'success').show();
bsModalDelete.hide();
loadProducts();
} else {
Toast(data.msg, 'error').show();
console.error(data);
}
});
}; };
const onClickMoveUp = (e) => { const onClickMoveUp = (e) => {
@@ -220,6 +283,8 @@ const ProductManager = (el = null) => {
return data; return data;
} catch (error) { } catch (error) {
Toast(error, 'error').show();
console.error(error); console.error(error);
return error; return error;
} }
@@ -285,6 +350,29 @@ const ProductManager = (el = null) => {
} }
}; };
// HTTP - Methode - DELETE
// CRU(D) - delete
// BREA(D) - delete - Produkt löschen
const deleteProduct = async (id) => {
try {
const response = await fetch(`${BASE_URL}/api/products/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
}); // HTTP Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json();
return data;
} catch (error) {
console.error(error);
return error;
}
};
// === FUNCTIONS ======== // === FUNCTIONS ========
const initProducts = () => { const initProducts = () => {
@@ -292,6 +380,7 @@ const ProductManager = (el = null) => {
products.forEach((product) => { products.forEach((product) => {
addProduct(product); addProduct(product);
}); });
Toast('Init products').show();
disableNonFunctionalButtons(); disableNonFunctionalButtons();
}); });
@@ -359,6 +448,14 @@ const ProductManager = (el = null) => {
DOM.inputEditPosition.value = position; DOM.inputEditPosition.value = position;
}; };
const resetFields = () => {
DOM.btnAdd.disabled = true;
DOM.inputPrice.value = '';
DOM.inputName.value = '';
DOM.formEdit.reset();
};
const disableNonFunctionalButtons = () => { const disableNonFunctionalButtons = () => {
Array.from(DOM.tBody.querySelectorAll('tr')).forEach((tr) => { Array.from(DOM.tBody.querySelectorAll('tr')).forEach((tr) => {
const btnMoveUp = tr.querySelector('.button-product-move-up'); const btnMoveUp = tr.querySelector('.button-product-move-up');
@@ -383,6 +480,7 @@ const ProductManager = (el = null) => {
return { return {
init, init,
initProducts, initProducts,
resetFields,
}; };
}; };

View File

@@ -0,0 +1,59 @@
import Toastify from 'toastify-js'; // das Modul wird von node_modules ausgelesen und gebundlet. Funktioniert nur mit JS-Bundler
const Toast = (text = '', variant = 'primary') => {
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 = () => {
Toastify({
text: text,
duration: 2000,
className: variant,
gravity: 'top', // `top` or `bottom`
position: 'center', // `left`, `center` or `right`
stopOnFocus: true, // Prevents dismissing of toast on hover
style: {
background: getColorByName(variant),
},
}).showToast();
};
return {
show,
};
};
export default Toast;

View File

@@ -0,0 +1 @@
@import 'toastify-1.12/toastify';

View File

@@ -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;
}
}

View File

@@ -9,7 +9,8 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bootstrap": "^5.3.8" "bootstrap": "^5.3.8",
"toastify-js": "^1.12.0"
}, },
"devDependencies": { "devDependencies": {
"browser-sync": "^3.0.4", "browser-sync": "^3.0.4",
@@ -4369,6 +4370,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toastify-js": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
"integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==",
"license": "MIT"
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@@ -4,6 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "run-p server css js",
"js": "npx esbuild dev/scripts/main.js --watch --bundle --sourcemap --outfile=assets/js/build.js", "js": "npx esbuild dev/scripts/main.js --watch --bundle --sourcemap --outfile=assets/js/build.js",
"http-server": "npx http-server -p 3000 -c-1", "http-server": "npx http-server -p 3000 -c-1",
"server": "npx browser-sync start --server --files 'assets/css/*.css, assets/js/*.js, **/*.html, *.html'", "server": "npx browser-sync start --server --files 'assets/css/*.css, assets/js/*.js, **/*.html, *.html'",
@@ -21,6 +22,7 @@
"sass": "^1.101.3" "sass": "^1.101.3"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.3.8" "bootstrap": "^5.3.8",
"toastify-js": "^1.12.0"
} }
} }

View File

@@ -49,6 +49,23 @@
<!-- ▼ product-manager ▼ --> <!-- ▼ product-manager ▼ -->
<div class="product-manager"> <div class="product-manager">
<div class="container"> <div class="container">
<nav class="nav-products-actions">
<ul class="list">
<li>
<button class="btn btn-danger button-products-reset">
<i class="fa-solid fa-box-archive"></i>
Reset Products
</button>
</li>
<li>
<button class="btn btn-dark button-products-save">
<i class="fa-solid fa-hard-drive"></i>
Save Products
</button>
</li>
</ul>
</nav>
<!-- table.table.table-striped.table-products>(thead.table-dark>tr>th{Name}+th{Price in &euro;})+tbody>tr>td*2 --> <!-- table.table.table-striped.table-products>(thead.table-dark>tr>th{Name}+th{Price in &euro;})+tbody>tr>td*2 -->
<table class="table table-striped table-products"> <table class="table table-striped table-products">
<thead class="table-dark"> <thead class="table-dark">
@@ -119,7 +136,10 @@
<i class="fas fa-file-pen"></i> <i class="fas fa-file-pen"></i>
<span class="visually-hidden">Edit Product</span> <span class="visually-hidden">Edit Product</span>
</button> </button>
<button class="btn btn-sm btn-danger button-product-remove"> <button
class="btn btn-sm btn-danger button-product-remove"
data-bs-toggle="modal"
data-bs-target="#modal-delete">
<i class="fas fa-trash-can"></i> <i class="fas fa-trash-can"></i>
<span class="visually-hidden">Remove Product</span> <span class="visually-hidden">Remove Product</span>
</button> </button>
@@ -188,6 +208,34 @@
</div> </div>
</div> </div>
<!-- ▲ /Modal ▲ --> <!-- ▲ /Modal ▲ -->
<!-- Modal Delete -->
<div
class="modal modal-delete fade"
id="modal-delete"
tabindex="-1"
aria-labelledby="modal-delete-label"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modal-delete-label">Delete Product</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
Do you really want to delete Product:<br />
<strong class="product-name">PRODUCT_NAME</strong>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger button-confirm-delete">Confirm to delete</button>
</div>
</div>
</div>
</div>
<!-- ▲ /Modal Delete ▲ -->
</div> </div>
<!-- ▲ /product-manager ▲ --> <!-- ▲ /product-manager ▲ -->
</main> </main>