mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
change things
This commit is contained in:
@@ -3,12 +3,13 @@ import { useNavigate, useParams, useLocation } from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ChevronLeft, ChevronRight, Check, FileText, ListChecks } from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight, Check, FileText, ListChecks, Link2, Plus } from "lucide-react";
|
||||
|
||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
import api from "@/utils/api.util";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -16,6 +17,7 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard";
|
||||
import DraftRequirementsEditor from "@/modules/admin/components/courses/DraftRequirementsEditor";
|
||||
import AttachUnitsDialog from "@/modules/admin/components/library/AttachUnitsDialog";
|
||||
|
||||
const schema = z.object({
|
||||
title: z.string().min(1, "Title is required."),
|
||||
@@ -42,8 +44,10 @@ export default function AddUnit() {
|
||||
|
||||
const backTarget = location.state?.returnTo ?? `/admin/courses/${courseId}/units`;
|
||||
|
||||
const [mode, setMode] = useState(null); // null | "create" — the Details/Requirements form only shows once "Create" is chosen
|
||||
const [step, setStep] = useState(0);
|
||||
const [requirements, setRequirements] = useState([]);
|
||||
const [attachOpen, setAttachOpen] = useState(false);
|
||||
|
||||
const { register, trigger, getValues, formState: { errors, isDirty } } = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -89,7 +93,7 @@ export default function AddUnit() {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => (step === 0 ? navigate(backTarget) : setStep(0))}
|
||||
onClick={() => (mode === "create" && step > 0 ? setStep(0) : navigate(backTarget))}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -101,105 +105,136 @@ export default function AddUnit() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stepper */}
|
||||
<div className="flex items-center gap-0">
|
||||
{STEPS.map((s, i) => {
|
||||
const Icon = s.icon;
|
||||
const isActive = step === i;
|
||||
const isDone = step > i;
|
||||
{/* Select existing vs. create new */}
|
||||
<div className="flex items-start justify-between gap-3 pb-3 border-b border-border">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Attach an existing library unit to reuse its content, or create a new one from scratch.
|
||||
</p>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setAttachOpen(true)}>
|
||||
<Link2 className="h-3.5 w-3.5 mr-1.5" /> Select
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setMode("create")}>
|
||||
<Plus className="h-3.5 w-3.5 mr-1.5" /> Create
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div key={s.id} className="flex items-center flex-1 last:flex-none">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className={cn(
|
||||
"h-8 w-8 rounded-full flex items-center justify-center border transition-colors",
|
||||
isDone && "bg-emerald-600 border-emerald-600 text-white",
|
||||
isActive && "border-primary bg-primary text-primary-foreground",
|
||||
!isActive && !isDone && "border-border bg-background text-muted-foreground"
|
||||
)}>
|
||||
{isDone ? <Check className="h-4 w-4" /> : <Icon className="h-4 w-4" />}
|
||||
{mode === "create" && (
|
||||
<>
|
||||
{/* Stepper */}
|
||||
<div className="flex items-center gap-0">
|
||||
{STEPS.map((s, i) => {
|
||||
const Icon = s.icon;
|
||||
const isActive = step === i;
|
||||
const isDone = step > i;
|
||||
|
||||
return (
|
||||
<div key={s.id} className="flex items-center flex-1 last:flex-none">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className={cn(
|
||||
"h-8 w-8 rounded-full flex items-center justify-center border transition-colors",
|
||||
isDone && "bg-emerald-600 border-emerald-600 text-white",
|
||||
isActive && "border-primary bg-primary text-primary-foreground",
|
||||
!isActive && !isDone && "border-border bg-background text-muted-foreground"
|
||||
)}>
|
||||
{isDone ? <Check className="h-4 w-4" /> : <Icon className="h-4 w-4" />}
|
||||
</div>
|
||||
<span className={cn(
|
||||
"text-[11px] font-medium whitespace-nowrap hidden sm:block",
|
||||
isActive ? "text-foreground" : "text-muted-foreground",
|
||||
isDone ? "text-emerald-600" : ""
|
||||
)}>
|
||||
{s.label}
|
||||
</span>
|
||||
</div>
|
||||
{i < STEPS.length - 1 && (
|
||||
<div className={cn(
|
||||
"flex-1 h-px mx-2 mb-4 transition-colors",
|
||||
step > i ? "bg-emerald-600" : "bg-border"
|
||||
)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-5">
|
||||
{step === 0 && (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="title">Title <span className="text-destructive">*</span></Label>
|
||||
<Input id="title" placeholder="Unit title" {...register("title")} />
|
||||
<FieldError message={errors.title?.message} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea id="description" placeholder="Optional description" rows={3} {...register("description")} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-w-[120px]">
|
||||
<Label htmlFor="order">Order</Label>
|
||||
<Input id="order" type="number" min={0} {...register("order")} />
|
||||
</div>
|
||||
<span className={cn(
|
||||
"text-[11px] font-medium whitespace-nowrap hidden sm:block",
|
||||
isActive ? "text-foreground" : "text-muted-foreground",
|
||||
isDone ? "text-emerald-600" : ""
|
||||
)}>
|
||||
{s.label}
|
||||
</span>
|
||||
</div>
|
||||
{i < STEPS.length - 1 && (
|
||||
<div className={cn(
|
||||
"flex-1 h-px mx-2 mb-4 transition-colors",
|
||||
step > i ? "bg-emerald-600" : "bg-border"
|
||||
)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step content */}
|
||||
<div className="rounded-lg border bg-card p-6 space-y-5">
|
||||
{step === 0 && (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="title">Title <span className="text-destructive">*</span></Label>
|
||||
<Input id="title" placeholder="Unit title" {...register("title")} />
|
||||
<FieldError message={errors.title?.message} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea id="description" placeholder="Optional description" rows={3} {...register("description")} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-w-[120px]">
|
||||
<Label htmlFor="order">Order</Label>
|
||||
<Input id="order" type="number" min={0} {...register("order")} />
|
||||
</div>
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure how learners complete this unit — optional, sensible defaults apply automatically. This is created together with the rest of the unit when you finish.
|
||||
</p>
|
||||
<DraftRequirementsEditor entityType="unit" items={requirements} onChange={setRequirements} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure how learners complete this unit — optional, sensible defaults apply automatically. This is created together with the rest of the unit when you finish.
|
||||
</p>
|
||||
<DraftRequirementsEditor entityType="unit" items={requirements} onChange={setRequirements} />
|
||||
<div className="flex justify-between gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => (step === 0 ? navigate(backTarget) : setStep(0))}
|
||||
disabled={loading}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||
{step === 0 ? "Cancel" : "Back"}
|
||||
</Button>
|
||||
|
||||
{step === 0 ? (
|
||||
<Button type="button" onClick={handleNext}>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
) : (
|
||||
// Only the true final step (Requirements) actually persists
|
||||
// anything — the unit and any draft requirements are created
|
||||
// together in one shot here.
|
||||
<Button type="button" onClick={handleCreate} disabled={loading}>
|
||||
{loading && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Create Unit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => (step === 0 ? navigate(backTarget) : setStep(0))}
|
||||
disabled={loading}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||
{step === 0 ? "Cancel" : "Back"}
|
||||
</Button>
|
||||
|
||||
{step === 0 ? (
|
||||
<Button type="button" onClick={handleNext}>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4 ml-1" />
|
||||
</Button>
|
||||
) : (
|
||||
// Only the true final step (Requirements) actually persists
|
||||
// anything — the unit and any draft requirements are created
|
||||
// together in one shot here.
|
||||
<Button type="button" onClick={handleCreate} disabled={loading}>
|
||||
{loading && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Create Unit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{unsavedChangesDialog}
|
||||
|
||||
<AttachUnitsDialog
|
||||
open={attachOpen}
|
||||
onOpenChange={setAttachOpen}
|
||||
attachedUnitIds={course?.units?.map((u) => u.unit_id) ?? []}
|
||||
onAttach={async (unitIds) => {
|
||||
await api.post(`/admin/courses/${courseId}/units/attach`, { unit_ids: unitIds });
|
||||
bypassOnce();
|
||||
navigate(backTarget);
|
||||
}}
|
||||
loading={loading}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user