SyntaxAdi
fix: improve mode differentiation, hide edit actions in read mode, and fix settings toggle
4f16843 | 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> | |
| ); | |
| } | |