diff --git a/client/css/style.css b/client/css/style.css new file mode 100644 index 0000000..3bedf74 --- /dev/null +++ b/client/css/style.css @@ -0,0 +1,200 @@ +/* Custom animations and styles */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.fade-in { + animation: fadeIn 0.5s ease-out; +} + +.slide-in { + animation: slideIn 0.3s ease-out; +} + +/* Card hover effects */ +.inventory-card { + transition: all 0.3s ease; + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + border: 1px solid #e2e8f0; +} + +.inventory-card:hover { + transform: translateY(-5px); + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + border-color: #cbd5e1; +} + +/* Stock level indicators */ +.stock-low { + background-color: #fef2f2; + color: #991b1b; + border: 1px solid #fecaca; +} + +.stock-medium { + background-color: #fffbeb; + color: #92400e; + border: 1px solid #fed7aa; +} + +.stock-high { + background-color: #f0fdf4; + color: #166534; + border: 1px solid #bbf7d0; +} + +/* Button animations */ +.btn-primary { + transition: all 0.2s ease; + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); +} + +.btn-primary:hover { + background: linear-gradient(135deg, #1d4ed8 0%, #1e40af 100%); + transform: translateY(-1px); +} + +.btn-danger { + transition: all 0.2s ease; + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); +} + +.btn-danger:hover { + background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); + transform: translateY(-1px); +} + +/* Form focus states */ +.form-input:focus { + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Modal animations */ +.modal-overlay { + backdrop-filter: blur(4px); + transition: opacity 0.3s ease; +} + +.modal-content { + transition: transform 0.3s ease; +} + +/* Loading spinner */ +.loading-spinner { + border-top-color: #3b82f6; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Price formatting */ +.price-display { + font-feature-settings: 'tnum'; + font-variant-numeric: tabular-nums; +} + +/* Responsive design improvements */ +@media (max-width: 768px) { + .inventory-card { + margin-bottom: 1rem; + } + + .form-grid { + grid-template-columns: 1fr; + } +} + +/* Status badges */ +.status-badge { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.025em; +} + +/* Quantity controls */ +.qty-controls { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.qty-btn { + width: 2rem; + height: 2rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + transition: all 0.2s ease; +} + +.qty-btn:hover { + transform: scale(1.1); +} + +/* Success and error messages */ +.alert { + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.alert-success { + background-color: #ecfdf5; + border-color: #a7f3d0; + color: #047857; +} + +.alert-error { + background-color: #fef2f2; + border-color: #fecaca; + color: #dc2626; +} + +/* Card content spacing */ +.card-content { + padding: 1.5rem; +} + +.card-header { + border-bottom: 1px solid #e5e7eb; + padding-bottom: 0.75rem; + margin-bottom: 1rem; +} + +.card-footer { + border-top: 1px solid #e5e7eb; + padding-top: 0.75rem; + margin-top: 1rem; +} \ No newline at end of file diff --git a/client/index.html b/client/index.html index 3f28a6b..1839788 100644 --- a/client/index.html +++ b/client/index.html @@ -1,31 +1,199 @@ - + + - Widuri Client - + + + Inventory Management + + + - - - - -
- - - - - - - - - + +
+ +
+

+ Inventory Management +

+

Manage your inventory items efficiently

+

Created by Syahdan & Claude Sonnet 4

+
-
- -
IDNamePhoneAct
- - - - - - + +
+

Add New Item

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + + + + + + + + + + +
+

+ Inventory Items +

+
+ +
+
+ + + + + + + + + diff --git a/client/js/script.js b/client/js/script.js new file mode 100644 index 0000000..4a53dcf --- /dev/null +++ b/client/js/script.js @@ -0,0 +1,371 @@ +// Inventory Management System +import ky from "https://cdn.jsdelivr.net/npm/ky@1.8.1/+esm"; + +class InventoryManager { + constructor() { + this.baseUrl = "http://localhost:11000/api"; // Replace with your Bruno API endpoint + this.client = ky.extend({ + prefixUrl: this.baseUrl, + headers: { + "Content-Type": "application/json", + }, + timeout: 10000, + retry: 2, + }); + + this.items = []; + this.init(); + } + + async init() { + this.bindEvents(); + await this.loadItems(); + } + + bindEvents() { + // Add item form + document.getElementById("addItemForm").addEventListener("submit", (e) => { + e.preventDefault(); + this.addItem(); + }); + + // Edit modal events + document.getElementById("editItemForm").addEventListener("submit", (e) => { + e.preventDefault(); + this.updateItem(); + }); + + document.getElementById("cancelEdit").addEventListener("click", () => { + this.closeEditModal(); + }); + + // Close modal when clicking outside + document.getElementById("editModal").addEventListener("click", (e) => { + if (e.target.id === "editModal") { + this.closeEditModal(); + } + }); + + // Close modal on escape key + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + this.closeEditModal(); + } + }); + } + + async loadItems() { + try { + this.showLoading(true); + this.hideMessages(); + + const response = await this.client.get("read"); + const result = await response.json(); + + if (result.status === "success") { + this.items = result; + this.renderItems(); + this.showLoading(false); + } else { + this.showLoading(false); + this.showError(result.message || "Failed to load items"); + } + } catch (error) { + this.showLoading(false); + this.showError( + "Failed to load inventory items. Please check your connection." + ); + console.error("Error loading items:", error); + } + } + + async addItem() { + const form = document.getElementById("addItemForm"); + const formData = new FormData(form); + + const newItem = { + name: formData.get("name").trim(), + price: parseFloat(formData.get("price")), + qty: parseInt(formData.get("qty")), + }; + + // Validation + if (!newItem.name || newItem.price < 0 || newItem.qty < 0) { + this.showError("Please fill in all fields with valid values."); + return; + } + + try { + this.showLoading(true); + this.hideMessages(); + + const response = await this.client.post("create", { + json: newItem, + }); + + const result = await response.json(); + + if (result.status === "success") { + // Add the new item with the ID returned from the API + this.items.data.push(result.data); + + this.renderItems(); + form.reset(); + this.showSuccess(result.message || "Item added successfully!"); + this.showLoading(false); + } else { + this.showLoading(false); + this.showError(result.message || "Failed to add item"); + } + } catch (error) { + this.showLoading(false); + this.showError("Failed to add item. Please try again."); + console.error("Error adding item:", error); + } + } + + async updateItem() { + const id = document.getElementById("editItemId").value; + const form = document.getElementById("editItemForm"); + const formData = new FormData(form); + + const updatedItem = { + id: parseInt(id), + name: formData.get("name").trim(), + price: parseFloat(formData.get("price")), + qty: parseInt(formData.get("qty")), + }; + + // Validation + if (!updatedItem.name || updatedItem.price < 0 || updatedItem.qty < 0) { + this.showError("Please fill in all fields with valid values."); + return; + } + + try { + this.showLoading(true); + this.hideMessages(); + + const response = await this.client.put(`update`, { + json: updatedItem, + }); + + const result = await response.json(); + + if (result.status === "success") { + const index = this.items.data.findIndex((item) => item.id === parseInt(id)); + if (index !== -1) { + this.items.data[index] = result.data; + } + + this.renderItems(); + this.closeEditModal(); + this.showSuccess(result.message || "Item updated successfully!"); + this.showLoading(false); + } else { + this.showLoading(false); + this.showError(result.message || "Failed to update item"); + } + } catch (error) { + this.showLoading(false); + this.showError("Failed to update item. Please try again."); + console.error("Error updating item:", error); + } + } + + async deleteItem(id) { + if (!confirm("Are you sure you want to delete this item?")) { + return; + } + + try { + this.showLoading(true); + this.hideMessages(); + + const response = await this.client.delete(`delete`, { + json: { id: parseInt(id) }, + }); + + const result = await response.json(); + + if (result.status === "success") { + this.items.data = this.items.data.filter((item) => item.id !== parseInt(id)); + this.renderItems(); + this.showSuccess(result.message || "Item deleted successfully!"); + this.showLoading(false); + } else { + this.showLoading(false); + this.showError(result.message || "Failed to delete item"); + } + } catch (error) { + this.showLoading(false); + this.showError("Failed to delete item. Please try again."); + console.error("Error deleting item:", error); + } + } + + openEditModal(item) { + document.getElementById("editItemId").value = item.id; + document.getElementById("editItemName").value = item.name; + document.getElementById("editItemPrice").value = item.price; + document.getElementById("editItemQty").value = item.qty; + + document.getElementById("editModal").classList.remove("hidden"); + document.getElementById("editItemName").focus(); + } + + closeEditModal() { + document.getElementById("editModal").classList.add("hidden"); + document.getElementById("editItemForm").reset(); + } + + renderItems() { + const grid = document.getElementById("inventoryGrid"); + const emptyState = document.getElementById("emptyState"); + + if (!this.items.data || this.items.data.length === 0) { + grid.innerHTML = ""; + emptyState.classList.remove("hidden"); + return; + } + + emptyState.classList.add("hidden"); + + const itemsHTML = this.items.data + .map((item) => this.createItemCard(item)) + .join(""); + grid.innerHTML = itemsHTML; + + // Add event listeners for buttons + this.items.data.forEach((item) => { + const editBtn = document.getElementById(`edit-${item.id}`); + const deleteBtn = document.getElementById(`delete-${item.id}`); + + if (editBtn) { + editBtn.addEventListener("click", () => this.openEditModal(item)); + } + + if (deleteBtn) { + deleteBtn.addEventListener("click", () => this.deleteItem(item.id)); + } + }); + } + + createItemCard(item) { + const stockLevel = this.getStockLevel(item.qty); + const stockClass = this.getStockClass(stockLevel); + + return ` +
+
+
+

+ ${item.name} +

+
+ + $${item.price.toFixed(2)} + + + ${stockLevel} + +
+
+ +
+
+ Quantity: + + ${item.qty} units + +
+ +
+ Total Value: + + $${(item.price * item.qty).toFixed(2)} + +
+
+ + +
+
+ `; + } + + getStockLevel(qty) { + if (qty <= 5) return "Low Stock"; + if (qty <= 20) return "Medium Stock"; + return "High Stock"; + } + + getStockClass(stockLevel) { + switch (stockLevel) { + case "Low Stock": + return "stock-low"; + case "Medium Stock": + return "stock-medium"; + case "High Stock": + return "stock-high"; + default: + return "stock-medium"; + } + } + + showLoading(show) { + const loading = document.getElementById("loading"); + if (show) { + loading.classList.remove("hidden"); + } else { + loading.classList.add("hidden"); + } + } + + showError(message) { + const errorDiv = document.getElementById("errorMessage"); + const errorText = document.getElementById("errorText"); + + errorText.textContent = message; + errorDiv.classList.remove("hidden"); + + setTimeout(() => { + errorDiv.classList.add("hidden"); + }, 5000); + } + + showSuccess(message) { + const successDiv = document.getElementById("successMessage"); + const successText = document.getElementById("successText"); + + successText.textContent = message; + successDiv.classList.remove("hidden"); + + setTimeout(() => { + successDiv.classList.add("hidden"); + }, 3000); + } + + hideMessages() { + document.getElementById("errorMessage").classList.add("hidden"); + document.getElementById("successMessage").classList.add("hidden"); + } +} + +// Initialize the app when DOM is loaded +document.addEventListener("DOMContentLoaded", () => { + new InventoryManager(); +});