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
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m39s
This commit is contained in:
@@ -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,10 +456,17 @@ 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>
|
||||
<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"
|
||||
@@ -465,6 +477,7 @@ export default function GlossariesPage() {
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{glossaries.length === 0 ? (
|
||||
<div className="editorial-card p-12 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm text-center">
|
||||
@@ -478,37 +491,54 @@ export default function GlossariesPage() {
|
||||
<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>
|
||||
<h3 className="text-lg font-serif font-medium text-brand-dark dark:text-white tracking-tight mb-1 truncate">
|
||||
<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-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>
|
||||
<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>{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>
|
||||
<span>{tgtInfo?.flag ?? '🌐'}</span>
|
||||
<span>{tgtInfo?.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>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user