import React from "react"; import classnames from "classnames"; import { Link } from "react-router-dom"; import { Spinner } from "react-bootstrap"; import { HttpTimelineInfo } from "@/http/timeline"; import TimelineLogo from "../common/TimelineLogo"; import UserTimelineLogo from "../common/UserTimelineLogo"; import LoadFailReload from "../common/LoadFailReload"; import FlatButton from "../common/button/FlatButton"; import Card from "../common/Card"; interface TimelineBoardItemProps { timeline: HttpTimelineInfo; // In height. offset?: number; // In px. arbitraryOffset?: number; // If not null, will disable navigation on click. actions?: { onDelete: () => void; onMove: { start: (e: React.PointerEvent) => void; moving: (e: React.PointerEvent) => void; end: (e: React.PointerEvent) => void; }; }; } const TimelineBoardItem: React.FC = ({ timeline, arbitraryOffset, offset, actions, }) => { const { name, title } = timeline; const isPersonal = name.startsWith("@"); const url = isPersonal ? `/users/${timeline.owner.username}` : `/timelines/${name}`; const content = ( <> {isPersonal ? ( ) : ( )} {title} {name} {actions != null ? (
{ e.currentTarget.setPointerCapture(e.pointerId); actions.onMove.start(e); }} onPointerUp={(e) => { actions.onMove.end(e); try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) { void null; } }} onPointerMove={actions.onMove.moving} />
) : null} ); const offsetStyle: React.CSSProperties = { transform: arbitraryOffset != null ? `translate(0,${arbitraryOffset}px)` : offset != null ? `translate(0,${offset * 100}%)` : undefined, transition: offset != null ? "transform 0.5s" : undefined, zIndex: arbitraryOffset != null ? 1 : undefined, }; return actions == null ? ( {content} ) : (
{content}
); }; interface TimelineBoardItemContainerProps { timelines: HttpTimelineInfo[]; editHandler?: { // offset may exceed index range plusing index. onMove: (timeline: string, index: number, offset: number) => void; onDelete: (timeline: string) => void; }; } const TimelineBoardItemContainer: React.FC = ({ timelines, editHandler, }) => { const [moveState, setMoveState] = React.useState(null); return ( <> {timelines.map((timeline, index) => { const height = 48; let offset: number | undefined = undefined; let arbitraryOffset: number | undefined = undefined; if (moveState != null) { if (index === moveState.index) { arbitraryOffset = moveState.offset; } else { if (moveState.offset >= 0) { const offsetCount = Math.round(moveState.offset / height); if ( index > moveState.index && index <= moveState.index + offsetCount ) { offset = -1; } else { offset = 0; } } else { const offsetCount = Math.round(-moveState.offset / height); if ( index < moveState.index && index >= moveState.index - offsetCount ) { offset = 1; } else { offset = 0; } } } } return ( { editHandler.onDelete(timeline.name); }, onMove: { start: (e) => { if (moveState != null) return; setMoveState({ index, offset: 0, startPointY: e.clientY, }); }, moving: (e) => { if (moveState == null) return; setMoveState({ index, offset: e.clientY - moveState.startPointY, startPointY: moveState.startPointY, }); }, end: () => { if (moveState != null) { const offsetCount = Math.round( moveState.offset / height ); editHandler.onMove( timeline.name, moveState.index, offsetCount ); } setMoveState(null); }, }, } : undefined } /> ); })} ); }; interface TimelineBoardUIProps { title?: string; timelines: HttpTimelineInfo[] | "offline" | "loading"; onReload: () => void; className?: string; editHandler?: { onMove: (timeline: string, index: number, offset: number) => void; onDelete: (timeline: string) => void; }; } const TimelineBoardUI: React.FC = (props) => { const { title, timelines, className, editHandler } = props; const editable = editHandler != null; const [editing, setEditing] = React.useState(false); return (
{title != null &&

{title}

} {editable && (editing ? ( { setEditing(false); }} /> ) : ( { setEditing(true); }} /> ))}
{(() => { if (timelines === "loading") { return (
); } else if (timelines === "offline") { return (
); } else { return ( { if (index + offset >= timelines.length) { offset = timelines.length - index - 1; } else if (index + offset < 0) { offset = -index; } editHandler.onMove(timeline, index, offset); }, } : undefined } /> ); } })()}
); }; export interface TimelineBoardProps { title?: string; className?: string; load: () => Promise; editHandler?: { onMove: (timeline: string, index: number, offset: number) => Promise; onDelete: (timeline: string) => Promise; }; } const TimelineBoard: React.FC = ({ className, title, load, editHandler, }) => { const [timelines, setTimelines] = React.useState< HttpTimelineInfo[] | "offline" | "loading" >("loading"); React.useEffect(() => { let subscribe = true; if (timelines === "loading") { void load().then( (timelines) => { if (subscribe) { setTimelines(timelines); } }, () => { setTimelines("offline"); } ); } return () => { subscribe = false; }; }, [load, timelines]); return ( { setTimelines("loading"); }} editHandler={ typeof timelines === "object" && editHandler != null ? { onMove: (timeline, index, offset) => { const newTimelines = timelines.slice(); const [t] = newTimelines.splice(index, 1); newTimelines.splice(index + offset, 0, t); setTimelines(newTimelines); editHandler.onMove(timeline, index, offset).then(null, () => { setTimelines(timelines); }); }, onDelete: (timeline) => { const newTimelines = timelines.slice(); newTimelines.splice( timelines.findIndex((t) => t.name === timeline), 1 ); setTimelines(newTimelines); editHandler.onDelete(timeline).then(null, () => { setTimelines(timelines); }); }, } : undefined } /> ); }; export default TimelineBoard;