Files
FarmaFinder/apps/frontend/src/components/SavedNotifications.jsx
T
Antoni Nuñez Romeu 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
feat(i18n): add bilingual support (Català / Castellano)
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.
2026-07-17 09:43:45 +02:00

117 lines
4.6 KiB
React
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, { 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;