Add full practice edit via GET/PUT .../full endpoints with question reorder and collapsible cards. Integrate bulk and scheduled SMS, email, push, and in-app notifications with a scheduled jobs page and improved recipient picker search. Co-authored-by: Cursor <cursoragent@cursor.com>
647 lines
21 KiB
TypeScript
647 lines
21 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
Link,
|
|
useNavigate,
|
|
useParams,
|
|
useSearchParams,
|
|
} from "react-router-dom";
|
|
import { ArrowLeft, Loader2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { Button } from "../../components/ui/button";
|
|
import { Stepper } from "../../components/ui/stepper";
|
|
import successIcon from "../../assets/success.svg";
|
|
import type { QuestionTypeDefinition } from "../../types/questionTypeDefinition.types";
|
|
import {
|
|
getExamPrepPracticeFull,
|
|
getLearnEnglishPracticeFull,
|
|
} from "../../api/courses.api";
|
|
import { getQuestionTypeDefinitions } from "../../api/questionTypeDefinitions.api";
|
|
import {
|
|
learnEnglishPracticeApiErrorMessage,
|
|
validateLearnEnglishQuestionsWithDefinitions,
|
|
} from "../../lib/learnEnglishPracticePublish";
|
|
import { executePracticeUpdate } from "../../lib/practiceEditOrchestrator";
|
|
import {
|
|
mapPracticeFullToFormState,
|
|
unwrapPracticeFullData,
|
|
type PreservedQuestionSetFields,
|
|
} from "../../lib/practiceFullMapper";
|
|
|
|
import { ContextStep } from "./components/practice-steps/ContextStep";
|
|
import { ScenarioStep } from "./components/practice-steps/ScenarioStep";
|
|
import { PersonaStep } from "./components/practice-steps/PersonaStep";
|
|
import { QuestionsStep } from "./components/practice-steps/QuestionsStep";
|
|
import { ReviewStep } from "./components/practice-steps/ReviewStep";
|
|
import { personaIdNumber } from "./components/practice-steps/constants";
|
|
import { useActivePersonas } from "../../hooks/useActivePersonas";
|
|
|
|
const STEP_LABELS = ["Practice", "Persona", "Questions", "Review"] as const;
|
|
|
|
export function EditPracticeFlow() {
|
|
const navigate = useNavigate();
|
|
const {
|
|
level,
|
|
programType,
|
|
courseId: routeCourseId,
|
|
unitId: routeUnitId,
|
|
moduleId: routeModuleId,
|
|
lessonId: routeLessonId,
|
|
practiceId: routePracticeId,
|
|
} = useParams<{
|
|
level?: string;
|
|
programType?: string;
|
|
courseId?: string;
|
|
unitId?: string;
|
|
moduleId?: string;
|
|
lessonId?: string;
|
|
practiceId?: string;
|
|
}>();
|
|
const [searchParams] = useSearchParams();
|
|
const backToParam = searchParams.get("backTo");
|
|
const lessonTitleRaw = searchParams.get("lessonTitle");
|
|
|
|
const practiceId = routePracticeId ? Number(routePracticeId) : NaN;
|
|
const validPracticeId = Number.isFinite(practiceId) && practiceId > 0;
|
|
|
|
const isExamPrep = Boolean(programType?.trim());
|
|
const lessonId = routeLessonId ?? searchParams.get("lessonId");
|
|
|
|
const effectiveBackTo = useMemo(() => {
|
|
if (backToParam?.trim()) return backToParam.trim();
|
|
if (routeLessonId) return "lesson";
|
|
if (isExamPrep && routeModuleId) return "module";
|
|
if (isExamPrep && routeCourseId) return "courses";
|
|
if (routeModuleId) return "module";
|
|
if (routeCourseId) return "courses";
|
|
return null;
|
|
}, [
|
|
backToParam,
|
|
routeLessonId,
|
|
isExamPrep,
|
|
routeModuleId,
|
|
routeCourseId,
|
|
]);
|
|
|
|
const courseId = isExamPrep
|
|
? routeCourseId ?? searchParams.get("courseId")
|
|
: routeCourseId ?? searchParams.get("courseId");
|
|
const moduleId = isExamPrep
|
|
? routeModuleId ?? searchParams.get("moduleId")
|
|
: routeModuleId ?? searchParams.get("moduleId");
|
|
const unitId = isExamPrep ? routeUnitId : null;
|
|
|
|
const lessonTitleDisplay = (() => {
|
|
const raw = lessonTitleRaw?.trim();
|
|
if (!raw) return null;
|
|
try {
|
|
return decodeURIComponent(raw);
|
|
} catch {
|
|
return raw;
|
|
}
|
|
})();
|
|
|
|
const isModuleContext = effectiveBackTo === "module";
|
|
const isCourseContext =
|
|
effectiveBackTo === "courses" || effectiveBackTo === "modules";
|
|
const isLessonContext = effectiveBackTo === "lesson" || Boolean(routeLessonId);
|
|
const isLessonPractice = useMemo(() => {
|
|
const lid = lessonId ? Number(lessonId) : NaN;
|
|
return Number.isFinite(lid) && lid > 0;
|
|
}, [lessonId]);
|
|
const isLearnEnglishLessonPractice = isLessonPractice && !isExamPrep;
|
|
|
|
const parentSummary = useMemo(() => {
|
|
if (lessonId)
|
|
return `Lesson #${lessonId}${lessonTitleDisplay ? ` — ${lessonTitleDisplay}` : ""}`;
|
|
if (isModuleContext && moduleId) return `Module #${moduleId}`;
|
|
if (isCourseContext && courseId) return `Course #${courseId}`;
|
|
return null;
|
|
}, [
|
|
lessonId,
|
|
lessonTitleDisplay,
|
|
isModuleContext,
|
|
isCourseContext,
|
|
moduleId,
|
|
courseId,
|
|
]);
|
|
|
|
const programLabel = isExamPrep
|
|
? programType === "skill"
|
|
? "Skill-Based Courses"
|
|
: "English Proficiency Exams"
|
|
: level
|
|
? `Program ${level}`
|
|
: null;
|
|
|
|
const backLabel =
|
|
effectiveBackTo === "lesson"
|
|
? "Back to lesson practices"
|
|
: effectiveBackTo === "module"
|
|
? "Back to Module"
|
|
: effectiveBackTo === "modules"
|
|
? "Back to Modules"
|
|
: effectiveBackTo === "courses"
|
|
? "Back to Course"
|
|
: isExamPrep
|
|
? "Back to Program"
|
|
: "Back to Courses";
|
|
|
|
const backPath = useMemo(() => {
|
|
if (isExamPrep) {
|
|
if (routeLessonId && programType && courseId && unitId && moduleId) {
|
|
const title = lessonTitleRaw
|
|
? `?lessonTitle=${encodeURIComponent(lessonTitleRaw)}`
|
|
: "";
|
|
return `/new-content/courses/${programType}/${courseId}/${unitId}/${moduleId}/lessons/${routeLessonId}/practices${title}`;
|
|
}
|
|
if (
|
|
effectiveBackTo === "module" &&
|
|
programType &&
|
|
courseId &&
|
|
unitId &&
|
|
moduleId
|
|
) {
|
|
return `/new-content/courses/${programType}/${courseId}/${unitId}/${moduleId}`;
|
|
}
|
|
if (effectiveBackTo === "courses" && programType && courseId) {
|
|
return `/new-content/courses/${programType}/${courseId}`;
|
|
}
|
|
if (programType) return `/new-content/courses/${programType}`;
|
|
return "/new-content";
|
|
}
|
|
if (routeLessonId && level && courseId && moduleId) {
|
|
const title = lessonTitleRaw
|
|
? `?lessonTitle=${encodeURIComponent(lessonTitleRaw)}`
|
|
: "";
|
|
return `/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}/lessons/${routeLessonId}/practices${title}`;
|
|
}
|
|
if (effectiveBackTo === "module" && level && courseId && moduleId) {
|
|
return `/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}`;
|
|
}
|
|
if (effectiveBackTo === "courses" && level && courseId) {
|
|
return `/new-content/learn-english/${level}/courses/${courseId}`;
|
|
}
|
|
if (level) return `/new-content/learn-english/${level}/courses`;
|
|
return "/new-content";
|
|
}, [
|
|
isExamPrep,
|
|
routeLessonId,
|
|
programType,
|
|
courseId,
|
|
unitId,
|
|
moduleId,
|
|
lessonTitleRaw,
|
|
effectiveBackTo,
|
|
level,
|
|
]);
|
|
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
const [isSaved, setIsSaved] = useState(false);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [loadingPractice, setLoadingPractice] = useState(true);
|
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
|
|
const [selectedPersona, setSelectedPersona] = useState<string | null>(null);
|
|
const [preservedQuestionSet, setPreservedQuestionSet] =
|
|
useState<PreservedQuestionSetFields>({
|
|
timeLimitMinutes: null,
|
|
passingScore: null,
|
|
introVideoUrl: "",
|
|
status: "PUBLISHED",
|
|
});
|
|
|
|
const {
|
|
personas,
|
|
loading: personasLoading,
|
|
error: personasError,
|
|
reload: reloadPersonas,
|
|
} = useActivePersonas();
|
|
|
|
const [formData, setFormData] = useState({
|
|
title: "",
|
|
description: "",
|
|
storyImageUrl: "",
|
|
shuffleQuestions: false,
|
|
tips: "",
|
|
questions: [
|
|
{
|
|
id: "q1",
|
|
displayOrder: 1,
|
|
serverQuestionId: null as number | null,
|
|
questionTypeDefinitionId: null as number | null,
|
|
text: "",
|
|
difficultyLevel: "EASY" as "EASY" | "MEDIUM" | "HARD",
|
|
points: 1,
|
|
dynamicFieldValues: {} as Record<string, string>,
|
|
mcqOptions: [
|
|
{ text: "", isCorrect: true },
|
|
{ text: "", isCorrect: false },
|
|
{ text: "", isCorrect: false },
|
|
{ text: "", isCorrect: false },
|
|
],
|
|
trueFalseCorrect: true,
|
|
shortAnswers: [""],
|
|
},
|
|
],
|
|
});
|
|
|
|
const [typeDefinitions, setTypeDefinitions] = useState<QuestionTypeDefinition[]>(
|
|
[],
|
|
);
|
|
const [definitionsLoading, setDefinitionsLoading] = useState(true);
|
|
const [definitionsError, setDefinitionsError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
setDefinitionsLoading(true);
|
|
setDefinitionsError(null);
|
|
try {
|
|
const { definitions: list } = await getQuestionTypeDefinitions({
|
|
include_system: true,
|
|
status: "ACTIVE",
|
|
});
|
|
if (!cancelled) setTypeDefinitions(list);
|
|
} catch (e) {
|
|
if (!cancelled) {
|
|
setDefinitionsError(learnEnglishPracticeApiErrorMessage(e));
|
|
setTypeDefinitions([]);
|
|
}
|
|
} finally {
|
|
if (!cancelled) setDefinitionsLoading(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!validPracticeId || definitionsLoading) return;
|
|
let cancelled = false;
|
|
(async () => {
|
|
setLoadingPractice(true);
|
|
setLoadError(null);
|
|
try {
|
|
const res = isExamPrep
|
|
? await getExamPrepPracticeFull(practiceId)
|
|
: await getLearnEnglishPracticeFull(practiceId);
|
|
const full = unwrapPracticeFullData(res);
|
|
if (!full) throw new Error("Practice details were missing from the response.");
|
|
const mapped = mapPracticeFullToFormState(full, typeDefinitions);
|
|
if (cancelled) return;
|
|
setFormData(mapped.formData);
|
|
setPreservedQuestionSet(mapped.preservedQuestionSet);
|
|
if (mapped.personaId != null) {
|
|
setSelectedPersona(String(mapped.personaId));
|
|
}
|
|
} catch (e) {
|
|
if (!cancelled) {
|
|
setLoadError(learnEnglishPracticeApiErrorMessage(e));
|
|
}
|
|
} finally {
|
|
if (!cancelled) setLoadingPractice(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [validPracticeId, practiceId, isExamPrep, typeDefinitions, definitionsLoading]);
|
|
|
|
const submitPractice = async (status: "DRAFT" | "PUBLISHED") => {
|
|
if (!validPracticeId) {
|
|
toast.error("Invalid practice", { description: "Missing practice id in the URL." });
|
|
return;
|
|
}
|
|
if (
|
|
!isLearnEnglishLessonPractice &&
|
|
(!formData.title.trim() || !formData.description.trim())
|
|
) {
|
|
toast.error("Title and story description are required", {
|
|
description: "Complete the first step before saving.",
|
|
});
|
|
return;
|
|
}
|
|
if (!selectedPersona) {
|
|
toast.error("Select a persona", {
|
|
description: "Choose a character on the Persona step before saving.",
|
|
});
|
|
return;
|
|
}
|
|
const personaId = personaIdNumber(selectedPersona);
|
|
if (!personaId) {
|
|
toast.error("Invalid persona", {
|
|
description: "Re-select a persona from the list and try again.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const mappedQuestions = formData.questions.map((q, index) => ({
|
|
questionText: String(q.text ?? "").trim(),
|
|
questionTypeDefinitionId: Number(q.questionTypeDefinitionId),
|
|
difficultyLevel: (q.difficultyLevel ?? "EASY") as "EASY" | "MEDIUM" | "HARD",
|
|
points: Number.isFinite(Number(q.points)) ? Number(q.points) : 1,
|
|
displayOrder:
|
|
Number.isFinite(Number(q.displayOrder)) && Number(q.displayOrder) > 0
|
|
? Number(q.displayOrder)
|
|
: index + 1,
|
|
serverQuestionId: q.serverQuestionId ?? null,
|
|
dynamicFieldValues: { ...(q.dynamicFieldValues ?? {}) },
|
|
mcqOptions: (q.mcqOptions ?? []).map(
|
|
(o: { text?: string; isCorrect?: boolean }) => ({
|
|
option_text: String(o.text ?? ""),
|
|
is_correct: Boolean(o.isCorrect),
|
|
}),
|
|
),
|
|
trueFalseAnswerIsTrue: q.trueFalseCorrect !== false,
|
|
shortAnswers: (q.shortAnswers ?? []).map((s: string) => String(s)),
|
|
}));
|
|
|
|
const validationMsg = validateLearnEnglishQuestionsWithDefinitions(
|
|
mappedQuestions,
|
|
typeDefinitions,
|
|
);
|
|
if (validationMsg) {
|
|
toast.error("Check your questions", { description: validationMsg });
|
|
return;
|
|
}
|
|
|
|
const lessonDefaultTitle =
|
|
lessonTitleDisplay?.trim() ||
|
|
(lessonId ? `Lesson ${lessonId} practice` : "Lesson practice");
|
|
|
|
setSubmitting(true);
|
|
try {
|
|
await executePracticeUpdate({
|
|
practiceId,
|
|
isExamPrep,
|
|
status,
|
|
formData,
|
|
personaId,
|
|
preservedQuestionSet: {
|
|
...preservedQuestionSet,
|
|
status,
|
|
},
|
|
questions: mappedQuestions,
|
|
definitions: typeDefinitions,
|
|
isLearnEnglishLessonPractice,
|
|
lessonDefaultTitle,
|
|
});
|
|
toast.success(
|
|
status === "PUBLISHED" ? "Practice updated and published" : "Practice saved as draft",
|
|
);
|
|
setIsSaved(true);
|
|
} catch (e) {
|
|
toast.error("Could not update practice", {
|
|
description: learnEnglishPracticeApiErrorMessage(e),
|
|
});
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const nextStep = () =>
|
|
setCurrentStep((prev) => Math.min(prev + 1, STEP_LABELS.length));
|
|
const prevStep = () => setCurrentStep((prev) => Math.max(prev - 1, 1));
|
|
|
|
if (!validPracticeId) {
|
|
return (
|
|
<div className="flex min-h-screen flex-col items-center justify-center px-6 text-center">
|
|
<p className="text-lg font-semibold text-grayScale-800">Invalid practice link</p>
|
|
<Button className="mt-6" variant="outline" onClick={() => navigate(-1)}>
|
|
Go back
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loadingPractice || definitionsLoading) {
|
|
return (
|
|
<div className="flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center">
|
|
<Loader2 className="h-10 w-10 animate-spin text-brand-500" />
|
|
<p className="text-sm font-medium text-grayScale-600">Loading practice details…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (loadError) {
|
|
return (
|
|
<div className="flex min-h-screen flex-col items-center justify-center px-6 text-center">
|
|
<p className="text-lg font-semibold text-grayScale-800">Could not load practice</p>
|
|
<p className="mt-2 max-w-md text-sm text-grayScale-600">{loadError}</p>
|
|
<div className="mt-6 flex gap-3">
|
|
<Button variant="outline" onClick={() => navigate(backPath)}>
|
|
{backLabel}
|
|
</Button>
|
|
<Button onClick={() => window.location.reload()}>Try again</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isSaved) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center min-h-screen px-4 text-center pb-20 animate-in fade-in zoom-in duration-500">
|
|
<div className="mb-10 relative">
|
|
<div className="absolute inset-0 bg-brand-500/10 blur-3xl rounded-full" />
|
|
<img
|
|
src={successIcon}
|
|
alt="Success"
|
|
className="h-[128px] w-[128px] relative"
|
|
/>
|
|
</div>
|
|
<h1 className="text-[28px] font-bold text-grayScale-900 mb-2">
|
|
Practice Updated Successfully!
|
|
</h1>
|
|
<p className="text-grayScale-600 text-md mb-14 max-w-lg font-medium leading-relaxed">
|
|
Your changes to this practice have been saved.
|
|
</p>
|
|
<Button
|
|
onClick={() => navigate(backPath)}
|
|
className="h-14 rounded-[6px] bg-[#9E2891] font-bold shadow-xl shadow-brand-500/20 text-[16px] text-white w-full max-w-[400px]"
|
|
>
|
|
{backLabel}
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const renderStep = () => {
|
|
const useContextStep =
|
|
isModuleContext || isCourseContext || isLessonContext;
|
|
|
|
if (useContextStep) {
|
|
switch (currentStep) {
|
|
case 1:
|
|
return (
|
|
<ContextStep
|
|
formData={formData}
|
|
setFormData={setFormData}
|
|
nextStep={nextStep}
|
|
onCancel={() => navigate(backPath)}
|
|
isLessonPractice={isLearnEnglishLessonPractice}
|
|
lessonTitle={lessonTitleDisplay}
|
|
parentSummary={parentSummary}
|
|
/>
|
|
);
|
|
case 2:
|
|
return (
|
|
<PersonaStep
|
|
personas={personas}
|
|
loading={personasLoading}
|
|
error={personasError}
|
|
onRetry={() => void reloadPersonas()}
|
|
selectedPersona={selectedPersona}
|
|
setSelectedPersona={setSelectedPersona}
|
|
nextStep={nextStep}
|
|
prevStep={prevStep}
|
|
/>
|
|
);
|
|
case 3:
|
|
return (
|
|
<QuestionsStep
|
|
formData={formData}
|
|
setFormData={setFormData}
|
|
nextStep={nextStep}
|
|
prevStep={prevStep}
|
|
typeDefinitions={typeDefinitions}
|
|
definitionsLoading={definitionsLoading}
|
|
definitionsError={definitionsError}
|
|
/>
|
|
);
|
|
case 4:
|
|
return (
|
|
<ReviewStep
|
|
formData={formData}
|
|
selectedPersona={selectedPersona}
|
|
personas={personas}
|
|
isLessonPractice={isLearnEnglishLessonPractice}
|
|
lessonTitle={lessonTitleDisplay}
|
|
programLabel={programLabel}
|
|
courseLabel={courseId ? `Course ${courseId}` : null}
|
|
moduleLabel={moduleId ? `Module ${moduleId}` : null}
|
|
prevStep={prevStep}
|
|
onEditContext={() => setCurrentStep(1)}
|
|
onEditQuestions={() => setCurrentStep(3)}
|
|
parentSummary={parentSummary}
|
|
typeDefinitions={typeDefinitions}
|
|
canPublish
|
|
submitting={submitting}
|
|
onSaveDraft={() => void submitPractice("DRAFT")}
|
|
onPublish={() => void submitPractice("PUBLISHED")}
|
|
publishLabel="Publish Updates"
|
|
publishingLabel="Publishing updates…"
|
|
/>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
switch (currentStep) {
|
|
case 1:
|
|
return (
|
|
<ScenarioStep
|
|
formData={formData}
|
|
setFormData={setFormData}
|
|
nextStep={nextStep}
|
|
cancelHref={backPath}
|
|
/>
|
|
);
|
|
case 2:
|
|
return (
|
|
<PersonaStep
|
|
personas={personas}
|
|
loading={personasLoading}
|
|
error={personasError}
|
|
onRetry={() => void reloadPersonas()}
|
|
selectedPersona={selectedPersona}
|
|
setSelectedPersona={setSelectedPersona}
|
|
nextStep={nextStep}
|
|
prevStep={prevStep}
|
|
/>
|
|
);
|
|
case 3:
|
|
return (
|
|
<QuestionsStep
|
|
formData={formData}
|
|
setFormData={setFormData}
|
|
nextStep={nextStep}
|
|
prevStep={prevStep}
|
|
typeDefinitions={typeDefinitions}
|
|
definitionsLoading={definitionsLoading}
|
|
definitionsError={definitionsError}
|
|
/>
|
|
);
|
|
case 4:
|
|
return (
|
|
<ReviewStep
|
|
formData={formData}
|
|
selectedPersona={selectedPersona}
|
|
personas={personas}
|
|
isLessonPractice={isLearnEnglishLessonPractice}
|
|
lessonTitle={lessonTitleDisplay}
|
|
programLabel={programLabel}
|
|
courseLabel={courseId ? `Course ${courseId}` : null}
|
|
moduleLabel={moduleId ? `Module ${moduleId}` : null}
|
|
prevStep={prevStep}
|
|
onEditContext={() => setCurrentStep(1)}
|
|
onEditQuestions={() => setCurrentStep(3)}
|
|
parentSummary={parentSummary}
|
|
typeDefinitions={typeDefinitions}
|
|
canPublish
|
|
submitting={submitting}
|
|
onSaveDraft={() => void submitPractice("DRAFT")}
|
|
onPublish={() => void submitPractice("PUBLISHED")}
|
|
publishLabel="Publish Updates"
|
|
publishingLabel="Publishing updates…"
|
|
/>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8 px-6 pb-16 pt-6">
|
|
<div className="mx-auto max-w-7xl w-full">
|
|
<div className="flex items-center justify-between mb-8">
|
|
<Link
|
|
to={backPath}
|
|
className="flex items-center gap-2 text-[15px] font-medium text-grayScale-600 transition-colors hover:text-brand-500 decoration-none"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
{backLabel}
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="mb-10">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-3xl font-bold text-[#0F172A]">Edit Practice</h1>
|
|
<Button
|
|
variant="outline"
|
|
className="rounded-[8px] border-grayScale-200 text-grayScale-600 h-10 px-6 font-bold bg-white hover:bg-grayScale-50"
|
|
onClick={() => navigate(backPath)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
<p className="text-grayScale-400 text-base">
|
|
Update story details, persona, and questions for practice #{practiceId}.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mx-auto w-[70%] mb-12">
|
|
<Stepper steps={[...STEP_LABELS]} currentStep={currentStep} />
|
|
</div>
|
|
|
|
<div
|
|
className={`mx-auto ${currentStep === 3 || currentStep === 4 ? "max-w-6xl" : "max-w-4xl"}`}
|
|
>
|
|
{renderStep()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|