Exam generation rework, batch user tables, fastapi endpoint switch

This commit is contained in:
Carlos-Mesquita
2024-11-04 23:29:14 +00:00
parent a2bc997e8f
commit 15c9c4d4bd
148 changed files with 11348 additions and 3901 deletions

View File

@@ -0,0 +1,246 @@
import React, { useCallback, useMemo, useReducer, useEffect, ReactNode } from "react";
import {
DndContext,
DragEndEvent,
DragStartEvent,
MeasuringStrategy,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
restrictToWindowEdges,
snapCenterToCursor,
} from "@dnd-kit/modifiers";
import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea";
import Header from "../../Shared/Header";
import Alert, { AlertItem } from "../Shared/Alert";
import clsx from "clsx";
import { Card, CardContent } from "@/components/ui/card";
import { Blank, DropZone } from "./DragNDrop";
import { getTextSegments, BlankState, BlanksState, BlanksAction } from "./FillBlanksReducer";
interface Props {
title?: string;
initialText: string;
description: string;
state: BlanksState;
module: string;
editing: boolean;
showBlankBank: boolean;
alerts: AlertItem[];
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
blanksDispatcher: React.Dispatch<BlanksAction>
onBlankSelect?: (blankId: number | null) => void;
onSave: () => void;
onDiscard: () => void;
onDelete: () => void;
children: ReactNode;
}
const BlanksEditor: React.FC<Props> = ({
title = "Fill Blanks",
initialText,
description,
state,
editing,
module,
children,
showBlankBank = true,
alerts,
blanksDispatcher,
onBlankSelect,
onSave,
onDiscard,
onDelete,
setEditing
}) => {
useEffect(() => {
const tokens = getTextSegments(initialText);
const initialBlanks = tokens.reduce((acc, token, idx) => {
if (token.type === 'blank') {
acc.push({ id: token.id, position: idx });
}
return acc;
}, [] as BlankState[]);
blanksDispatcher({ type: "SET_TEXT", payload: initialText });
blanksDispatcher({ type: "SET_BLANKS", payload: initialBlanks });
}, [initialText, blanksDispatcher]);
const tokens = useMemo(() => {
return getTextSegments(state.text || "");
}, [state.text]);
const handleDragStart = useCallback((event: DragStartEvent) => {
blanksDispatcher({ type: "SET_DRAGGED_ITEM", payload: event.active.id.toString() });
}, [blanksDispatcher]);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (!over) return;
const blankId = parseInt(active.id.toString().split("-").pop() || "");
const dropIndex = parseInt(over.id.toString().split("-")[1]);
blanksDispatcher({
type: "MOVE_BLANK",
payload: { blankId, newPosition: dropIndex },
});
blanksDispatcher({ type: "SET_DRAGGED_ITEM", payload: null });
},
[blanksDispatcher]
);
const handleTextChange = useCallback(
(newText: string) => {
const processedText = newText.replace(/\[(\d+)\]/g, "{{$1}}");
blanksDispatcher({ type: "SET_TEXT", payload: processedText });
},
[blanksDispatcher]
);
useEffect(() => {
if (onBlankSelect !== undefined) onBlankSelect(state.selectedBlankId);
}, [state.selectedBlankId, onBlankSelect]);
const handleBlankSelect = (blankId: number) => {
blanksDispatcher({
type: "SELECT_BLANK",
payload: blankId === state.selectedBlankId ? null : blankId,
});
};
const handleBlankRemove = useCallback((blankId: number) => {
blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId });
}, [blanksDispatcher]);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 4,
tolerance: 5,
},
})
);
const modifiers = [snapCenterToCursor, restrictToWindowEdges];
const measuring = {
droppable: {
strategy: MeasuringStrategy.Always,
},
};
return (
<div className="space-y-4 p-4">
<Header
title={title}
description={description}
editing={editing}
handleSave={onSave}
modeHandle={onDelete}
handleDiscard={onDiscard}
/>
{alerts.length > 0 && <Alert alerts={alerts} />}
<Card>
<CardContent className="p-4 text-white font-semibold flex gap-2">
<button
onClick={() => blanksDispatcher({ type: "ADD_BLANK" }) }
className={`px-3 py-1.5 bg-ielts-${module} rounded-md hover:bg-ielts-${module}/50 transition-colors`}
>
Add Blank
</button>
<button
onClick={() => blanksDispatcher({ type: "TOGGLE_EDIT_MODE" })}
className={clsx(
"px-3 py-1.5 rounded-md transition-colors",
`bg-ielts-${module} text-white hover:bg-ielts-${module}/50`
)}
>
{state.textMode ? "Drag Mode" : "Text Mode"}
</button>
</CardContent>
</Card>
<DndContext
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
modifiers={modifiers}
measuring={measuring}
>
<Card>
<CardContent className="p-4">
{state.textMode ? (
<AutoExpandingTextArea
value={state.text.replace(/{{(\d+)}}/g, "[$1]")}
onChange={(text) => { handleTextChange(text); if (!editing) setEditing(true) } }
className="w-full h-full min-h-[200px] p-2 bg-white border rounded-md"
placeholder="Enter text here. Use [1], [2], etc. for blanks..."
/>
) : (
<div className="leading-relaxed p-4">
{tokens.map((token, index) => {
const isWordToken = token.type === 'text' && !token.isWhitespace;
const showDropZone = isWordToken || token.type === 'blank';
return (
<React.Fragment key={index}>
{showDropZone && <DropZone index={index} module={module} />}
{token.type === 'blank' ? (
<Blank
id={token.id}
module={module}
variant="text"
isSelected={token.id === state.selectedBlankId}
isDragging={state.draggedItemId === `text-blank-${token.id}`}
/>
) : token.isLineBreak ? (
<br />
) : (
<span className="select-none">{token.content}</span>
)}
</React.Fragment>
);
})}
{tokens.length > 0 &&
tokens[tokens.length - 1].type === 'text' && (
<DropZone index={tokens.length} module={module} />
)}
</div>
)}
</CardContent>
</Card>
{(!state.textMode && showBlankBank) && (
<Card>
<CardContent className="flex flex-wrap gap-2 p-4">
{state.blanks.map(blank => (
<Blank
key={blank.id}
id={blank.id}
module={module}
variant="bank"
isSelected={blank.id === state.selectedBlankId}
isDragging={state.draggedItemId === `bank-blank-${blank.id}`}
onSelect={handleBlankSelect}
onRemove={handleBlankRemove}
disabled={state.textMode}
/>
))}
</CardContent>
</Card>
)}
{children}
</DndContext>
</div>
);
}
export default BlanksEditor;