1
0
Files
OpenWall/mobile-shopping/src/App.jsx
2025-07-18 10:54:23 +02:00

422 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;