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
+
+
+
-
-
-
-
-
-
-
-
- ID |
- Name |
- Phone |
- Act |
-
-
+
+
+
+
+
+ Inventory Management
+
+
Manage your inventory items efficiently
+
Created by Syahdan & Claude Sonnet 4
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Inventory Items
+
+
+
+
+
+
+
+
+
📦
+
+ No items in inventory
+
+
Add your first item to get started!
+
+
+
+
+
+
+
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 `
+
+
+
+
+
+
+ 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();
+});