From 6426e333f9da2961a62ddc6fff5acc7b71791ece Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 18 Jul 2025 10:47:44 +0200 Subject: [PATCH] Add shopping feature to dashboard --- dashboard/electron.vite.config.mjs | 11 +- dashboard/src/renderer/src/pages/Shopping.jsx | 386 +++++++++++++- .../src/renderer/src/pages/Shopping.sass | 477 ++++++++++++++++++ dashboard/src/renderer/src/pages/pages.sass | 155 +----- .../renderer/src/services/ShoppingService.js | 51 ++ .../src/renderer/src/utils/RequestUtil.js | 85 ++++ 6 files changed, 1003 insertions(+), 162 deletions(-) create mode 100644 dashboard/src/renderer/src/pages/Shopping.sass create mode 100644 dashboard/src/renderer/src/services/ShoppingService.js create mode 100644 dashboard/src/renderer/src/utils/RequestUtil.js diff --git a/dashboard/electron.vite.config.mjs b/dashboard/electron.vite.config.mjs index 5b54e20..163ac06 100644 --- a/dashboard/electron.vite.config.mjs +++ b/dashboard/electron.vite.config.mjs @@ -15,6 +15,15 @@ export default defineConfig({ '@renderer': resolve('src/renderer/src') } }, - plugins: [react()] + plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:3001', + changeOrigin: true, + secure: false + } + } + } } }) diff --git a/dashboard/src/renderer/src/pages/Shopping.jsx b/dashboard/src/renderer/src/pages/Shopping.jsx index debc603..3c94616 100644 --- a/dashboard/src/renderer/src/pages/Shopping.jsx +++ b/dashboard/src/renderer/src/pages/Shopping.jsx @@ -1,9 +1,379 @@ -const Shopping = () => { - return ( -
-

Einkaufen

-
- ) -} +import React, { useState, useEffect } from 'react'; +import { FaPlus, FaTrash, FaCheck, FaEdit, FaShoppingCart, FaClock } from 'react-icons/fa'; +import shoppingService from '../services/ShoppingService'; +import './Shopping.sass'; -export default Shopping +const Shopping = () => { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [initialLoad, setInitialLoad] = useState(true); + const [error, setError] = useState(null); + const [newItem, setNewItem] = useState({ name: '', amount: '1' }); + const [editingItem, setEditingItem] = useState(null); + const [showAddForm, setShowAddForm] = useState(false); + + // Load shopping items on component mount + useEffect(() => { + loadItems(); + + // Set up auto-refresh every 5 seconds + const interval = setInterval(() => { + loadItems(); + }, 5000); + + // Cleanup interval on component unmount + return () => clearInterval(interval); + }, []); + + const loadItems = async () => { + try { + if (initialLoad) { + setLoading(true); + } + const data = await shoppingService.getItems(); + setItems(data); + setError(null); + } catch (err) { + setError('Failed to load shopping items'); + console.error('Error loading items:', err); + } finally { + if (initialLoad) { + setLoading(false); + setInitialLoad(false); + } + } + }; + + const handleAddItem = async (e) => { + e.preventDefault(); + if (!newItem.name.trim()) return; + + try { + const createdItem = await shoppingService.createItem({ + name: newItem.name.trim(), + amount: newItem.amount.trim() || '1' + }); + setItems([createdItem, ...items]); + setNewItem({ name: '', amount: '1' }); + setShowAddForm(false); + } catch (err) { + setError('Failed to add item'); + console.error('Error adding item:', err); + } + }; + + const handleToggleItem = async (id) => { + try { + const updatedItem = await shoppingService.toggleItem(id); + setItems(items.map(item => + item.id === id ? updatedItem : item + )); + } catch (err) { + setError('Failed to update item'); + console.error('Error toggling item:', err); + } + }; + + const handleDeleteItem = async (id) => { + try { + await shoppingService.deleteItem(id); + setItems(items.filter(item => item.id !== id)); + } catch (err) { + setError('Failed to delete item'); + console.error('Error deleting item:', err); + } + }; + + const handleUpdateItem = async (id, updatedData) => { + try { + const updatedItem = await shoppingService.updateItem(id, updatedData); + setItems(items.map(item => + item.id === id ? updatedItem : item + )); + setEditingItem(null); + } catch (err) { + setError('Failed to update item'); + console.error('Error updating item:', err); + } + }; + + const handleDeleteChecked = async () => { + try { + await shoppingService.deleteCheckedItems(); + setItems(items.filter(item => !item.checked)); + } catch (err) { + setError('Failed to delete checked items'); + console.error('Error deleting checked items:', err); + } + }; + + const formatDate = (dateString) => { + const date = new Date(dateString); + return date.toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + }; + + const getTimeUntilDeletion = (checkedAt) => { + if (!checkedAt) return null; + const twoHoursLater = new Date(new Date(checkedAt).getTime() + 2 * 60 * 60 * 1000); + const now = new Date(); + const timeDiff = twoHoursLater - now; + + if (timeDiff <= 0) return 'Wird gelöscht...'; + + const minutes = Math.floor(timeDiff / (1000 * 60)); + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + + if (hours > 0) { + return `${hours}h ${remainingMinutes}m`; + } + return `${minutes}m`; + }; + + const uncheckedItems = items.filter(item => !item.checked); + const checkedItems = items.filter(item => item.checked); + + if (loading && initialLoad) { + return ( +
+
+ +

Einkaufsliste wird geladen...

+
+
+ ); + } + + return ( +
+
+

+ + Einkaufsliste +

+
+ + {checkedItems.length > 0 && ( + + )} +
+
+ + {error && ( +
+ {error} + +
+ )} + + {showAddForm && ( +
+
+ setNewItem({ ...newItem, name: e.target.value })} + autoFocus + /> + setNewItem({ ...newItem, amount: e.target.value })} + /> + + +
+
+ )} + +
+ {/* Unchecked Items */} +
+

Zu kaufen ({uncheckedItems.length})

+
+ {uncheckedItems.length === 0 ? ( +
+ +

Keine Artikel auf der Einkaufsliste

+
+ ) : ( + uncheckedItems.map(item => ( + + )) + )} +
+
+ + {/* Checked Items */} + {checkedItems.length > 0 && ( +
+

Erledigt ({checkedItems.length})

+
+ {checkedItems.map(item => ( + + ))} +
+
+ )} +
+
+ ); +}; + +// Shopping Item Component +const ShoppingItem = ({ + item, + onToggle, + onDelete, + onUpdate, + editingItem, + setEditingItem, + formatDate, + getTimeUntilDeletion +}) => { + const [editData, setEditData] = useState({ name: item.name, amount: item.amount }); + + const handleEdit = () => { + setEditingItem(item.id); + setEditData({ name: item.name, amount: item.amount }); + }; + + const handleSave = () => { + onUpdate(item.id, editData); + }; + + const handleCancel = () => { + setEditingItem(null); + setEditData({ name: item.name, amount: item.amount }); + }; + + const isEditing = editingItem === item.id; + + return ( +
+ + +
+ {isEditing ? ( +
+ setEditData({ ...editData, name: e.target.value })} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSave(); + if (e.key === 'Escape') handleCancel(); + }} + autoFocus + /> + setEditData({ ...editData, amount: e.target.value })} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSave(); + if (e.key === 'Escape') handleCancel(); + }} + /> +
+ ) : ( + <> +
+ {item.name} + {item.amount} +
+
+ + {formatDate(item.date)} + + {item.checked && item.checkedAt && getTimeUntilDeletion && ( + + + {getTimeUntilDeletion(item.checkedAt)} + + )} +
+ + )} +
+ +
+ {isEditing ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+ ); +}; + +export default Shopping; diff --git a/dashboard/src/renderer/src/pages/Shopping.sass b/dashboard/src/renderer/src/pages/Shopping.sass new file mode 100644 index 0000000..d2f1ed8 --- /dev/null +++ b/dashboard/src/renderer/src/pages/Shopping.sass @@ -0,0 +1,477 @@ +// Shopping Page - Single Unified Glassmorphism Container +.shopping-container + position: absolute + top: 7rem + left: 0.5rem + right: 0.5rem + bottom: 0.5rem + background: rgba(255, 255, 255, 0.35) + border-radius: 24px + backdrop-filter: blur(40px) + border: 1px solid rgba(255, 255, 255, 0.4) + box-shadow: 0 16px 64px rgba(0, 0, 0, 0.1) + padding: 2rem + display: flex + flex-direction: column + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif + overflow: hidden + + .loading + display: flex + flex-direction: column + align-items: center + justify-content: center + flex: 1 + color: #1e293b + + .loading-icon + font-size: 3rem + margin-bottom: 1rem + color: #4CAF50 + animation: spin 2s linear infinite + + p + font-size: 1.1rem + margin: 0 + font-weight: 600 + + .shopping-header + display: flex + justify-content: space-between + align-items: center + margin-bottom: 2rem + padding-bottom: 1.5rem + border-bottom: 1px solid rgba(255, 255, 255, 0.3) + flex-wrap: wrap + gap: 1.5rem + flex-shrink: 0 + + h1 + display: flex + align-items: center + gap: 0.75rem + margin: 0 + color: #1e293b + font-size: 2.25rem + font-weight: 700 + + svg + color: #4CAF50 + font-size: 2rem + + .header-actions + display: flex + gap: 0.75rem + flex-wrap: wrap + + .error-message + background: rgba(244, 67, 54, 0.1) + border: 1px solid rgba(244, 67, 54, 0.3) + color: #dc2626 + padding: 1rem 1.5rem + border-radius: 16px + margin-bottom: 1.5rem + display: flex + justify-content: space-between + align-items: center + border-left: 4px solid #f44336 + font-weight: 600 + flex-shrink: 0 + + button + background: rgba(244, 67, 54, 0.1) + border: 1px solid rgba(244, 67, 54, 0.3) + font-size: 1.2rem + cursor: pointer + color: #dc2626 + padding: 0.5rem + border-radius: 8px + transition: all 0.3s ease + font-weight: 600 + + &:hover + background: rgba(244, 67, 54, 0.2) + transform: scale(1.1) + + .add-item-form + margin-bottom: 2rem + flex-shrink: 0 + transition: all 0.3s ease + + .form-group + display: flex + gap: 1rem + align-items: center + flex-wrap: wrap + + input + flex: 1 + min-width: 200px + padding: 1rem 1.25rem + border: 1px solid rgba(255, 255, 255, 0.5) + border-radius: 12px + background: rgba(255, 255, 255, 0.6) + backdrop-filter: blur(20px) + font-size: 1rem + color: #1e293b + font-weight: 500 + transition: all 0.3s ease + + &:focus + outline: none + border-color: rgba(76, 175, 80, 0.7) + background: rgba(255, 255, 255, 0.8) + box-shadow: 0 8px 32px rgba(76, 175, 80, 0.2) + transform: translateY(-1px) + + &::placeholder + color: rgba(30, 41, 59, 0.7) + font-weight: 500 + + .shopping-lists + flex: 1 + display: flex + flex-direction: column + gap: 2rem + overflow-y: auto + padding-right: 0.5rem + + &::-webkit-scrollbar + width: 6px + + &::-webkit-scrollbar-track + background: rgba(255, 255, 255, 0.1) + border-radius: 3px + + &::-webkit-scrollbar-thumb + background: rgba(255, 255, 255, 0.3) + border-radius: 3px + + &:hover + background: rgba(255, 255, 255, 0.5) + + .shopping-section + flex-shrink: 0 + + h2 + color: #1e293b + margin: 0 0 1.5rem 0 + font-size: 1.5rem + font-weight: 700 + display: flex + align-items: center + gap: 0.5rem + padding-bottom: 1rem + border-bottom: 1px solid rgba(255, 255, 255, 0.2) + + .empty-state + display: flex + flex-direction: column + align-items: center + padding: 3rem 2rem + color: #64748b + text-align: center + background: rgba(255, 255, 255, 0.2) + border-radius: 16px + border: 1px solid rgba(255, 255, 255, 0.3) + margin-top: 1rem + + svg + font-size: 3rem + margin-bottom: 1rem + opacity: 0.6 + + p + font-size: 1.1rem + margin: 0 + font-weight: 600 + + .items-list + display: flex + flex-direction: column + gap: 1rem + margin-top: 1rem + + .shopping-item + display: flex + align-items: center + gap: 1.25rem + padding: 1.5rem + background: rgba(255, 255, 255, 0.4) + border: 1px solid rgba(255, 255, 255, 0.6) + border-radius: 16px + transition: all 0.3s ease + backdrop-filter: blur(20px) + min-height: 80px + + &:hover + transform: translateY(-2px) + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15) + background: rgba(255, 255, 255, 0.55) + border-color: rgba(255, 255, 255, 0.8) + + &.checked + background: rgba(76, 175, 80, 0.15) + border-color: rgba(76, 175, 80, 0.4) + + .item-name + text-decoration: line-through + color: #64748b + + .check-btn + background: linear-gradient(135deg, #4CAF50, #388e3c) + border-color: #4CAF50 + color: white + box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4) + + .check-btn + width: 48px + height: 48px + border-radius: 50% + border: 2px solid rgba(76, 175, 80, 0.5) + background: rgba(255, 255, 255, 0.6) + display: flex + align-items: center + justify-content: center + cursor: pointer + transition: all 0.3s ease + flex-shrink: 0 + backdrop-filter: blur(10px) + + &:hover + border-color: #4CAF50 + background: rgba(76, 175, 80, 0.2) + transform: scale(1.1) + box-shadow: 0 6px 20px rgba(76, 175, 80, 0.3) + + svg + font-size: 1.1rem + font-weight: 700 + + .item-content + flex: 1 + min-width: 0 + + .item-main + display: flex + justify-content: space-between + align-items: center + margin-bottom: 0.75rem + + .item-name + font-weight: 700 + color: #1e293b + font-size: 1.15rem + flex: 1 + min-width: 0 + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + + .item-amount + background: rgba(59, 130, 246, 0.2) + color: #1e40af + padding: 0.5rem 1rem + border-radius: 10px + font-size: 0.9rem + font-weight: 700 + white-space: nowrap + margin-left: 1rem + border: 1px solid rgba(59, 130, 246, 0.4) + backdrop-filter: blur(10px) + + .item-meta + display: flex + justify-content: space-between + align-items: center + gap: 1rem + + .item-date + color: #64748b + font-size: 0.9rem + font-weight: 600 + + .deletion-timer + display: flex + align-items: center + gap: 0.5rem + color: #f59e0b + font-size: 0.9rem + font-weight: 700 + background: rgba(245, 158, 11, 0.15) + padding: 0.375rem 0.75rem + border-radius: 8px + border: 1px solid rgba(245, 158, 11, 0.4) + backdrop-filter: blur(10px) + + svg + font-size: 0.8rem + + .edit-form + display: flex + gap: 1rem + width: 100% + + input + padding: 1rem + border: 1px solid rgba(255, 255, 255, 0.6) + border-radius: 10px + font-size: 1rem + background: rgba(255, 255, 255, 0.7) + backdrop-filter: blur(10px) + color: #1e293b + font-weight: 600 + + &:focus + outline: none + border-color: rgba(76, 175, 80, 0.7) + background: rgba(255, 255, 255, 0.85) + box-shadow: 0 4px 16px rgba(76, 175, 80, 0.2) + + input:first-child + flex: 2 + + input:last-child + flex: 1 + min-width: 100px + + .item-actions + display: flex + gap: 0.75rem + flex-shrink: 0 + + .btn + padding: 0.875rem 1.75rem + border: 1px solid rgba(255, 255, 255, 0.5) + border-radius: 12px + cursor: pointer + font-size: 0.95rem + font-weight: 700 + display: flex + align-items: center + gap: 0.75rem + transition: all 0.3s ease + white-space: nowrap + backdrop-filter: blur(20px) + + &:hover + transform: translateY(-2px) + + &.btn-primary + background: linear-gradient(135deg, rgba(76, 175, 80, 0.4), rgba(56, 142, 60, 0.4)) + color: white + box-shadow: 0 6px 20px rgba(76, 175, 80, 0.2) + border-color: rgba(76, 175, 80, 0.5) + + &:hover + background: linear-gradient(135deg, rgba(76, 175, 80, 0.6), rgba(56, 142, 60, 0.6)) + box-shadow: 0 12px 40px rgba(76, 175, 80, 0.3) + + &.btn-secondary + background: linear-gradient(135deg, rgba(100, 116, 139, 0.4), rgba(71, 85, 105, 0.4)) + color: white + border-color: rgba(100, 116, 139, 0.5) + + &:hover + background: linear-gradient(135deg, rgba(100, 116, 139, 0.6), rgba(71, 85, 105, 0.6)) + box-shadow: 0 12px 40px rgba(100, 116, 139, 0.3) + + &.btn-danger + background: linear-gradient(135deg, rgba(244, 67, 54, 0.4), rgba(211, 47, 47, 0.4)) + color: white + border-color: rgba(244, 67, 54, 0.5) + + &:hover + background: linear-gradient(135deg, rgba(244, 67, 54, 0.6), rgba(211, 47, 47, 0.6)) + box-shadow: 0 12px 40px rgba(244, 67, 54, 0.3) + + &.btn-small + padding: 0.625rem 1rem + font-size: 0.85rem + min-width: 44px + justify-content: center + + svg + margin: 0 + font-size: 0.9rem + +@keyframes spin + from + transform: rotate(0deg) + to + transform: rotate(360deg) + +// Enhanced Responsive Design +@media (max-width: 768px) + .shopping-container + top: 6.5rem + left: 0.375rem + right: 0.375rem + bottom: 0.375rem + padding: 1.5rem + + .shopping-header + flex-direction: column + align-items: stretch + text-align: center + padding-bottom: 1rem + + h1 + justify-content: center + margin-bottom: 1rem + font-size: 2rem + + .header-actions + justify-content: center + + .add-item-form + .form-group + flex-direction: column + + input + min-width: auto + width: 100% + + .shopping-item + padding: 1.25rem + + .item-content .item-main + flex-direction: column + align-items: flex-start + gap: 0.75rem + + .item-amount + margin-left: 0 + align-self: flex-start + + .item-actions + flex-direction: column + +@media (max-width: 480px) + .shopping-container + top: 6rem + left: 0.25rem + right: 0.25rem + bottom: 0.25rem + padding: 1rem + + .shopping-item + flex-direction: column + align-items: stretch + gap: 1.25rem + padding: 1rem + + .check-btn + align-self: flex-start + + .item-actions + flex-direction: row + justify-content: flex-end + gap: 0.5rem + + .btn + padding: 0.75rem 1rem + font-size: 0.85rem + + &.btn-small + padding: 0.5rem 0.75rem diff --git a/dashboard/src/renderer/src/pages/pages.sass b/dashboard/src/renderer/src/pages/pages.sass index 47550ce..abc3a26 100644 --- a/dashboard/src/renderer/src/pages/pages.sass +++ b/dashboard/src/renderer/src/pages/pages.sass @@ -1,4 +1,5 @@ -// Pages Styles - Light Mode with Glass Effects +// Import individual page styles +@import './Shopping.sass' // Common page container .page-container @@ -121,158 +122,6 @@ background: linear-gradient(135deg, rgba(168, 85, 247, 0.3), rgba(147, 51, 234, 0.3)) color: white -// Shopping Styles -.shopping-container - max-width: 800px - margin: 0 auto - -.shopping-header - display: flex - justify-content: space-between - align-items: center - margin-bottom: 2rem - - h1 - display: flex - align-items: center - gap: 0.75rem - font-size: 2rem - font-weight: 700 - color: #1e293b - margin: 0 - -.shopping-stats - display: flex - gap: 1rem - - span - background: rgba(255, 255, 255, 0.3) - padding: 0.5rem 1rem - border-radius: 12px - font-size: 0.875rem - font-weight: 500 - color: #475569 - -.add-item-form - background: rgba(255, 255, 255, 0.25) - border-radius: 20px - padding: 1.5rem - margin-bottom: 2rem - backdrop-filter: blur(40px) - border: 1px solid rgba(255, 255, 255, 0.3) - -.input-group - display: flex - gap: 1rem - - input - flex: 1 - padding: 0.75rem 1rem - border: 1px solid rgba(255, 255, 255, 0.4) - border-radius: 12px - background: rgba(255, 255, 255, 0.3) - color: #1e293b - font-size: 1rem - backdrop-filter: blur(20px) - - &::placeholder - color: rgba(30, 41, 59, 0.6) - - &:focus - outline: none - border-color: rgba(59, 130, 246, 0.5) - background: rgba(255, 255, 255, 0.4) - -.add-btn - background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(5, 150, 105, 0.3)) - border: 1px solid rgba(255, 255, 255, 0.4) - border-radius: 12px - padding: 0.75rem 1rem - color: white - cursor: pointer - transition: all 0.3s ease - - &:hover - background: linear-gradient(135deg, rgba(16, 185, 129, 0.4), rgba(5, 150, 105, 0.4)) - transform: translateY(-2px) - -.shopping-section - margin-bottom: 2rem - - h3 - font-size: 1.25rem - font-weight: 600 - color: #1e293b - margin-bottom: 1rem - -.items-list - background: rgba(255, 255, 255, 0.25) - border-radius: 20px - padding: 1.5rem - backdrop-filter: blur(40px) - border: 1px solid rgba(255, 255, 255, 0.3) - -.shopping-item - display: flex - align-items: center - gap: 1rem - padding: 1rem 0 - border-bottom: 1px solid rgba(255, 255, 255, 0.2) - - &:last-child - border-bottom: none - - &.completed - opacity: 0.6 - - .item-name - text-decoration: line-through - -.check-btn - width: 32px - height: 32px - border-radius: 50% - border: 2px solid rgba(16, 185, 129, 0.5) - background: transparent - color: transparent - cursor: pointer - transition: all 0.3s ease - - &.checked - background: linear-gradient(135deg, rgba(16, 185, 129, 0.8), rgba(5, 150, 105, 0.8)) - color: white - - &:hover - border-color: rgba(16, 185, 129, 0.8) - background: rgba(16, 185, 129, 0.1) - -.item-details - flex: 1 - display: flex - flex-direction: column - gap: 0.25rem - -.item-name - font-weight: 500 - color: #1e293b - -.item-quantity, .item-category - font-size: 0.875rem - color: #64748b - -.delete-btn - background: transparent - border: none - color: #ef4444 - cursor: pointer - padding: 0.5rem - border-radius: 8px - transition: all 0.3s ease - - &:hover - background: rgba(239, 68, 68, 0.1) - color: #dc2626 - // Notes Styles .notes-container max-width: 1200px diff --git a/dashboard/src/renderer/src/services/ShoppingService.js b/dashboard/src/renderer/src/services/ShoppingService.js new file mode 100644 index 0000000..52ccd35 --- /dev/null +++ b/dashboard/src/renderer/src/services/ShoppingService.js @@ -0,0 +1,51 @@ +import requestUtil from '../utils/RequestUtil'; + +class ShoppingService { + constructor() { + this.endpoint = '/api/shopping'; + } + + // Get all shopping items + async getItems() { + return requestUtil.get(this.endpoint); + } + + // Get a specific shopping item + async getItem(id) { + return requestUtil.get(`${this.endpoint}/${id}`); + } + + // Create a new shopping item + async createItem(item) { + return requestUtil.post(this.endpoint, item); + } + + // Update a shopping item + async updateItem(id, item) { + return requestUtil.put(`${this.endpoint}/${id}`, item); + } + + // Toggle checked status of an item + async toggleItem(id) { + return requestUtil.patch(`${this.endpoint}/${id}/toggle`); + } + + // Delete a shopping item + async deleteItem(id) { + return requestUtil.delete(`${this.endpoint}/${id}`); + } + + // Delete all checked items + async deleteCheckedItems() { + return requestUtil.delete(`${this.endpoint}/checked/all`); + } + + // Health check + async healthCheck() { + return requestUtil.get('/api/health'); + } +} + +// Create and export a singleton instance +const shoppingService = new ShoppingService(); +export default shoppingService; diff --git a/dashboard/src/renderer/src/utils/RequestUtil.js b/dashboard/src/renderer/src/utils/RequestUtil.js new file mode 100644 index 0000000..aff454a --- /dev/null +++ b/dashboard/src/renderer/src/utils/RequestUtil.js @@ -0,0 +1,85 @@ +class RequestUtil { + constructor() { + this.baseURL = this.getBaseURL(); + } + + getBaseURL() { + // In production, use the static endpoint, otherwise use proxy + if (process.env.NODE_ENV === 'production') { + return 'https://static.endpoint.com'; + } + return ''; // Uses proxy in development + } + + async request(endpoint, options = {}) { + const url = `${this.baseURL}${endpoint}`; + + const defaultOptions = { + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }; + + const config = { + ...defaultOptions, + ...options + }; + + try { + const response = await fetch(url, config); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Request failed' })); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('Request failed:', error); + throw error; + } + } + + // GET request + async get(endpoint) { + return this.request(endpoint, { + method: 'GET' + }); + } + + // POST request + async post(endpoint, data = {}) { + return this.request(endpoint, { + method: 'POST', + body: JSON.stringify(data) + }); + } + + // PUT request + async put(endpoint, data = {}) { + return this.request(endpoint, { + method: 'PUT', + body: JSON.stringify(data) + }); + } + + // PATCH request + async patch(endpoint, data = {}) { + return this.request(endpoint, { + method: 'PATCH', + body: JSON.stringify(data) + }); + } + + // DELETE request + async delete(endpoint) { + return this.request(endpoint, { + method: 'DELETE' + }); + } +} + +// Create and export a singleton instance +const requestUtil = new RequestUtil(); +export default requestUtil;