1
0

Add shopping feature to dashboard

This commit is contained in:
2025-07-18 10:47:44 +02:00
parent 68480757dc
commit 6426e333f9
6 changed files with 1003 additions and 162 deletions

View File

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

View File

@@ -1,9 +1,379 @@
const Shopping = () => {
return (
<div>
<h1>Einkaufen</h1>
</div>
)
}
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 (
<div className="shopping-container">
<div className="loading">
<FaShoppingCart className="loading-icon" />
<p>Einkaufsliste wird geladen...</p>
</div>
</div>
);
}
return (
<div className="shopping-container">
<div className="shopping-header">
<h1>
<FaShoppingCart />
Einkaufsliste
</h1>
<div className="header-actions">
<button
className="btn btn-primary"
onClick={() => setShowAddForm(!showAddForm)}
>
<FaPlus />
Artikel hinzufügen
</button>
{checkedItems.length > 0 && (
<button
className="btn btn-danger"
onClick={handleDeleteChecked}
>
<FaTrash />
Erledigte löschen ({checkedItems.length})
</button>
)}
</div>
</div>
{error && (
<div className="error-message">
{error}
<button onClick={() => setError(null)}>×</button>
</div>
)}
{showAddForm && (
<form className="add-item-form" onSubmit={handleAddItem}>
<div className="form-group">
<input
type="text"
placeholder="Artikelname..."
value={newItem.name}
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
autoFocus
/>
<input
type="text"
placeholder="Menge"
value={newItem.amount}
onChange={(e) => setNewItem({ ...newItem, amount: e.target.value })}
/>
<button type="submit" className="btn btn-primary">
<FaPlus />
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowAddForm(false);
setNewItem({ name: '', amount: '1' });
}}
>
Abbrechen
</button>
</div>
</form>
)}
<div className="shopping-lists">
{/* Unchecked Items */}
<div className="shopping-section">
<h2>Zu kaufen ({uncheckedItems.length})</h2>
<div className="items-list">
{uncheckedItems.length === 0 ? (
<div className="empty-state">
<FaShoppingCart />
<p>Keine Artikel auf der Einkaufsliste</p>
</div>
) : (
uncheckedItems.map(item => (
<ShoppingItem
key={item.id}
item={item}
onToggle={handleToggleItem}
onDelete={handleDeleteItem}
onUpdate={handleUpdateItem}
editingItem={editingItem}
setEditingItem={setEditingItem}
formatDate={formatDate}
/>
))
)}
</div>
</div>
{/* Checked Items */}
{checkedItems.length > 0 && (
<div className="shopping-section">
<h2>Erledigt ({checkedItems.length})</h2>
<div className="items-list">
{checkedItems.map(item => (
<ShoppingItem
key={item.id}
item={item}
onToggle={handleToggleItem}
onDelete={handleDeleteItem}
onUpdate={handleUpdateItem}
editingItem={editingItem}
setEditingItem={setEditingItem}
formatDate={formatDate}
getTimeUntilDeletion={getTimeUntilDeletion}
/>
))}
</div>
</div>
)}
</div>
</div>
);
};
// 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 (
<div className={`shopping-item ${item.checked ? 'checked' : ''}`}>
<button
className="check-btn"
onClick={() => onToggle(item.id)}
>
<FaCheck />
</button>
<div className="item-content">
{isEditing ? (
<div className="edit-form">
<input
type="text"
value={editData.name}
onChange={(e) => setEditData({ ...editData, name: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave();
if (e.key === 'Escape') handleCancel();
}}
autoFocus
/>
<input
type="text"
value={editData.amount}
onChange={(e) => setEditData({ ...editData, amount: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave();
if (e.key === 'Escape') handleCancel();
}}
/>
</div>
) : (
<>
<div className="item-main">
<span className="item-name">{item.name}</span>
<span className="item-amount">{item.amount}</span>
</div>
<div className="item-meta">
<span className="item-date">
{formatDate(item.date)}
</span>
{item.checked && item.checkedAt && getTimeUntilDeletion && (
<span className="deletion-timer">
<FaClock />
{getTimeUntilDeletion(item.checkedAt)}
</span>
)}
</div>
</>
)}
</div>
<div className="item-actions">
{isEditing ? (
<>
<button className="btn btn-primary btn-small" onClick={handleSave}>
<FaCheck />
</button>
<button className="btn btn-secondary btn-small" onClick={handleCancel}>
×
</button>
</>
) : (
<>
<button className="btn btn-secondary btn-small" onClick={handleEdit}>
<FaEdit />
</button>
<button className="btn btn-danger btn-small" onClick={() => onDelete(item.id)}>
<FaTrash />
</button>
</>
)}
</div>
</div>
);
};
export default Shopping;

View File

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

View File

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

View File

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

View File

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