Create mobile-shopping app
This commit is contained in:
421
mobile-shopping/src/App.jsx
Normal file
421
mobile-shopping/src/App.jsx
Normal 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;
|
Reference in New Issue
Block a user