More changes than expected. Mobile version improvements & PWA
Build & Push Docker Images / test-backend (push) Successful in 44s
Build & Push Docker Images / test-frontend (push) Successful in 52s
Build & Push Docker Images / build-backend (push) Failing after 34s
Build & Push Docker Images / build-frontend (push) Failing after 30s

This commit is contained in:
Ichitux
2026-05-20 22:28:17 +02:00
parent 7b288d33cb
commit 2eff93e66a
118 changed files with 13780 additions and 209 deletions
@@ -0,0 +1,113 @@
import React, { useEffect, useState } from 'react';
import './SavedNotifications.css';
function SavedNotifications({ onClose }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [items, setItems] = useState([]);
const [busyId, setBusyId] = useState(null);
useEffect(() => {
let cancelled = false;
(async () => {
try {
const res = await fetch('/api/notifications/mine', { credentials: 'include' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (cancelled) return;
const merged = [
...(data.pharmacy || []),
...(data.global || []),
].sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
setItems(merged);
} catch (err) {
if (!cancelled) setError(err.message || 'Could not load saved notifications');
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => { cancelled = true; };
}, []);
async function handleDelete(item) {
const key = `${item.scope}:${item.id}`;
setBusyId(key);
try {
const res = await fetch('/api/notifications/mine', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ scope: item.scope, id: item.id }),
});
if (!res.ok && res.status !== 204) throw new Error(`HTTP ${res.status}`);
setItems(prev => prev.filter(i => !(i.scope === item.scope && i.id === item.id)));
} catch (err) {
setError(err.message || 'Could not remove notification');
} finally {
setBusyId(null);
}
}
return (
<div className="saved-notifications-backdrop" onClick={onClose}>
<div className="saved-notifications-modal" onClick={e => e.stopPropagation()}>
<div className="saved-notifications-header">
<h2>🔔 Saved Notifications</h2>
<button className="saved-notifications-close" onClick={onClose} aria-label="Close">×</button>
</div>
<div className="saved-notifications-body">
{loading && <p className="saved-notifications-status">Loading</p>}
{!loading && error && <p className="saved-notifications-error">{error}</p>}
{!loading && !error && items.length === 0 && (
<p className="saved-notifications-empty">
No saved notifications yet. Tap the 🔕 bell on an out-of-stock pharmacy to get notified when it's restocked.
</p>
)}
{!loading && !error && items.length > 0 && (
<ul className="saved-notifications-list">
{items.map(item => {
const key = `${item.scope}:${item.id}`;
return (
<li key={key} className="saved-notifications-item">
<div className="saved-notifications-item-main">
<div className="saved-notifications-item-name">
{item.medicine_name || item.medicine_nregistro}
</div>
<div className="saved-notifications-item-meta">
{item.scope === 'pharmacy' ? (
<>
<span className="saved-notifications-chip saved-notifications-chip--pharmacy">
🏥 {item.pharmacy_name || `Pharmacy #${item.pharmacy_id}`}
</span>
{item.pharmacy_address && (
<span className="saved-notifications-address">{item.pharmacy_address}</span>
)}
</>
) : (
<span className="saved-notifications-chip">
Any pharmacy
</span>
)}
</div>
</div>
<button
type="button"
className="saved-notifications-remove"
onClick={() => handleDelete(item)}
disabled={busyId === key}
aria-label="Remove notification"
>
{busyId === key ? '' : 'Remove'}
</button>
</li>
);
})}
</ul>
)}
</div>
</div>
</div>
);
}
export default SavedNotifications;