bengali-lkc / src /components /EntryCard.tsx
saifulislamoni's picture
Fix word detail navigation and compact dictionary cards
b05c3f0
Raw
History Blame Contribute Delete
9.44 kB
import Link from "next/link";
import type { LexicalEntry } from "@/lib/types";
import { entriesBySynset, senseById, synsetById } from "@/lib/lexicon";
import { REL_LABEL } from "@/lib/format";
import { WordLink } from "@/components/WordLink";
import { IdChip, LangBadge, PosBadge } from "./ui";
/**
* Dictionary entry card (SRS §8.1.2). When `showIds` is false (Visitor
* tier) internal IDs and ILI codes are hidden and relations are shown
* by target word form only.
*/
export function EntryCard({
entry,
showIds,
compact = false,
detailPage = false,
}: {
entry: LexicalEntry;
showIds: boolean;
compact?: boolean;
/** True on `/word/[id]` — hides the redundant self-link control. */
detailPage?: boolean;
}) {
const firstSynset = entry.senses[0]
? synsetById.get(entry.senses[0].synset)
: undefined;
const previewDefinition = firstSynset?.definition?.text;
const cardClassName =
"rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:shadow-md";
const header = (
<header className="flex flex-wrap items-center gap-3">
{detailPage ? (
<h1 className="font-bengali text-2xl font-bold text-slate-900">
{entry.lemma}
</h1>
) : (
<WordLink
href={`/word/${entry.id}`}
className="font-bengali text-2xl font-bold text-slate-900 hover:text-emerald-700"
>
{entry.lemma}
</WordLink>
)}
<PosBadge pos={entry.pos} />
{showIds ? <IdChip id={entry.id} /> : null}
{entry.senses.length > 1 ? (
<span className="text-xs font-medium text-slate-400">
{entry.senses.length} senses (polysemous)
</span>
) : null}
{!detailPage ? (
<span className="ml-auto text-xs font-semibold text-emerald-700 group-hover:underline">
Show Details →
</span>
) : null}
</header>
);
const senseSections = (
<div className="mt-4 space-y-4">
{entry.senses.map((sense, idx) => {
const synset = synsetById.get(sense.synset);
const synonyms = (entriesBySynset.get(sense.synset) ?? []).filter(
(e) => e.id !== entry.id
);
return (
<div
key={sense.id}
className="rounded-xl border border-slate-100 bg-slate-50/50 p-4"
>
<div className="flex flex-wrap items-center gap-2 text-xs text-slate-500">
<span className="font-semibold text-slate-600">
Sense {idx + 1}
</span>
{showIds ? (
<>
<IdChip id={sense.id} />
<span></span>
<Link href={`/synset/${sense.synset}`}>
<IdChip id={sense.synset} />
</Link>
{synset ? (
<span className="rounded bg-violet-50 px-1.5 py-0.5 font-mono text-[11px] text-violet-700 ring-1 ring-inset ring-violet-200">
ILI {synset.ili}
</span>
) : null}
</>
) : null}
</div>
{synset?.definition ? (
<p className="font-bengali mt-2 text-sm leading-relaxed text-slate-700">
<LangBadge lang={synset.definition.language} />{" "}
{synset.definition.text}
</p>
) : (
<p className="mt-2 text-xs italic text-slate-400">
No definition recorded for this concept yet.
</p>
)}
{synonyms.length > 0 ? (
<p className="mt-2 text-xs text-slate-500">
<span className="font-semibold">Synonyms:</span>{" "}
{synonyms.slice(0, 8).map((s, i) => (
<span key={s.id}>
{i > 0 ? ", " : ""}
<WordLink
href={`/word/${s.id}`}
className="font-bengali text-sm text-emerald-700 hover:underline"
>
{s.lemma}
</WordLink>
</span>
))}
{synonyms.length > 8 ? ` +${synonyms.length - 8} more` : ""}
</p>
) : null}
{!compact && sense.relations && sense.relations.length > 0 ? (
<div className="mt-3 border-t border-slate-100 pt-2">
<p className="text-[11px] font-bold uppercase tracking-wide text-slate-400">
Sense relations
</p>
<ul className="mt-1 space-y-1">
{sense.relations.map((rel, i) => {
const target = senseById.get(rel.target);
return (
<li
key={`${rel.target}-${i}`}
className="flex flex-wrap items-center gap-2 text-xs text-slate-600"
>
<span className="rounded bg-white px-1.5 py-0.5 font-medium ring-1 ring-inset ring-slate-200">
{REL_LABEL[rel.relType] ?? rel.relType}
</span>
{target ? (
<WordLink
href={`/word/${target.entry.id}`}
className="font-bengali text-sm font-semibold text-emerald-700 hover:underline"
>
{target.entry.lemma}
</WordLink>
) : (
<span className="italic text-slate-400">
external sense
</span>
)}
{showIds ? <IdChip id={rel.target} /> : null}
</li>
);
})}
</ul>
</div>
) : null}
{!compact && synset?.relations && synset.relations.length > 0 ? (
<div className="mt-3 border-t border-slate-100 pt-2">
<p className="text-[11px] font-bold uppercase tracking-wide text-slate-400">
Synset relations
</p>
<ul className="mt-1 space-y-1">
{synset.relations.map((rel, i) => {
const targetSynset = synsetById.get(rel.target);
const targetWords = entriesBySynset.get(rel.target) ?? [];
return (
<li
key={`${rel.target}-${i}`}
className="flex flex-wrap items-center gap-2 text-xs text-slate-600"
>
<span className="rounded bg-white px-1.5 py-0.5 font-medium ring-1 ring-inset ring-slate-200">
{REL_LABEL[rel.relType] ?? rel.relType}
</span>
{targetWords.length > 0 ? (
targetWords.slice(0, 4).map((w, j) => (
<WordLink
key={w.id}
href={`/word/${w.id}`}
className="font-bengali text-sm font-semibold text-emerald-700 hover:underline"
>
{w.lemma}
{j < Math.min(targetWords.length, 4) - 1 ? "," : ""}
</WordLink>
))
) : (
<span className="italic text-slate-400">
(lexical gap)
</span>
)}
{showIds ? (
<Link href={`/synset/${rel.target}`}>
<IdChip id={rel.target} />
</Link>
) : null}
{targetSynset?.definition && !compact ? (
<span className="w-full pl-1 text-[11px] italic text-slate-400">
{targetSynset.definition.text.slice(0, 90)}
{targetSynset.definition.text.length > 90 ? "…" : ""}
</span>
) : null}
</li>
);
})}
</ul>
</div>
) : null}
</div>
);
})}
</div>
);
if (compact && !detailPage) {
return (
<WordLink
href={`/word/${entry.id}`}
className={`group block ${cardClassName}`}
>
{header}
{previewDefinition ? (
<p className="font-bengali mt-3 line-clamp-2 text-sm leading-relaxed text-slate-600">
{previewDefinition}
</p>
) : (
<p className="mt-3 text-xs italic text-slate-400">
No definition recorded — open for full entry.
</p>
)}
</WordLink>
);
}
return (
<article className={cardClassName}>
{header}
{senseSections}
</article>
);
}