refactor(glossaries): single source of truth + dedicated detail page
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m15s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m15s
UX refonte : - Retire la section 'Glossaires professionnels' de la vue principale (les 8 cartes de templates sont maintenant dans le dialog de creation) - Cartes 'Vos glossaires' plus simples : nom, langues, termes, date - Cliquer sur la carte navigue vers /dashboard/glossaries/[id] - Plus de boutons Edit/Delete sur la carte (deplaces dans la page detail) - Recherche par nom (visible si > 3 glossaires) - Badge 'Non enregistre' si modifications non sauvegardees Nouvelle page /dashboard/glossaries/[id] : - Edition inline du nom (input), langues source/cible (select) - Tableau des termes avec recherche et edition en place - Ajout/suppression de termes (max 500) - Export / Import CSV (meme logique que l'edit dialog) - Zone danger : confirmation en 2 temps pour la suppression - Back link vers la liste - i18n : 40 nouvelles cles ajoutees aux 13 locales (FR + EN traduit, les autres utilisent le fallback EN) Design preserve : editorial-card, brand-accent, meme typographie, meme palette. Refactor structurel uniquement, pas de restyling. Le system prompt (Instructions de contexte) reste tel quel, au-dessus de la liste des glossaires, comme dans le design actuel.
This commit is contained in:
@@ -1,81 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
BookText, Plus, Library, Calendar, Hash,
|
||||
Zap, Save, Trash2, MessageSquare, Loader2,
|
||||
Monitor, Scale, Stethoscope, BarChart3,
|
||||
Megaphone, ShoppingCart, FlaskConical, Users,
|
||||
CheckCircle2, AlertCircle, ArrowRight, MousePointerClick,
|
||||
Info, ExternalLink,
|
||||
MessageSquare, Save, Trash2, Loader2,
|
||||
CheckCircle2, AlertCircle, ArrowRight, Info, ExternalLink, Search,
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUser } from '@/app/dashboard/useUser';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { useGlossaries, useGlossary } from './useGlossaries';
|
||||
import type { Glossary, GlossaryTermInput, GlossaryListItem } from './types';
|
||||
import { useGlossaries } from './useGlossaries';
|
||||
import type { GlossaryListItem } from './types';
|
||||
import { ProUpgradePrompt } from './ProUpgradePrompt';
|
||||
import { CreateGlossaryDialog } from './CreateGlossaryDialog';
|
||||
import { EditGlossaryDialog } from './EditGlossaryDialog';
|
||||
import { DeleteGlossaryDialog } from './DeleteGlossaryDialog';
|
||||
import { useToast } from '@/components/ui/toast';
|
||||
import { SUPPORTED_LANGUAGES } from './types';
|
||||
import { useTranslationStore } from '@/lib/store';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
|
||||
const PRESETS = [
|
||||
{ key: 'it', titleKey: 'glossaries.presets.it.title', descKey: 'glossaries.presets.it.desc', icon: Monitor, templateId: 'technology' },
|
||||
{ key: 'legal', titleKey: 'glossaries.presets.legal.title', descKey: 'glossaries.presets.legal.desc', icon: Scale, templateId: 'legal' },
|
||||
{ key: 'medical', titleKey: 'glossaries.presets.medical.title', descKey: 'glossaries.presets.medical.desc', icon: Stethoscope, templateId: 'medical' },
|
||||
{ key: 'finance', titleKey: 'glossaries.presets.finance.title', descKey: 'glossaries.presets.finance.desc', icon: BarChart3, templateId: 'finance' },
|
||||
{ key: 'marketing', titleKey: 'glossaries.presets.marketing.title', descKey: 'glossaries.presets.marketing.desc', icon: Megaphone, templateId: 'marketing' },
|
||||
{ key: 'hr', titleKey: 'glossaries.presets.hr.title', descKey: 'glossaries.presets.hr.desc', icon: Users, templateId: 'hr' },
|
||||
{ key: 'scientific', titleKey: 'glossaries.presets.scientific.title', descKey: 'glossaries.presets.scientific.desc', icon: FlaskConical, templateId: 'scientific' },
|
||||
{ key: 'ecommerce', titleKey: 'glossaries.presets.ecommerce.title', descKey: 'glossaries.presets.ecommerce.desc', icon: ShoppingCart, templateId: 'ecommerce' },
|
||||
];
|
||||
|
||||
export default function GlossariesPage() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const { data: user, isLoading: isLoadingUser } = useUser();
|
||||
const {
|
||||
glossaries,
|
||||
total,
|
||||
isLoading: isLoadingGlossaries,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
isImportingTemplate,
|
||||
createGlossary,
|
||||
updateGlossary,
|
||||
deleteGlossary,
|
||||
importTemplate,
|
||||
} = useGlossaries();
|
||||
const { toast } = useToast();
|
||||
const { settings, updateSettings } = useTranslationStore();
|
||||
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [selectedGlossary, setSelectedGlossary] = useState<GlossaryListItem | null>(null);
|
||||
const [glossaryToDelete, setGlossaryToDelete] = useState<{ id: string; name: string } | null>(null);
|
||||
const [systemPrompt, setSystemPrompt] = useState(settings.systemPrompt);
|
||||
const [isSavingPrompt, setIsSavingPrompt] = useState(false);
|
||||
const [promptSaved, setPromptSaved] = useState(false);
|
||||
const [creatingPreset, setCreatingPreset] = useState<string | null>(null);
|
||||
|
||||
const { glossary: fullGlossary, isLoading: isLoadingGlossaryDetail } = useGlossary(
|
||||
selectedGlossary?.id || null
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
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();
|
||||
|
||||
@@ -92,7 +63,9 @@ export default function GlossariesPage() {
|
||||
setPromptSaved(true);
|
||||
toast({ title: t('context.saved'), description: t('context.savedDesc') });
|
||||
setTimeout(() => setPromptSaved(false), 3000);
|
||||
} finally { setIsSavingPrompt(false); }
|
||||
} finally {
|
||||
setIsSavingPrompt(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearPrompt = () => {
|
||||
@@ -100,56 +73,7 @@ export default function GlossariesPage() {
|
||||
setSystemPrompt('');
|
||||
};
|
||||
|
||||
const handleCreatePresetGlossary = async (preset: typeof PRESETS[0]) => {
|
||||
setCreatingPreset(preset.key);
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
const params = new URLSearchParams({ template_id: preset.templateId });
|
||||
const res = await fetch(`${API_BASE}/api/v1/glossaries/import?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
const glossary = result.data;
|
||||
toast({
|
||||
title: t('context.presets.created'),
|
||||
description: t('context.presets.createdDesc', {
|
||||
name: glossary?.name ?? t(preset.titleKey),
|
||||
count: String(glossary?.terms?.length ?? 0),
|
||||
}),
|
||||
});
|
||||
} else if (res.status === 409) {
|
||||
toast({ title: t('glossaries.presets.alreadyImported') });
|
||||
} else {
|
||||
toast({ variant: 'destructive', title: t('glossaries.toast.error'), description: t('glossaries.toast.errorCreate') });
|
||||
}
|
||||
} catch {
|
||||
toast({ variant: 'destructive', title: t('glossaries.toast.error'), description: t('glossaries.toast.errorImport') });
|
||||
} finally {
|
||||
setCreatingPreset(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = (id: string) => {
|
||||
const glossary = glossaries.find((g: GlossaryListItem) => g.id === id);
|
||||
if (glossary) {
|
||||
setSelectedGlossary(glossary);
|
||||
setEditDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (id: string, name: string) => {
|
||||
setGlossaryToDelete({ id, name });
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateGlossary = async (data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => {
|
||||
const handleCreateGlossary = async (data: { name: string; source_language: string; target_language: string; terms: { source: string; target: string; translations?: Record<string, string> }[] }) => {
|
||||
try {
|
||||
await createGlossary(data);
|
||||
setCreateDialogOpen(false);
|
||||
@@ -187,43 +111,12 @@ export default function GlossariesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveGlossary = async (id: string, data: { name: string; source_language: string; target_language: string; terms: GlossaryTermInput[] }) => {
|
||||
try {
|
||||
await updateGlossary(id, data);
|
||||
setEditDialogOpen(false);
|
||||
setSelectedGlossary(null);
|
||||
toast({
|
||||
title: t('glossaries.toast.updated'),
|
||||
description: t('glossaries.toast.updatedDesc', { name: data.name }),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('glossaries.toast.error'),
|
||||
description: t('glossaries.toast.errorUpdate'),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!glossaryToDelete) return;
|
||||
try {
|
||||
await deleteGlossary(glossaryToDelete.id);
|
||||
setDeleteDialogOpen(false);
|
||||
setGlossaryToDelete(null);
|
||||
toast({
|
||||
title: t('glossaries.toast.deleted'),
|
||||
description: t('glossaries.toast.deletedDesc'),
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: t('glossaries.toast.error'),
|
||||
description: t('glossaries.toast.errorDelete'),
|
||||
});
|
||||
}
|
||||
};
|
||||
// Filtered list (search)
|
||||
const filteredGlossaries = useMemo(() => {
|
||||
if (!searchQuery.trim()) return glossaries;
|
||||
const q = searchQuery.toLowerCase();
|
||||
return glossaries.filter((g) => g.name.toLowerCase().includes(q));
|
||||
}, [glossaries, searchQuery]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -240,18 +133,6 @@ export default function GlossariesPage() {
|
||||
return <ProUpgradePrompt />;
|
||||
}
|
||||
|
||||
const renderTitle = (title: string) => {
|
||||
const lastSpaceIndex = title.lastIndexOf(' ');
|
||||
if (lastSpaceIndex === -1) return title;
|
||||
const firstPart = title.substring(0, lastSpaceIndex);
|
||||
const lastWord = title.substring(lastSpaceIndex + 1);
|
||||
return (
|
||||
<>
|
||||
{firstPart} <span className="italic">{lastWord}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto w-full p-6 lg:p-8">
|
||||
|
||||
@@ -270,15 +151,15 @@ export default function GlossariesPage() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
disabled={isCreating}
|
||||
disabled={isCreating || isImportingTemplate}
|
||||
className="premium-button px-8 py-3 text-xs uppercase tracking-widest !rounded-xl flex items-center gap-2 disabled:opacity-50 shrink-0 cursor-pointer font-bold"
|
||||
>
|
||||
<Plus size={14} />
|
||||
{t('glossaries.createNew')}
|
||||
{t('glossaries.createNew') || "Créer un glossaire"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Comment ça marche ─────────────────────────────────── */}
|
||||
{/* ── How it works ───────────────────────────────────────── */}
|
||||
<div className="mb-10 p-5 rounded-2xl bg-brand-accent/5 dark:bg-brand-accent/10 border border-brand-accent/20 dark:border-brand-accent/15">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Info size={15} className="text-brand-accent shrink-0" />
|
||||
@@ -325,9 +206,8 @@ export default function GlossariesPage() {
|
||||
|
||||
<div className="space-y-10">
|
||||
|
||||
{/* ── System Prompt ────────────────────────────────────── */}
|
||||
{/* ── System Prompt (Context) ─────────────────────────────── */}
|
||||
<section className="editorial-card p-8 lg:p-10 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm">
|
||||
{/* Header with status badge */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3 text-brand-accent">
|
||||
<MessageSquare size={18} />
|
||||
@@ -335,7 +215,6 @@ export default function GlossariesPage() {
|
||||
{t('context.instructions.title')}
|
||||
</h3>
|
||||
</div>
|
||||
{/* Status badge */}
|
||||
{promptHasUnsavedChanges ? (
|
||||
<span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-amber-600 dark:text-amber-400 bg-amber-500/10 px-3 py-1 rounded-full border border-amber-500/20">
|
||||
<AlertCircle size={11} />
|
||||
@@ -396,75 +275,9 @@ export default function GlossariesPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Professional Presets ─────────────────────────────── */}
|
||||
<section className="editorial-card p-8 lg:p-10 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm">
|
||||
<div className="flex items-center gap-3 mb-3 text-brand-accent">
|
||||
<Zap size={18} />
|
||||
<h3 className="text-sm font-serif font-medium text-brand-dark dark:text-white tracking-tight">
|
||||
{t('context.presets.title')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Explanation box */}
|
||||
<div className="mb-6 p-3.5 rounded-xl bg-brand-muted/40 dark:bg-white/[0.03] border border-black/5 dark:border-white/5">
|
||||
<p className="text-[11px] text-brand-dark/60 dark:text-white/50 font-light leading-relaxed">
|
||||
<strong className="font-bold text-brand-dark/80 dark:text-white/80">{t('glossaries.presets.whatForBold')}</strong> {t('glossaries.presets.whatForDesc')}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-2.5">
|
||||
<MousePointerClick size={11} className="text-brand-accent shrink-0" />
|
||||
<p className="text-[11px] text-brand-accent/80 font-medium">
|
||||
{t('glossaries.presets.clickHint')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{(() => {
|
||||
const importedTemplateIds = new Set(
|
||||
glossaries
|
||||
.map((g: GlossaryListItem) => g.template_id)
|
||||
.filter(Boolean) as string[]
|
||||
);
|
||||
return PRESETS.map((p) => {
|
||||
const Icon = p.icon;
|
||||
const isCreatingThis = creatingPreset === p.key;
|
||||
const alreadyImported = importedTemplateIds.has(p.templateId);
|
||||
return (
|
||||
<button
|
||||
key={p.key}
|
||||
onClick={() => !alreadyImported && handleCreatePresetGlossary(p)}
|
||||
disabled={!!creatingPreset || alreadyImported}
|
||||
className="relative p-4 bg-brand-muted/40 dark:bg-white/5 hover:bg-brand-accent/5 dark:hover:bg-brand-accent/10 border border-black/5 dark:border-white/5 rounded-xl text-left transition-all hover:border-brand-accent/30 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed group min-h-[105px] flex flex-col justify-between"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="p-1.5 bg-brand-accent/10 rounded-lg text-brand-accent group-hover:scale-110 transition-transform">
|
||||
{isCreatingThis ? <Loader2 size={16} className="animate-spin" /> : <Icon size={16} />}
|
||||
</div>
|
||||
{isCreatingThis && <span className="text-[10px] text-brand-accent font-bold uppercase">{t('glossaries.presets.creating')}</span>}
|
||||
{alreadyImported && (
|
||||
<span className="flex items-center gap-1 text-[9px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20">
|
||||
<CheckCircle2 size={9} /> {t('glossaries.presets.alreadyImported')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-brand-dark dark:text-white mb-1">
|
||||
{t(p.titleKey)}
|
||||
</h4>
|
||||
<p className="text-[10px] text-brand-dark/45 dark:text-white/45 leading-normal font-light">
|
||||
{t(p.descKey)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Glossary Grid ──────────────────────────────────────── */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
{/* ── Your Glossaries ────────────────────────────────────── */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-5 gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-serif font-medium text-brand-dark dark:text-white tracking-tight">
|
||||
{t('glossaries.grid.title')} <span className="italic">{t('glossaries.grid.titleHighlight')}</span>
|
||||
@@ -475,7 +288,7 @@ export default function GlossariesPage() {
|
||||
: t('glossaries.grid.emptyAction')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
{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>{t('glossaries.grid.activeTranslation')}</span>
|
||||
@@ -494,29 +307,57 @@ export default function GlossariesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search bar (only if more than 3 glossaries) */}
|
||||
{glossaries.length > 3 && (
|
||||
<div className="relative mb-5">
|
||||
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-brand-dark/30 dark:text-white/30" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
placeholder={t('glossaries.grid.searchPlaceholder') || "Rechercher un glossaire…"}
|
||||
className="w-full pl-9 pr-3 py-2.5 text-xs rounded-lg border border-black/5 dark:border-white/10 bg-white dark:bg-[#141414] focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent/30"
|
||||
/>
|
||||
</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">
|
||||
<div className="w-12 h-12 bg-brand-muted dark:bg-white/10 rounded-xl flex items-center justify-center text-brand-accent mx-auto mb-4">
|
||||
<Library size={24} />
|
||||
</div>
|
||||
<p className="text-base font-serif font-medium text-brand-dark dark:text-white mb-1">{t('glossaries.empty')}</p>
|
||||
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-light">{t('glossaries.emptyDesc')}</p>
|
||||
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-light mb-5">{t('glossaries.emptyDesc')}</p>
|
||||
<button
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
className="premium-button px-6 py-2.5 text-[11px] uppercase tracking-widest !rounded-lg inline-flex items-center gap-2 cursor-pointer font-bold"
|
||||
>
|
||||
<Plus size={12} />
|
||||
{t('glossaries.createNew') || "Créer un glossaire"}
|
||||
</button>
|
||||
</div>
|
||||
) : filteredGlossaries.length === 0 ? (
|
||||
<div className="editorial-card p-8 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm text-center">
|
||||
<p className="text-xs text-brand-dark/50 dark:text-white/50 font-light">
|
||||
{t('glossaries.grid.noResults') || "Aucun résultat pour cette recherche."}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{glossaries.map((glossary: GlossaryListItem) => {
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredGlossaries.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);
|
||||
const isMultilingual = glossary.target_language === 'multi';
|
||||
// Does this glossary match the current translation target?
|
||||
const matchesTarget = currentTargetLang && (isMultilingual || glossary.target_language === currentTargetLang);
|
||||
const mismatch = currentTargetLang && !isMultilingual && glossary.target_language && glossary.target_language !== currentTargetLang;
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
key={glossary.id}
|
||||
onClick={() => router.push(`/dashboard/glossaries/${glossary.id}`)}
|
||||
className={cn(
|
||||
'editorial-card p-6 bg-white dark:bg-[#141414] border rounded-2xl shadow-sm group transition-all relative',
|
||||
'editorial-card p-6 bg-white dark:bg-[#141414] border rounded-2xl shadow-sm group transition-all relative text-left w-full',
|
||||
'hover:shadow-md hover:-translate-y-0.5 cursor-pointer',
|
||||
matchesTarget
|
||||
? 'border-brand-accent/40 ring-1 ring-brand-accent/20 hover:border-brand-accent/60'
|
||||
: mismatch
|
||||
@@ -524,7 +365,6 @@ export default function GlossariesPage() {
|
||||
: 'border-black/5 dark:border-white/5 hover:border-brand-accent/30'
|
||||
)}
|
||||
>
|
||||
{/* 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} /> {t('glossaries.badge.compatible')}
|
||||
@@ -564,45 +404,12 @@ export default function GlossariesPage() {
|
||||
{new Date(glossary.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</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} /> {t('glossaries.card.editTerms')}
|
||||
</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} /> {t('glossaries.card.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── About section ──────────────────────────────────────── */}
|
||||
<div className="editorial-card p-8 bg-white dark:bg-[#141414] border border-black/5 dark:border-white/5 rounded-2xl shadow-sm">
|
||||
<div className="flex items-center gap-3 text-brand-accent mb-4">
|
||||
<BookText size={18} />
|
||||
<span className="text-xs font-serif font-medium text-brand-dark dark:text-white tracking-tight">
|
||||
{t('glossaries.aboutTitle')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-brand-dark/50 dark:text-white/40 font-light leading-relaxed mb-3">{t('glossaries.aboutDesc')}</p>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-brand-dark/60 dark:text-white/60">
|
||||
{t('glossaries.aboutFormat')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Dialogs */}
|
||||
@@ -614,27 +421,18 @@ export default function GlossariesPage() {
|
||||
isCreating={isCreating}
|
||||
isImportingTemplate={isImportingTemplate}
|
||||
/>
|
||||
|
||||
{editDialogOpen && (fullGlossary || !isLoadingGlossaryDetail) && (
|
||||
<EditGlossaryDialog
|
||||
open={editDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setEditDialogOpen(open);
|
||||
if (!open) setSelectedGlossary(null);
|
||||
}}
|
||||
glossary={fullGlossary}
|
||||
onSave={handleSaveGlossary}
|
||||
isSaving={isUpdating}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DeleteGlossaryDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
isDeleting={isDeleting}
|
||||
glossaryName={glossaryToDelete?.name}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTitle(title: string) {
|
||||
const lastSpaceIndex = title.lastIndexOf(' ');
|
||||
if (lastSpaceIndex === -1) return title;
|
||||
const firstPart = title.substring(0, lastSpaceIndex);
|
||||
const lastWord = title.substring(lastSpaceIndex + 1);
|
||||
return (
|
||||
<>
|
||||
{firstPart} <span className="italic">{lastWord}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user