1
0

Create mobile-shopping app

This commit is contained in:
2025-07-18 10:54:23 +02:00
parent 99d531ba8c
commit ffa00654d8
15 changed files with 6598 additions and 0 deletions

421
mobile-shopping/src/App.jsx Normal file
View File

@@ -0,0 +1,421 @@
import React, { useState, useEffect } from 'react';
import { FaPlus, FaTrash, FaCheck, FaEdit, FaShoppingCart, FaClock } from 'react-icons/fa';
import shoppingService from './services/ShoppingService';
import './App.sass';
const App = () => {
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);
const [deferredPrompt, setDeferredPrompt] = useState(null);
const [showInstallPrompt, setShowInstallPrompt] = useState(false);
// Load shopping items on component mount
useEffect(() => {
loadItems();
// Set up auto-refresh every 5 seconds
const interval = setInterval(() => {
loadItems();
}, 5000);
// PWA install prompt handling
const handleBeforeInstallPrompt = (e) => {
e.preventDefault();
setDeferredPrompt(e);
setShowInstallPrompt(true);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
// Cleanup interval and event listener on component unmount
return () => {
clearInterval(interval);
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
};
}, []);
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 handleInstallApp = async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setDeferredPrompt(null);
setShowInstallPrompt(false);
}
}
};
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="app">
<div className="loading">
<FaShoppingCart className="loading-icon" />
<p>Einkaufsliste wird geladen...</p>
</div>
</div>
);
}
return (
<div className="app">
<header className="header">
<div className="header-content">
<h1>
<FaShoppingCart />
Einkaufsliste
</h1>
<div className="header-actions">
{checkedItems.length > 0 && (
<button
className="btn btn-danger"
onClick={handleDeleteChecked}
>
<FaTrash />
{checkedItems.length}
</button>
)}
</div>
</div>
</header>
<main className="main">
{error && (
<div className="error-message">
{error}
<button onClick={() => setError(null)}>×</button>
</div>
)}
{showInstallPrompt && (
<div className="install-prompt">
<p>Diese App auf dem Homescreen installieren?</p>
<div className="install-actions">
<button className="btn btn-primary" onClick={handleInstallApp}>
Installieren
</button>
<button className="btn btn-secondary" onClick={() => setShowInstallPrompt(false)}>
Später
</button>
</div>
</div>
)}
{showAddForm && (
<div className="add-form-overlay">
<form className="add-form" onSubmit={handleAddItem}>
<h3>Neuer Artikel</h3>
<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 })}
/>
</div>
<div className="form-actions">
<button type="button" className="btn btn-secondary" onClick={() => setShowAddForm(false)}>
Abbrechen
</button>
<button type="submit" className="btn btn-primary">
Hinzufügen
</button>
</div>
</form>
</div>
)}
<div className="shopping-content">
{/* Unchecked Items */}
<section 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>
</section>
{/* Checked Items */}
{checkedItems.length > 0 && (
<section 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>
</section>
)}
</div>
</main>
<div className="fab-container">
<button
className="fab"
onClick={() => setShowAddForm(true)}
>
<FaPlus />
</button>
</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 className="edit-actions">
<button className="btn btn-primary btn-small" onClick={handleSave}>
<FaCheck />
</button>
<button className="btn btn-secondary btn-small" onClick={handleCancel}>
×
</button>
</div>
</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-secondary btn-small" onClick={handleEdit}>
<FaEdit />
</button>
<button className="btn btn-danger btn-small" onClick={() => onDelete(item.id)}>
<FaTrash />
</button>
</>
)}
</div>
</div>
);
};
export default App;