fix: glossary cards UX - boutons clairs, badge compatible/incompatible, alerte cible dans sidebar
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m39s

This commit is contained in:
2026-05-31 13:46:13 +02:00
parent d657b65adb
commit 3a9de12f26
2 changed files with 88 additions and 46 deletions

View File

@@ -10,6 +10,7 @@ import {
Info, ExternalLink,
} from 'lucide-react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
import { useUser } from '@/app/dashboard/useUser';
import { useI18n } from '@/lib/i18n';
import { useGlossaries, useGlossary } from './useGlossaries';
@@ -70,6 +71,10 @@ export default function GlossariesPage() {
const isPro = user?.tier === 'pro';
const isLoading = isLoadingUser || isLoadingGlossaries;
// Current translation target from store
const currentTargetLang = settings.defaultTargetLanguage;
const currentTargetInfo = SUPPORTED_LANGUAGES.find(l => l.code === currentTargetLang);
// Track whether prompt has unsaved changes
const promptHasUnsavedChanges = systemPrompt !== settings.systemPrompt;
const promptIsActive = !!settings.systemPrompt?.trim();
@@ -451,19 +456,27 @@ export default function GlossariesPage() {
</h2>
<p className="text-[11px] text-brand-dark/45 dark:text-white/40 font-light mt-1">
{glossaries.length > 0
? `${glossaries.length} glossaire${glossaries.length > 1 ? 's' : ''}sélectionnez-en un dans la page Traduire pour l'activer`
? `${glossaries.length} glossaire${glossaries.length > 1 ? 's' : ''}cliquez sur une carte pour la modifier`
: 'Créez votre premier glossaire ou importez un preset ci-dessus'}
</p>
</div>
{glossaries.length > 0 && (
<Link
href="/dashboard/translate"
className="flex items-center gap-1.5 text-[11px] font-bold text-brand-accent hover:underline shrink-0"
>
<ExternalLink size={12} />
Aller à Traduire pour activer
</Link>
)}
<div className="flex items-center gap-3">
{currentTargetInfo && (
<span className="flex items-center gap-1.5 text-[10px] font-bold text-brand-dark/50 dark:text-white/40 bg-brand-muted dark:bg-white/5 border border-black/5 dark:border-white/5 px-3 py-1.5 rounded-full">
<span>Traduction active :</span>
<span>{currentTargetInfo.flag} {currentTargetInfo.label}</span>
</span>
)}
{glossaries.length > 0 && (
<Link
href="/dashboard/translate"
className="flex items-center gap-1.5 text-[11px] font-bold text-brand-accent hover:underline shrink-0"
>
<ExternalLink size={12} />
Aller à Traduire pour activer
</Link>
)}
</div>
</div>
{glossaries.length === 0 ? (
@@ -475,40 +488,57 @@ export default function GlossariesPage() {
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-light">{t('glossaries.emptyDesc')}</p>
</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{glossaries.map((glossary: GlossaryListItem) => {
const termCount = glossary.terms_count ?? 0;
const srcInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language);
const tgtInfo = SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language);
// Does this glossary match the current translation target?
const matchesTarget = currentTargetLang && glossary.target_language === currentTargetLang;
const mismatch = currentTargetLang && glossary.target_language && glossary.target_language !== currentTargetLang;
return (
<div
key={glossary.id}
className="editorial-card p-6 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm group hover:-translate-y-1 hover:border-brand-accent/30 transition-all cursor-pointer relative"
onClick={() => handleEditClick(glossary.id)}
className={cn(
'editorial-card p-6 bg-white dark:bg-[#141414] border rounded-2xl shadow-sm group transition-all relative',
matchesTarget
? 'border-brand-accent/40 ring-1 ring-brand-accent/20 hover:border-brand-accent/60'
: mismatch
? 'border-amber-300/40 dark:border-amber-500/20 hover:border-amber-400/60 opacity-75 hover:opacity-100'
: 'border-black/5 dark:border-white/5 hover:border-brand-accent/30'
)}
>
<div className="flex justify-between items-start mb-6">
<div className="w-10 h-10 bg-brand-muted dark:bg-white/10 rounded-xl flex items-center justify-center text-brand-accent group-hover:bg-brand-dark dark:group-hover:bg-white group-hover:text-white dark:group-hover:text-brand-dark transition-all">
{/* Match / mismatch badge */}
{matchesTarget && (
<div className="absolute top-3 right-3 flex items-center gap-1 bg-brand-accent/10 text-brand-accent px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider">
<CheckCircle2 size={9} /> Compatible
</div>
)}
{mismatch && (
<div className="absolute top-3 right-3 flex items-center gap-1 bg-amber-500/10 text-amber-600 dark:text-amber-400 px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider">
<AlertCircle size={9} /> Autre cible
</div>
)}
<div className="flex items-start gap-3 mb-5">
<div className="w-10 h-10 bg-brand-muted dark:bg-white/10 rounded-xl flex items-center justify-center text-brand-accent shrink-0">
<Library size={18} />
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteClick(glossary.id, glossary.name);
}}
className="text-[10px] bg-red-500/10 hover:bg-red-500 hover:text-white text-red-500 px-2.5 py-1 rounded-full font-bold uppercase tracking-wider transition-all cursor-pointer"
>
Supprimer
</button>
<div className="flex-1 min-w-0 pt-0.5">
<h3 className="text-sm font-serif font-semibold text-brand-dark dark:text-white tracking-tight leading-snug line-clamp-2">
{glossary.name}
</h3>
<p className="text-[11px] text-brand-dark/40 dark:text-white/40 font-medium flex items-center gap-1 mt-0.5">
<span>{srcInfo?.flag ?? '🌐'}</span>
<span>{srcInfo?.label ?? glossary.source_language}</span>
<span className="text-brand-accent font-bold"></span>
<span>{tgtInfo?.flag ?? '🌐'}</span>
<span>{tgtInfo?.label ?? glossary.target_language}</span>
</p>
</div>
</div>
<h3 className="text-lg font-serif font-medium text-brand-dark dark:text-white tracking-tight mb-1 truncate">
{glossary.name}
</h3>
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-medium flex items-center gap-1.5">
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.flag ?? '🌐'}</span>
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.source_language)?.label ?? glossary.source_language}</span>
<span className="text-brand-accent font-bold"></span>
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language)?.flag ?? '🌐'}</span>
<span>{SUPPORTED_LANGUAGES.find(l => l.code === glossary.target_language)?.label ?? glossary.target_language}</span>
</p>
<div className="flex justify-between items-center pt-4 mt-5 border-t border-black/5 dark:border-white/10 text-xs text-brand-dark/40 dark:text-white/40">
<div className="flex justify-between items-center pt-4 border-t border-black/5 dark:border-white/10 text-xs text-brand-dark/40 dark:text-white/40">
<span className="flex items-center gap-1">
<Hash size={12} className="text-brand-accent" />
{termCount} {t('glossaries.defineTerms')}
@@ -518,12 +548,24 @@ export default function GlossariesPage() {
{new Date(glossary.created_at).toLocaleDateString()}
</span>
</div>
{/* "How to activate" hint on hover */}
<div className="absolute inset-x-0 bottom-0 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="mx-3 mb-3 py-1.5 px-3 bg-brand-accent/10 dark:bg-brand-accent/15 rounded-lg flex items-center gap-1.5">
<MousePointerClick size={10} className="text-brand-accent shrink-0" />
<p className="text-[9px] text-brand-accent font-bold uppercase tracking-wider">Sélectionnez-le dans la page Traduire pour l'activer</p>
</div>
{/* Action buttons — always visible, unambiguous */}
<div className="flex gap-2 mt-4">
<button
onClick={() => handleEditClick(glossary.id)}
className="flex-1 py-2 px-3 rounded-lg bg-brand-muted/60 dark:bg-white/5 hover:bg-brand-accent/10 dark:hover:bg-brand-accent/15 text-brand-dark/70 dark:text-white/60 hover:text-brand-accent text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer flex items-center justify-center gap-1.5"
>
<Save size={10} /> Modifier les termes
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteClick(glossary.id, glossary.name);
}}
className="py-2 px-3 rounded-lg bg-red-500/5 hover:bg-red-500 hover:text-white text-red-500 text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer flex items-center gap-1.5"
>
<Trash2 size={10} /> Supprimer
</button>
</div>
</div>
);

View File

@@ -315,7 +315,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
Le glossaire force la traduction de termes précis. Choisissez un glossaire dont la <strong>langue source</strong> correspond à la langue d'origine de votre document.
</p>
{/* Mismatch Warning */}
{/* Mismatch Warning — source language */}
{selected && sourceLang !== 'auto' && selected.source_language !== sourceLang && (
<div className="flex items-start gap-1.5 p-2 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 text-[10px] leading-normal font-medium animate-fade-in">
<span className="shrink-0 text-amber-500">⚠️</span>
@@ -325,12 +325,12 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
</div>
)}
{/* Incompatibility Warning */}
{selected && selected.source_language === targetLang && (
{/* Mismatch Warning — target language */}
{selected && selected.target_language && selected.target_language !== targetLang && (
<div className="flex items-start gap-1.5 p-2 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400 text-[10px] leading-normal font-medium animate-fade-in">
<span className="shrink-0 text-red-500">⚠️</span>
<span className="shrink-0">🎯</span>
<span>
<strong>Incompatibilité :</strong> La langue source du glossaire est identique à la langue cible de traduction ({getFlag(targetLang)}). Les termes ne seront pas appliqués correctement.
<strong>Incompatibilité de cible :</strong> Ce glossaire est prévu pour traduire vers <strong>{getFlag(selected.target_language)} {selected.target_language.toUpperCase()}</strong>, mais votre document cible <strong>{targetFlag} {targetLang.toUpperCase()}</strong>. Les termes risquent de ne pas être pertinents.
</span>
</div>
)}