From 4e2c36e925b96979765e16261ae275be4710d148 Mon Sep 17 00:00:00 2001 From: Miquel Date: Sun, 1 Nov 2026 16:11:22 +0100 Subject: [PATCH 1/6] feat: group account actions in a header menu and add the Arena Cat logo Co-Authored-By: Claude Sonnet 5.5 --- frontend/README.md | 7 +- frontend/src/App.tsx | 80 ++++------------ frontend/src/assets/arena-cat-logo.svg | 1 + frontend/src/components/AccountMenu.tsx | 118 ++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 62 deletions(-) create mode 100644 frontend/src/assets/arena-cat-logo.svg create mode 100644 frontend/src/components/AccountMenu.tsx diff --git a/frontend/README.md b/frontend/README.md index 04cba66..770720d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -24,6 +24,10 @@ fragments d'àudio en català i valorant les transcripcions. Les preguntes i el llindar provenen del backend; l'acreditació queda desada a `users.qualified_at` i evita repetir la prova en sessions posteriors. +La capçalera mostra el logotip i el nom, que duen a l'avaluació (`/`), i l'enllaç al rànquing. +Amb sessió hi apareix també el **menú del compte** (recordatoris, tancar la sessió i donar de +baixa el compte). + El **Rànquing** (`/ranking`) és accessible des de la capçalera amb sessió o sense. Amb sessió, el botó **Avalua** permet tornar a l’avaluació. @@ -122,6 +126,7 @@ s'hagi decidit. | `src/taskStore.ts` | Conserva la tasca en curs entre recàrregues i llegeix el `vote_after` del token. | | `src/errors.ts` | Tradueix al català els errors de validació de FastAPI, que arriben en anglès. | | `src/App.tsx` | Estat de sessió i disposició general (capçalera i peu). | +| `src/components/AccountMenu.tsx` | Menú desplegable del compte a la capçalera. | | `src/components/Login.tsx` | Alta i inici de sessió. | | `src/components/QualificationView.tsx` | Formulari de competència lingüística, correcció i resultat. | | `src/components/TaskView.tsx` | Filtre de categoria, tasca, vot, omissió i final de recorregut. | @@ -153,4 +158,4 @@ s'hagi decidit. Si acaben necessitant URL pròpia (compartir enllaç, botó «enrere»), caldrà `react-router-dom`; si no, es poden fer igual que ara, amb estat de React. -La capçalera ofereix **Recordatoris** (`/reminders`) amb sessió iniciada. Als múltiples de deu vots es mostra una invitació per activar els recordatoris setmanals, com a màxim un cop cada trenta dies i només si no estan activats. Consulteu els [recordatoris per correu](../docs/usuaris_autenticacio.md#recordatoris-voluntaris) per a les preferències i la baixa sense sessió. +El menú del compte ofereix **Recordatoris** (`/reminders`) amb sessió iniciada. Als múltiples de deu vots es mostra una invitació per activar els recordatoris setmanals, com a màxim un cop cada trenta dies i només si no estan activats. Consulteu els [recordatoris per correu](../docs/usuaris_autenticacio.md#recordatoris-voluntaris) per a les preferències i la baixa sense sessió. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 82ef920..34dabed 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,9 @@ import { useCallback, useEffect, useState } from "react"; import { Link, Navigate, Route, Routes, useNavigate, useSearchParams } from "react-router-dom"; import { api, UNAUTHENTICATED_EVENT } from "./api"; +import arenaLogo from "./assets/arena-cat-logo.svg"; import logotip from "./assets/softcatala-logotip.png"; +import AccountMenu from "./components/AccountMenu"; import ActivityView from "./components/ActivityView"; import DeleteAccountDialog from "./components/DeleteAccountDialog"; import ForgotPasswordView from "./components/ForgotPasswordView"; @@ -93,52 +95,28 @@ export default function App() {
-
-

Arena Cat

-

Avaluació humana de models d'IA en català

-
+ + + + Arena Cat + + Avaluació humana de models d'IA en català + + +
Rànquing {session?.authenticated && ( - <> - - Recordatoris - - {session.email} - - {/* Icona sola: `title` per al ratolí i `aria-label` per al lector de - pantalla, que altrament només trobaria un botó sense nom. */} - - + setShowDeleteAccount(true)} + /> )}
@@ -326,23 +304,3 @@ function CategoriesStatus({ error, onRetry }: { error: boolean; onRetry: () => v
); } - -/** Porta amb una fletxa cap enfora: el gest habitual per a «surt». */ -function LogoutIcon() { - return ( - - ); -} diff --git a/frontend/src/assets/arena-cat-logo.svg b/frontend/src/assets/arena-cat-logo.svg new file mode 100644 index 0000000..b3e914f --- /dev/null +++ b/frontend/src/assets/arena-cat-logo.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/components/AccountMenu.tsx b/frontend/src/components/AccountMenu.tsx new file mode 100644 index 0000000..fc01714 --- /dev/null +++ b/frontend/src/components/AccountMenu.tsx @@ -0,0 +1,118 @@ +import { useEffect, useRef, useState } from "react"; +import { Link } from "react-router-dom"; + +interface Props { + email: string | null; + onLogout: () => void; + onDeleteAccount: () => void; +} + +const ITEM = + "block w-full px-4 py-2 text-left text-sm hover:bg-brand-100 focus-visible:bg-brand-100 focus-visible:outline-none"; + +/** Menú desplegable amb les accions del compte de la persona autenticada. */ +export default function AccountMenu({ email, onLogout, onDeleteAccount }: Props) { + const [open, setOpen] = useState(false); + const root = useRef(null); + const button = useRef(null); + + useEffect(() => { + if (!open) return; + const onPointerDown = (event: PointerEvent) => { + if (!root.current?.contains(event.target as Node)) setOpen(false); + }; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + setOpen(false); + button.current?.focus(); + }; + document.addEventListener("pointerdown", onPointerDown); + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("pointerdown", onPointerDown); + document.removeEventListener("keydown", onKeyDown); + }; + }, [open]); + + // Tanca el menú abans d'executar l'acció: alguna n'obre un diàleg i el menú no + // hauria de quedar-se obert darrere. + function run(action: () => void) { + setOpen(false); + action(); + } + + return ( +
+ + {open && ( +
+

+ {email} +

+ setOpen(false)} + className={`${ITEM} text-slate-700`} + > + Recordatoris + +
+ + +
+ )} +
+ ); +} + +function ChevronIcon({ open }: { open: boolean }) { + return ( + + ); +} From 9237257e3b02fc5e2652f180db133cf8ef26f7c4 Mon Sep 17 00:00:00 2001 From: Miquel Date: Sun, 1 Nov 2026 16:19:17 +0100 Subject: [PATCH 2/6] feat: include task skips in the data export Co-Authored-By: Claude Sonnet 5.5 --- backend/app/routes/auth.py | 2 +- backend/app/schemas.py | 11 ++++++ backend/app/services/auth_service.py | 9 ++++- backend/tests/test_auth_api.py | 58 +++++++++++++++++++++++++++- docs/auth_flow_manual.md | 2 +- docs/usuaris_autenticacio.md | 3 +- 6 files changed, 79 insertions(+), 6 deletions(-) diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index 1148052..440d6ac 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -158,7 +158,7 @@ def delete_account( @router.get("/auth/export") def export_data(current_user: CurrentUser, db: DbSession) -> ExportDataResponse: - """Exporta les dades personals i els vots de l'usuari autenticat.""" + """Exporta les dades personals, els vots i les omissions de l'usuari autenticat.""" return auth_service.export_user_data(db, current_user) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 69c7440..3d350f0 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -248,6 +248,16 @@ class ExportVoteResponse(BaseModel): created_at: datetime +class ExportSkipResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + prompt_id: int + response_a_id: int + response_b_id: int + created_at: datetime + + class ExportUserResponse(BaseModel): model_config = ConfigDict(from_attributes=True) @@ -269,6 +279,7 @@ class ExportUserResponse(BaseModel): class ExportDataResponse(BaseModel): user: ExportUserResponse votes: list[ExportVoteResponse] + skips: list[ExportSkipResponse] class PairwiseStat(BaseModel): diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index c7d1cc0..3ac211d 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -8,10 +8,11 @@ from sqlalchemy.orm import Session as OrmSession from app.config import get_settings -from app.models import Session, User, Vote +from app.models import Session, TaskSkip, User, Vote from app.schemas import ( DeleteAccountResponse, ExportDataResponse, + ExportSkipResponse, ExportUserResponse, ExportVoteResponse, ForgotPasswordRequest, @@ -414,12 +415,16 @@ def delete_account( def export_user_data(db: OrmSession, user: User) -> ExportDataResponse: - """Exporta les dades personals i els vots de l'usuari autenticat.""" + """Exporta les dades personals de l'usuari autenticat: el compte, els vots i les omissions.""" votes = db.scalars( select(Vote).where(Vote.user_id == user.id).order_by(Vote.created_at.asc()) ).all() + skips = db.scalars( + select(TaskSkip).where(TaskSkip.user_id == user.id).order_by(TaskSkip.created_at.asc()) + ).all() return ExportDataResponse( user=ExportUserResponse.model_validate(user), votes=[ExportVoteResponse.model_validate(vote) for vote in votes], + skips=[ExportSkipResponse.model_validate(skip) for skip in skips], ) diff --git a/backend/tests/test_auth_api.py b/backend/tests/test_auth_api.py index 1b0c036..c0b391d 100644 --- a/backend/tests/test_auth_api.py +++ b/backend/tests/test_auth_api.py @@ -7,7 +7,7 @@ from sqlalchemy import select from app.config import get_settings -from app.models import Category, Prompt, Response, Session, User, Vote, Winner +from app.models import Category, Prompt, Response, Session, TaskSkip, User, Vote, Winner from app.security import ( compute_email_hash, create_email_verification_token, @@ -309,6 +309,62 @@ def test_export_data_returns_user_and_votes(client, session, logged_in_user): assert data["votes"][0]["winner"] == "a" +def test_export_data_includes_skipped_tasks_in_chronological_order( + client, session, logged_in_user, create_user +): + user = logged_in_user("export_skips@example.com") + other = create_user("export_other@example.com") + category = Category(code="export_skips_cat", name="Categoria omissions") + session.add(category) + session.flush() + prompt = Prompt( + version="v1", code="export_skips", category_id=category.id, text="Text omissions" + ) + session.add(prompt) + session.flush() + first = Response(prompt_id=prompt.id, model="model_A", text="Resposta A") + second = Response(prompt_id=prompt.id, model="model_B", text="Resposta B") + third = Response(prompt_id=prompt.id, model="model_C", text="Resposta C") + session.add_all([first, second, third]) + session.flush() + base = datetime(2026, 1, 1, tzinfo=UTC) + late = TaskSkip( + prompt_id=prompt.id, + user_id=user.id, + response_a_id=first.id, + response_b_id=third.id, + created_at=base + timedelta(days=1), + ) + early = TaskSkip( + prompt_id=prompt.id, + user_id=user.id, + response_a_id=first.id, + response_b_id=second.id, + created_at=base, + ) + foreign = TaskSkip( + prompt_id=prompt.id, + user_id=other.id, + response_a_id=second.id, + response_b_id=third.id, + ) + session.add_all([late, early, foreign]) + session.commit() + + data = client.get("/api/auth/export").json() + + assert [skip["id"] for skip in data["skips"]] == [early.id, late.id] + assert data["skips"][0]["prompt_id"] == prompt.id + assert data["skips"][0]["response_a_id"] == first.id + assert data["skips"][0]["response_b_id"] == second.id + + +def test_export_data_without_skips_returns_an_empty_list(client, logged_in_user): + logged_in_user("export_noskips@example.com") + + assert client.get("/api/auth/export").json()["skips"] == [] + + def test_export_data_requires_session(client): response = client.get("/api/auth/export") diff --git a/docs/auth_flow_manual.md b/docs/auth_flow_manual.md index e4e594f..d992bfc 100644 --- a/docs/auth_flow_manual.md +++ b/docs/auth_flow_manual.md @@ -273,7 +273,7 @@ com l'omissió retornen `410`: cal obtenir una tasca nova. curl -s -b "$COOKIES" "$BASE_URL/api/auth/export" ``` -Resposta esperada (`200`): objecte amb `user` i la llista de `votes`. +Resposta esperada (`200`): objecte amb `user` i les llistes de `votes` i `skips`. --- diff --git a/docs/usuaris_autenticacio.md b/docs/usuaris_autenticacio.md index 01f4978..832ec39 100644 --- a/docs/usuaris_autenticacio.md +++ b/docs/usuaris_autenticacio.md @@ -327,7 +327,7 @@ Les rutes de la taula són les definides a | `GET` | `/auth/session` | *(cookie opcional)* | `{ authenticated, email, email_verified, qualified }` | — | | `POST` | `/auth/logout` | *(cookie)* | `{ status: "logged_out" }` | — | | `POST` | `/auth/delete-account` | `{ current_password }` + cookie | `{ status: "deleted" }` | 401 (sessió/contrasenya) | -| `GET` | `/auth/export` | *(cookie)* | `{ user, votes }` | 401 (sessió) | +| `GET` | `/auth/export` | *(cookie)* | `{ user, votes, skips }` | 401 (sessió) | Els esquemes de petició i resposta són a [`backend/app/schemas.py`](../backend/app/schemas.py). @@ -340,6 +340,7 @@ L'endpoint `GET /auth/export` requereix una sessió activa i retorna: - Les dades del compte (`ExportUserResponse`): `id`, `email`, `email_verified_at`, `qualified_at`, `consent_version`, `consent_at`, `created_at`, `deleted_at`. - Tots els vots de l'usuari (`ExportVoteResponse`), ordenats cronològicament. +- Totes les tasques que ha omès (`ExportSkipResponse`), també per ordre cronològic. L'exportació inclou també els vots de versions antigues dels prompts. From 01767ff44451a6e028c8bc68951d3b11c9130f90 Mon Sep 17 00:00:00 2001 From: Miquel Date: Sun, 1 Nov 2026 16:19:51 +0100 Subject: [PATCH 3/6] feat: let users download their data from the account menu Co-Authored-By: Claude Sonnet 5.5 --- docs/usuaris_autenticacio.md | 5 +++- frontend/README.md | 5 ++-- frontend/src/api.ts | 4 +++ frontend/src/components/AccountMenu.tsx | 35 +++++++++++++++++++++++++ frontend/src/download.ts | 10 +++++++ frontend/src/types.ts | 7 +++++ 6 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 frontend/src/download.ts diff --git a/docs/usuaris_autenticacio.md b/docs/usuaris_autenticacio.md index 832ec39..e917bdc 100644 --- a/docs/usuaris_autenticacio.md +++ b/docs/usuaris_autenticacio.md @@ -344,9 +344,12 @@ L'endpoint `GET /auth/export` requereix una sessió activa i retorna: L'exportació inclou també els vots de versions antigues dels prompts. +El menú del compte de la interfície l'ofereix com a «Descarrega les meves dades», +que baixa la resposta com a fitxer JSON. + ### Baixa i anonimització (`delete_account`) -La interfície ofereix una icona de baixa al costat del correu, a la capçalera. +La interfície ofereix l'opció «Dona de baixa el compte» al menú del compte, a la capçalera. Obre un diàleg que demana la contrasenya actual i informa que els vots i les omissions es conservaran. Després de confirmar la baixa, es mostra la portada. diff --git a/frontend/README.md b/frontend/README.md index 770720d..9cf553a 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -25,8 +25,8 @@ Les preguntes i el llindar provenen del backend; l'acreditació queda desada a `users.qualified_at` i evita repetir la prova en sessions posteriors. La capçalera mostra el logotip i el nom, que duen a l'avaluació (`/`), i l'enllaç al rànquing. -Amb sessió hi apareix també el **menú del compte** (recordatoris, tancar la sessió i donar de -baixa el compte). +Amb sessió hi apareix també el **menú del compte** (recordatoris, descarregar les dades personals, +donar de baixa el compte i tancar la sessió). El **Rànquing** (`/ranking`) és accessible des de la capçalera amb sessió o sense. Amb sessió, el botó **Avalua** permet tornar a l’avaluació. @@ -122,6 +122,7 @@ s'hagi decidit. |---|---| | `src/api.ts` | Client de l'API. `credentials: "include"` a totes les crides, imprescindible per la cookie de sessió. | | `src/types.ts` | Tipus alineats amb `backend/app/schemas.py`. | +| `src/download.ts` | Baixa un objecte com a fitxer JSON (exportació de dades). | | `src/diff.ts` | Separa l'enunciat en instrucció i text, i calcula la diferència per paraules. És el que fa avaluable la categoria «correcció». | | `src/taskStore.ts` | Conserva la tasca en curs entre recàrregues i llegeix el `vote_after` del token. | | `src/errors.ts` | Tradueix al català els errors de validació de FastAPI, que arriben en anglès. | diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1086d9d..7b0ecdf 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -13,6 +13,7 @@ import type { Activity, Category, CategoryFilter, + ExportData, Progress, QualificationChoice, QualificationResult, @@ -110,6 +111,9 @@ export const api = { logout: () => request<{ status: string }>("/auth/logout", { method: "POST" }), + /** Dades personals, vots i omissions de la persona autenticada (RGPD). */ + exportData: () => request("/auth/export"), + deleteAccount: async (password: string) => { try { return await request<{ status: string }>( diff --git a/frontend/src/components/AccountMenu.tsx b/frontend/src/components/AccountMenu.tsx index fc01714..d44d8eb 100644 --- a/frontend/src/components/AccountMenu.tsx +++ b/frontend/src/components/AccountMenu.tsx @@ -1,6 +1,9 @@ import { useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; +import { api, ApiError } from "../api"; +import { downloadJson } from "../download"; + interface Props { email: string | null; onLogout: () => void; @@ -13,6 +16,8 @@ const ITEM = /** Menú desplegable amb les accions del compte de la persona autenticada. */ export default function AccountMenu({ email, onLogout, onDeleteAccount }: Props) { const [open, setOpen] = useState(false); + const [exporting, setExporting] = useState(false); + const [exportError, setExportError] = useState(null); const root = useRef(null); const button = useRef(null); @@ -41,6 +46,22 @@ export default function AccountMenu({ email, onLogout, onDeleteAccount }: Props) action(); } + async function exportData() { + setExporting(true); + setExportError(null); + try { + const date = new Date().toISOString().slice(0, 10); + downloadJson(await api.exportData(), `arena-cat-dades-${date}.json`); + setOpen(false); + } catch (err) { + setExportError( + err instanceof ApiError ? err.message : "No s'han pogut descarregar les dades.", + ); + } finally { + setExporting(false); + } + } + return (
+ {exportError && ( +

+ {exportError} +

+ )}
{open && ( @@ -121,6 +118,7 @@ export default function AccountMenu({ email, onLogout, onDeleteAccount }: Props) > Dona de baixa el compte +
@@ -125,6 +128,7 @@ export default function AccountMenu({ email, onLogout, onDeleteAccount }: Props) onClick={() => run(onLogout)} className={`${ITEM} text-slate-700`} > + Tanca la sessió
@@ -133,6 +137,62 @@ export default function AccountMenu({ email, onLogout, onDeleteAccount }: Props) ); } +/** Icona d'un element del menú: mateix traç que la resta i mida fixa per alinear el text. */ +function MenuIcon({ children }: { children: React.ReactNode }) { + return ( + + ); +} + +/** Porta amb una fletxa cap enfora: el gest habitual per a «surt». */ +function LogoutIcon() { + return ( + + + + + + ); +} + +function BellIcon() { + return ( + + + + + ); +} + +function DownloadIcon() { + return ( + + + + + + ); +} + +function TrashIcon() { + return ( + + + + ); +} + function UserIcon() { return ( Date: Sun, 1 Nov 2026 18:29:42 +0100 Subject: [PATCH 6/6] fix: make the account menu fit narrow screens and keyboard-friendly, and stop caching the data export Drop the ARIA menu roles (no arrow-key navigation is implemented), close the dropdown when focus leaves it, keep it inside the viewport and right-align the header controls when they wrap. Send Cache-Control: no-store on /auth/export. Co-Authored-By: Claude Sonnet 5.5 --- backend/app/routes/auth.py | 4 ++- backend/tests/test_auth_api.py | 6 +++++ docs/usuaris_autenticacio.md | 3 ++- frontend/src/App.tsx | 2 +- frontend/src/components/AccountMenu.tsx | 35 +++++++++---------------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index 440d6ac..c0f3bf4 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -157,8 +157,10 @@ def delete_account( @router.get("/auth/export") -def export_data(current_user: CurrentUser, db: DbSession) -> ExportDataResponse: +def export_data(response: Response, current_user: CurrentUser, db: DbSession) -> ExportDataResponse: """Exporta les dades personals, els vots i les omissions de l'usuari autenticat.""" + # Contenen dades personals: ni el navegador ni els intermediaris les han de desar. + response.headers["Cache-Control"] = "no-store" return auth_service.export_user_data(db, current_user) diff --git a/backend/tests/test_auth_api.py b/backend/tests/test_auth_api.py index c0b391d..ba4c479 100644 --- a/backend/tests/test_auth_api.py +++ b/backend/tests/test_auth_api.py @@ -365,6 +365,12 @@ def test_export_data_without_skips_returns_an_empty_list(client, logged_in_user) assert client.get("/api/auth/export").json()["skips"] == [] +def test_export_data_is_not_cacheable(client, logged_in_user): + logged_in_user("export_cache@example.com") + + assert client.get("/api/auth/export").headers["cache-control"] == "no-store" + + def test_export_data_requires_session(client): response = client.get("/api/auth/export") diff --git a/docs/usuaris_autenticacio.md b/docs/usuaris_autenticacio.md index e917bdc..3577117 100644 --- a/docs/usuaris_autenticacio.md +++ b/docs/usuaris_autenticacio.md @@ -342,7 +342,8 @@ L'endpoint `GET /auth/export` requereix una sessió activa i retorna: - Tots els vots de l'usuari (`ExportVoteResponse`), ordenats cronològicament. - Totes les tasques que ha omès (`ExportSkipResponse`), també per ordre cronològic. -L'exportació inclou també els vots de versions antigues dels prompts. +L'exportació inclou també els vots de versions antigues dels prompts. La resposta porta la +capçalera `Cache-Control: no-store` perquè no es desi en memòria cau. El menú del compte de la interfície l'ofereix com a «Descarrega les meves dades», que baixa la resposta com a fitxer JSON. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 34dabed..1229a39 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -107,7 +107,7 @@ export default function App() { -
+
Rànquing diff --git a/frontend/src/components/AccountMenu.tsx b/frontend/src/components/AccountMenu.tsx index e21fc94..6b54d23 100644 --- a/frontend/src/components/AccountMenu.tsx +++ b/frontend/src/components/AccountMenu.tsx @@ -63,12 +63,18 @@ export default function AccountMenu({ email, onLogout, onDeleteAccount }: Props) } return ( -
+
{ + // Tanca el desplegable quan el focus surt d'aquest bloc (per exemple amb Tab). + if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false); + }} + > {open && ( -
+

{email}

- setOpen(false)} - className={`${ITEM} text-slate-700`} - > + setOpen(false)} className={`${ITEM} text-slate-700`}> Recordatoris -
-