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 && (
+
+ )}
+
+
+ {/* 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;