diff --git a/messages/de.json b/messages/de.json index 277b88ac3..62fa4aec1 100644 --- a/messages/de.json +++ b/messages/de.json @@ -34,7 +34,8 @@ "retry": "Erneut versuchen", "reportIssue": "Fehler melden", "showPassword": "Passwort anzeigen", - "hidePassword": "Passwort verbergen" + "hidePassword": "Passwort verbergen", + "learnMore": "Mehr erfahren" }, "doctorReport": { "title": "Gesundheitsbericht", diff --git a/messages/en.json b/messages/en.json index bf05239fc..30a6934e1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -34,7 +34,8 @@ "retry": "Retry", "reportIssue": "Report issue", "showPassword": "Show password", - "hidePassword": "Hide password" + "hidePassword": "Hide password", + "learnMore": "Learn more" }, "doctorReport": { "title": "Health Report", diff --git a/messages/es.json b/messages/es.json index bb1b6992f..9ab22a6a3 100644 --- a/messages/es.json +++ b/messages/es.json @@ -34,7 +34,8 @@ "retry": "Reintentar", "reportIssue": "Reportar problema", "showPassword": "Mostrar contraseña", - "hidePassword": "Ocultar contraseña" + "hidePassword": "Ocultar contraseña", + "learnMore": "Más información" }, "doctorReport": { "title": "Informe de salud", diff --git a/messages/fr.json b/messages/fr.json index fb09bcfa2..d2df8eb8d 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -34,7 +34,8 @@ "retry": "Réessayer", "reportIssue": "Signaler un problème", "showPassword": "Afficher le mot de passe", - "hidePassword": "Masquer le mot de passe" + "hidePassword": "Masquer le mot de passe", + "learnMore": "En savoir plus" }, "doctorReport": { "title": "Rapport de santé", diff --git a/messages/it.json b/messages/it.json index 9c2a5496c..46e4e9c83 100644 --- a/messages/it.json +++ b/messages/it.json @@ -34,7 +34,8 @@ "retry": "Riprova", "reportIssue": "Segnala un problema", "showPassword": "Mostra password", - "hidePassword": "Nascondi password" + "hidePassword": "Nascondi password", + "learnMore": "Scopri di più" }, "doctorReport": { "title": "Rapporto sanitario", diff --git a/messages/pl.json b/messages/pl.json index e4c29338e..6df51477c 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -34,7 +34,8 @@ "retry": "Spróbuj ponownie", "reportIssue": "Zgłoś problem", "showPassword": "Pokaż hasło", - "hidePassword": "Ukryj hasło" + "hidePassword": "Ukryj hasło", + "learnMore": "Dowiedz się więcej" }, "doctorReport": { "title": "Raport zdrowotny", diff --git a/src/components/ui/learn-more-link.tsx b/src/components/ui/learn-more-link.tsx new file mode 100644 index 000000000..df5c47b2c --- /dev/null +++ b/src/components/ui/learn-more-link.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { ExternalLink } from "lucide-react"; + +import { learnLinkForMetric } from "@/lib/learn-links"; +import { useTranslations } from "@/lib/i18n/context"; +import { cn } from "@/lib/utils"; + +/** + * v1.21.0 — discreet "Learn more" pointer to a public /learn guide. + * + * One small, calm anchor (no new colour, no card chrome) that links a concept + * out to its plain-language guide. Fail-closed by construction: + * + * - the href is resolved through `learnLinkForMetric` — a closed-set, + * registry-backed URL, never user/model content — so there is no XSS + * surface and no invented link; + * - an unmapped concept resolves to `null` and the component renders + * nothing, so a surface that wires a concept with no guide simply shows + * no pointer rather than a broken one. + * + * It is a literal `` (no markdown library): `target="_blank"` with + * `rel="noopener noreferrer"`, the guide title as the accessible label. + */ +export interface LearnMoreLinkProps { + /** + * A `LearnConcept` key (or any metric id) to resolve. Anything unmapped + * renders nothing. + */ + concept: string; + className?: string; +} + +export function LearnMoreLink({ concept, className }: LearnMoreLinkProps) { + const { t } = useTranslations(); + const guide = learnLinkForMetric(concept); + if (guide == null) return null; + + const label = t("common.learnMore"); + + return ( + + {label} + + ); +}