MNEMO / client /src /components /EntryList.jsx
SyntaxAdi
fix: improve mode differentiation, hide edit actions in read mode, and fix settings toggle
4f16843
Raw
History Blame Contribute Delete
6.55 kB
import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { AnswerRenderer } from "./AnswerRenderer";
function SortableEntryRow({
entry,
expanded,
readOnly,
onToggle,
onEdit,
onDelete,
onOpen,
isLast
}) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: entry._id });
const rowRef = useRef(null);
const style = {
transform: CSS.Transform.toString(transform),
transition
};
useEffect(() => {
if (expanded && rowRef.current) {
const timeoutId = setTimeout(() => {
rowRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
}, 100);
return () => clearTimeout(timeoutId);
}
}, [expanded]);
const setCombinedRef = (node) => {
setNodeRef(node);
rowRef.current = node;
};
return (
<article className={`entry-row ${expanded ? "is-expanded" : ""}`} ref={setCombinedRef} style={style}>
<div className="entry-question-row" onClick={() => onToggle(entry._id)} style={{ cursor: "pointer", display: "flex", alignItems: "flex-start", gap: "16px", flexWrap: "wrap" }}>
<div style={{ display: "flex", alignItems: "center", gap: "12px", flexShrink: 0 }}>
<button
aria-label={expanded ? "Collapse answer" : "Expand answer"}
className={`entry-chevron ${expanded ? "expanded" : ""}`}
onClick={(e) => {
e.stopPropagation();
onToggle(entry._id);
}}
type="button"
style={{
background: "transparent",
border: "none",
color: "var(--text-muted)",
display: "flex",
alignItems: "center",
justifyContent: "center",
transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
transition: "transform 0.2s ease",
width: "24px",
height: "24px"
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</button>
{!readOnly && (
<button
className="drag-handle"
{...attributes}
{...listeners}
onClick={(e) => e.stopPropagation()}
type="button"
style={{ background: "transparent", border: "none", color: "var(--text-muted)", cursor: "grab", padding: "4px" }}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>
</button>
)}
</div>
<div className="entry-question-button" style={{ flex: "1 1 200px", fontSize: "1.1rem", fontWeight: 500, minWidth: 0, wordBreak: "break-word" }}>
<AnswerRenderer answer={entry.question} />
</div>
<div className={`entry-actions ${readOnly ? "is-hidden" : ""}`} style={{ display: "flex", gap: "8px", marginLeft: "auto", paddingTop: "4px" }}>
<button
className="icon-button"
onClick={(e) => {
e.stopPropagation();
onEdit(entry);
}}
type="button"
title="Edit"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button
className="icon-button"
onClick={(e) => {
e.stopPropagation();
onDelete(entry);
}}
type="button"
title="Delete"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</div>
</div>
<div className={`entry-answer ${expanded ? "expanded" : ""}`} style={{ paddingLeft: "min(40px, 12px)" }}>
<div style={{ padding: "min(24px, 16px)", background: "var(--bg-surface)", borderRadius: "var(--radius-md)", border: "1px solid var(--border-subtle)", marginTop: "16px" }}>
<AnswerRenderer answer={entry.answer} />
{isLast && expanded && (
<div className="back-to-top-wrapper" style={{ marginTop: "24px", borderTop: "1px solid var(--border-subtle)", paddingTop: "16px", display: "flex", justifyContent: "flex-end" }}>
<button
className="button-ghost"
onClick={(e) => {
e.stopPropagation();
onToggle(entry._id);
window.scrollTo({ top: 0, behavior: "smooth" });
}}
style={{ fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.1em" }}
type="button"
>
↑ Go to top
</button>
</div>
)}
</div>
</div>
</article>
);
}
export function EntryList({ entries, mode, onEdit, onDelete, expandedIds, onToggle }) {
const navigate = useNavigate();
const readOnly = mode === "read";
const ids = useMemo(() => entries.map((entry) => entry._id), [entries]);
return (
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
<div className="entry-list">
{entries.map((entry, index) => (
<SortableEntryRow
key={entry._id}
entry={entry}
expanded={expandedIds.includes(entry._id)}
readOnly={readOnly}
onDelete={onDelete}
onEdit={onEdit}
onOpen={(item) => navigate(`/app/entry/${item._id}`)}
onToggle={onToggle}
isLast={index === entries.length - 1}
/>
))}
</div>
</SortableContext>
);
}