38374341ed
Run Tests on Branches / Detect Changes (push) Successful in 11s
Run Tests on Branches / Backend Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Successful in 1m44s
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Add lightweight i18n system using React Context + useTranslation hook.
No external dependencies. Language persisted in localStorage('ff-lang').
New files:
- src/i18n/locales/ca.js (~410 translation keys)
- src/i18n/locales/es.js (~410 translation keys)
- src/i18n/LanguageContext.jsx (Provider + t() function)
- src/i18n/useTranslation.js (hook)
- src/i18n/index.js (barrel export)
Modified 22 files:
- main.jsx: wrap App with LanguageProvider
- App.test.jsx: add LanguageProvider wrapper for tests
- 13 user components: BottomNav, HomeView, LoginModal, SearchView,
ProfileView, AlertsView, ScannerView, PharmacyList, MedicineResults,
ProductResults, SavedNotifications, ErrorBoundary, SearchBar
- 6 admin components: AdminView, LoginForm, PharmacyManagement,
MedicineManagement, PharmacyMedicineLink, PharmacyProductLink
ProfileView includes language selector (globe icon + modal CA/ES)
placed next to the Theme selector.
No routes changed. No API calls affected. No navigation changes.
117 lines
4.6 KiB
React
117 lines
4.6 KiB
React
import React, { useEffect, useState } from 'react';
|
||
import { useTranslation } from '../i18n';
|
||
import './SavedNotifications.css';
|
||
|
||
function SavedNotifications({ onClose, onNotificationChange }) {
|
||
const { t } = useTranslation();
|
||
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 || t('savedNotifications.loadError'));
|
||
} 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)));
|
||
onNotificationChange?.();
|
||
} catch (err) {
|
||
setError(err.message || t('savedNotifications.deleteError'));
|
||
} 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>{t('savedNotifications.title')}</h2>
|
||
<button className="saved-notifications-close" onClick={onClose} aria-label={t('savedNotifications.close')}>×</button>
|
||
</div>
|
||
<div className="saved-notifications-body">
|
||
{loading && <p className="saved-notifications-status">{t('savedNotifications.loading')}</p>}
|
||
{!loading && error && <p className="saved-notifications-error">{error}</p>}
|
||
{!loading && !error && items.length === 0 && (
|
||
<p className="saved-notifications-empty">
|
||
{t('savedNotifications.empty')}
|
||
</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 || `${t('savedNotifications.anyPharmacy')} #${item.pharmacy_id}`}
|
||
</span>
|
||
{item.pharmacy_address && (
|
||
<span className="saved-notifications-address">{item.pharmacy_address}</span>
|
||
)}
|
||
</>
|
||
) : (
|
||
<span className="saved-notifications-chip">
|
||
{t('savedNotifications.anyPharmacy')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
className="saved-notifications-remove"
|
||
onClick={() => handleDelete(item)}
|
||
disabled={busyId === key}
|
||
aria-label={t('savedNotifications.delete')}
|
||
>
|
||
{busyId === key ? '…' : t('savedNotifications.delete')}
|
||
</button>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default SavedNotifications;
|