feat(settings,questions,school,textbooks): add brand config, question components, school dialogs, textbooks hooks

settings:

- Add actions-brand, brand-config, data-access-brand for brand management

- Add admin-file-upload-card, admin-notification-config-card, admin-school-info-card, admin-security-policy-card

- Add ai-provider-delete-dialog, ai-provider-selector, brand-config-card

- Add security-recent-logins-section, security-two-factor-section

- Add config/profile-overview-config, data-access-profile-overview, lib/system-settings-utils

questions:

- Add batch-operations, import-export-buttons, knowledge-point-selector, options-editor

- Add question-bank-results-client, question-cascade-filter, question-content-renderer, utils

school:

- Add grade-delete-dialog, grade-form-dialog, grade-list-toolbar, grade-overview-cards

- Add use-grade-data hook

textbooks:

- Add textbook-form-fields component

- Add use-kp-create, use-kp-delete, use-kp-update hooks
This commit is contained in:
SpecialX
2026-07-03 10:26:00 +08:00
parent 138b6f1b00
commit f3c223d914
77 changed files with 5397 additions and 2864 deletions

View File

@@ -97,9 +97,17 @@ function SortableChapterItem({ chapter, level, selectedId, onSelect, textbookId,
<div className="w-5 shrink-0 mr-1" />
)}
<div
className="flex-1 min-w-0 flex items-center gap-2"
<div
className="flex-1 min-w-0 flex items-center gap-2 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm"
role="button"
tabIndex={0}
onClick={() => onSelect(chapter)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
onSelect(chapter)
}
}}
>
{hasChildren ? (
<Folder className={cn("h-4 w-4 shrink-0 transition-colors", isOpen || isSelected ? "text-blue-500/80" : "text-muted-foreground/50")} />

View File

@@ -4,8 +4,7 @@ import { memo } from "react"
import { Handle, Position, type NodeProps } from "@xyflow/react"
import { useTranslations } from "next-intl"
import { cn } from "@/shared/lib/utils"
import type { GraphNodeData, MasteryLevel } from "../types"
import type { GraphLayoutNodeData } from "../graph-layout"
import type { GraphNodeData, MasteryLevel, KpWithRelations } from "../types"
import { NODE_WIDTH } from "../graph-layout"
/** 根据掌握度计算色彩等级 */
@@ -30,11 +29,23 @@ const MASTERY_BAR_COLORS: Record<MasteryLevel, string> = {
unassessed: "bg-muted",
}
function GraphKpNodeComponent({ data, selected }: NodeProps) {
/**
* 类型守卫:从 React Flow node.dataRecord<string, unknown>)安全提取图谱节点数据。
* 替代 as unknown as 双重断言,提供运行时安全。
*/
function extractNodeData(data: Record<string, unknown>): {
kp: KpWithRelations
graphData?: GraphNodeData
} {
const kp = data.kp as KpWithRelations
const graphData = data.graphData as GraphNodeData | undefined
return { kp, graphData }
}
function GraphKpNodeComponent(props: NodeProps) {
const t = useTranslations("textbooks")
const nodeData = data as unknown as GraphLayoutNodeData
const { kp } = nodeData
const graphData = (data as unknown as { graphData?: GraphNodeData }).graphData
const { data, selected } = props
const { kp, graphData } = extractNodeData(data)
const mastery = graphData?.mastery ?? null
const masteryLevel = getMasteryLevel(mastery?.masteryLevel ?? null)
const showMastery = graphData?.viewMode === "student-mastery" || graphData?.viewMode === "class-mastery"

View File

@@ -42,7 +42,7 @@ export function GraphNodeDetailPanel({
<div className="flex flex-col h-full border-l bg-background">
<div className="flex items-center justify-between p-3 border-b shrink-0">
<h3 className="text-sm font-semibold truncate">{t("graph.detail.title")}</h3>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={onClose}>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={onClose} aria-label={t("graph.detail.close")}>
<X className="h-4 w-4" />
</Button>
</div>
@@ -141,6 +141,7 @@ export function GraphNodeDetailPanel({
size="sm"
className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive"
onClick={() => onRemovePrerequisite(p.id)}
aria-label={t("graph.detail.removePrerequisite")}
>
<Trash2 className="h-3 w-3" />
</Button>

View File

@@ -37,6 +37,7 @@ import {
import { Button } from "@/shared/components/ui/button"
import type { GraphViewMode, GraphNodeData } from "../types"
import { computeGraphLayout } from "../graph-layout"
import type { GraphLayoutNodeData } from "../graph-layout"
import { useGraphData } from "../hooks/use-graph-data"
import {
createPrerequisiteAction,
@@ -202,35 +203,44 @@ function KnowledgeGraphInner({ textbookId, initialViewMode = "structure" }: Know
const handleAddPrerequisite = useCallback(async () => {
if (!selectedKpId || !newPrereqId || !textbookId) return
setIsSavingPrereq(true)
const formData = new FormData()
formData.set("knowledgePointId", selectedKpId)
formData.set("prerequisiteKpId", newPrereqId)
formData.set("textbookId", textbookId)
const result = await createPrerequisiteAction(formData)
setIsSavingPrereq(false)
if (result.success) {
toast.success(t("graph.detail.prerequisiteAdded"))
setAddPrereqOpen(false)
setNewPrereqId("")
reload()
} else {
toast.error(result.message)
try {
const formData = new FormData()
formData.set("knowledgePointId", selectedKpId)
formData.set("prerequisiteKpId", newPrereqId)
formData.set("textbookId", textbookId)
const result = await createPrerequisiteAction(formData)
if (result.success) {
toast.success(t("graph.detail.prerequisiteAdded"))
setAddPrereqOpen(false)
setNewPrereqId("")
reload()
} else {
toast.error(result.message)
}
} catch (e) {
toast.error(e instanceof Error ? e.message : t("graph.detail.prerequisiteAddFailed"))
} finally {
setIsSavingPrereq(false)
}
}, [selectedKpId, newPrereqId, textbookId, t, reload])
// 删除前置依赖
const handleRemovePrerequisite = useCallback(async (prereqId: string) => {
if (!selectedKpId || !textbookId) return
const formData = new FormData()
formData.set("knowledgePointId", selectedKpId)
formData.set("prerequisiteKpId", prereqId)
formData.set("textbookId", textbookId)
const result = await deletePrerequisiteAction(formData)
if (result.success) {
toast.success(t("graph.detail.prerequisiteRemoved"))
reload()
} else {
toast.error(result.message)
try {
const formData = new FormData()
formData.set("knowledgePointId", selectedKpId)
formData.set("prerequisiteKpId", prereqId)
formData.set("textbookId", textbookId)
const result = await deletePrerequisiteAction(formData)
if (result.success) {
toast.success(t("graph.detail.prerequisiteRemoved"))
reload()
} else {
toast.error(result.message)
}
} catch (e) {
toast.error(e instanceof Error ? e.message : t("graph.detail.prerequisiteRemoveFailed"))
}
}, [selectedKpId, textbookId, t, reload])
@@ -306,9 +316,10 @@ function KnowledgeGraphInner({ textbookId, initialViewMode = "structure" }: Know
<MiniMap
className="!bg-background !border !rounded-lg"
nodeColor={(node) => {
// node.data 是 Record<string, unknown>;从 unknown 安全转换读取 graphData
const graphData = (node.data as unknown as { graphData?: { chapterColor: string } })?.graphData
return graphData?.chapterColor ?? "#6b7280"
// 安全的类型收窄:node.data 是 Record<string, unknown>
// GraphLayoutNodeData 有索引签名 [key: string]: unknown是 Record 的子类型
const data = node.data as GraphLayoutNodeData
return data.graphData?.chapterColor ?? "#6b7280"
}}
/>
</ReactFlow>

View File

@@ -81,7 +81,8 @@ export function KnowledgePointDialogs({
<DialogTitle>{t("createTitle")}</DialogTitle>
<DialogDescription>{t("createDesc")}</DialogDescription>
</DialogHeader>
<form action={onCreateKnowledgePoint as (formData: FormData) => void}>
{/* 包装为 void 返回,避免 as 断言Promise 仍会被 React 处理 */}
<form action={(formData: FormData) => { void onCreateKnowledgePoint(formData) }}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">{t("name")}</Label>

View File

@@ -3,13 +3,15 @@
/**
* 教材模块内联 Error Boundary。
*
* 用于包裹独立数据区块(章节树、内容区、知识点区、图谱区),
* 隔离故障域,避免单点错误导致整个阅读器白屏
* 薄包装:委托给共享 SectionErrorBoundary通过自定义 fallback 渲染
* 调用方传入的 fallbackTitle/fallbackDescription/retryLabel
* 保留同名导出和 props 以兼容现有 import。
*/
import { Component, type ReactNode } from "react"
import type { ReactNode } from "react"
import { AlertCircle } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { SectionErrorBoundary } from "@/shared/components/section-error-boundary"
interface TextbookSectionErrorBoundaryProps {
children: ReactNode
@@ -21,52 +23,36 @@ interface TextbookSectionErrorBoundaryProps {
retryLabel?: string
}
interface TextbookSectionErrorBoundaryState {
hasError: boolean
}
export class TextbookSectionErrorBoundary extends Component<
TextbookSectionErrorBoundaryProps,
TextbookSectionErrorBoundaryState
> {
constructor(props: TextbookSectionErrorBoundaryProps) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(): TextbookSectionErrorBoundaryState {
return { hasError: true }
}
handleReset = (): void => {
this.setState({ hasError: false })
}
render(): ReactNode {
if (this.state.hasError) {
// 默认值为空字符串,强制调用方传入 i18n 文案
const title = this.props.fallbackTitle ?? ""
const description = this.props.fallbackDescription ?? ""
const retryLabel = this.props.retryLabel ?? ""
return (
// 任意值 min-h-[200px]:错误降级 UI 最小高度,保证视觉占位
<div className="flex h-full min-h-[200px] flex-col items-center justify-center gap-3 p-6 text-center">
<AlertCircle className="h-8 w-8 text-muted-foreground" />
{title && (
<p className="text-sm font-medium text-foreground">{title}</p>
)}
{description && (
<p className="text-xs text-muted-foreground">{description}</p>
)}
{retryLabel && (
<Button size="sm" variant="outline" onClick={this.handleReset}>
{retryLabel}
</Button>
)}
</div>
)
}
return this.props.children
}
export function TextbookSectionErrorBoundary({
children,
fallbackTitle,
fallbackDescription,
retryLabel,
}: TextbookSectionErrorBoundaryProps): ReactNode {
const fallback = (_error: Error, reset: () => void): ReactNode => (
<div
role="alert"
aria-live="assertive"
className="flex h-full min-h-[200px] flex-col items-center justify-center gap-3 p-6 text-center"
>
<AlertCircle className="h-8 w-8 text-muted-foreground" aria-hidden="true" />
{fallbackTitle && (
<p className="text-sm font-medium text-foreground">{fallbackTitle}</p>
)}
{fallbackDescription && (
<p className="text-xs text-muted-foreground">{fallbackDescription}</p>
)}
{retryLabel && (
<Button size="sm" variant="outline" onClick={reset}>
{retryLabel}
</Button>
)}
</div>
)
return (
<SectionErrorBoundary fallback={fallback}>
{children}
</SectionErrorBoundary>
)
}

View File

@@ -44,7 +44,7 @@ interface TextbookCardProps {
export function TextbookCard({ textbook, hrefBase, hideActions }: TextbookCardProps) {
const t = useTranslations("textbooks")
const router = useRouter()
const base = hrefBase || "/teacher/textbooks"
const base = hrefBase ?? "/teacher/textbooks" // 默认教师端,学生端通过 hrefBase prop 覆盖
const colorClass = getSubjectColor(textbook.subject)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)

View File

@@ -20,48 +20,67 @@ import {
} from "@/shared/components/ui/context-menu"
import { RichTextEditor } from "@/shared/components/ui/rich-text-editor"
interface TextbookContentPanelProps {
/**
* 章节数据组:当前选中章节及其预处理后的内容。
*/
export interface ChapterDataGroup {
selected: Chapter | null
processedContent: string
}
/**
* 编辑状态组:章节内容编辑相关的状态与操作。
*/
export interface EditingStateGroup {
isEditing: boolean
editContent: string
setEditContent: (content: string) => void
canEdit: boolean
highlightedKpId: string | null
onHighlight: (id: string) => void
onSwitchToKnowledgeTab: () => void
isSaving: boolean
startEditing: () => void
cancelEditing: () => void
saveContent: () => void
}
/**
* 文本选区组:右键菜单与文本选区相关的状态与操作。
*/
export interface TextSelectionGroup {
contentRef: React.RefObject<HTMLDivElement | null>
onPointerDown: (e: React.PointerEvent) => void
onContextMenuChange: (open: boolean) => void
selectedText: string
setCreateDialogOpen: (open: boolean) => void
startEditing: () => void
cancelEditing: () => void
saveContent: () => void
isSaving: boolean
processedContent: string
}
/**
* 知识点高亮组:高亮跳转相关的状态与操作。
*/
export interface KpHighlightGroup {
highlightedKpId: string | null
onHighlight: (id: string) => void
onSwitchToKnowledgeTab: () => void
}
interface TextbookContentPanelProps {
chapter: ChapterDataGroup
editing: EditingStateGroup
selection: TextSelectionGroup
highlight: KpHighlightGroup
canEdit: boolean
}
export function TextbookContentPanel({
selected,
isEditing,
editContent,
setEditContent,
chapter,
editing,
selection,
highlight,
canEdit,
highlightedKpId,
onHighlight,
onSwitchToKnowledgeTab,
contentRef,
onPointerDown,
onContextMenuChange,
selectedText,
setCreateDialogOpen,
startEditing,
cancelEditing,
saveContent,
isSaving,
processedContent,
}: TextbookContentPanelProps) {
}: TextbookContentPanelProps): React.ReactNode {
const t = useTranslations("textbooks")
const { selected, processedContent } = chapter
const { isEditing, editContent, setEditContent, isSaving, startEditing, cancelEditing, saveContent } = editing
const { contentRef, onPointerDown, onContextMenuChange, selectedText, setCreateDialogOpen } = selection
const { highlightedKpId, onHighlight, onSwitchToKnowledgeTab } = highlight
if (!selected) {
return (

View File

@@ -14,18 +14,9 @@ import {
DialogTitle,
DialogTrigger,
} from "@/shared/components/ui/dialog"
import { Input } from "@/shared/components/ui/input"
import { Label } from "@/shared/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import { createTextbookAction } from "../actions"
import { SUBJECTS, GRADES } from "../constants"
import { toast } from "sonner"
import { TextbookFormFields } from "./textbook-form-fields"
function SubmitButton() {
const { pending } = useFormStatus()
@@ -71,65 +62,7 @@ export function TextbookFormDialog() {
<DialogDescription>{t("dialog.create.description")}</DialogDescription>
</DialogHeader>
<form action={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">
{t("field.title")}
</Label>
<Input
id="title"
name="title"
placeholder={t("field.titlePlaceholder")}
className="col-span-3"
required
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="subject" className="text-right">
{t("field.subject")}
</Label>
<Select name="subject" required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder={t("field.subjectPlaceholder")} />
</SelectTrigger>
<SelectContent>
{SUBJECTS.map((s) => (
<SelectItem key={s.value} value={s.value}>
{t(`subject.${s.labelKey}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="grade" className="text-right">
{t("field.grade")}
</Label>
<Select name="grade" required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder={t("field.gradePlaceholder")} />
</SelectTrigger>
<SelectContent>
{GRADES.map((g) => (
<SelectItem key={g.value} value={g.value}>
{t(`grade.${g.labelKey}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="publisher" className="text-right">
{t("field.publisher")}
</Label>
<Input
id="publisher"
name="publisher"
placeholder={t("field.publisherPlaceholder")}
className="col-span-3"
/>
</div>
</div>
<TextbookFormFields />
<DialogFooter>
<SubmitButton />
</DialogFooter>

View File

@@ -0,0 +1,107 @@
"use client"
import { useTranslations } from "next-intl"
import { Input } from "@/shared/components/ui/input"
import { Label } from "@/shared/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import { SUBJECTS, GRADES } from "../constants"
/**
* 教材表单字段共享组件。
*
* 用于 TextbookFormDialog创建模式和 TextbookSettingsDialog编辑模式
* 消除 title/subject/grade/publisher 四个字段的 JSX 重复。
*
* - 创建模式:不传 defaultValues字段使用 placeholder
* - 编辑模式:传 defaultValues字段使用 defaultValue
*/
export interface TextbookFormFieldsProps {
/** 编辑模式下的默认值;创建模式不传 */
defaultValues?: {
title?: string | null
subject?: string | null
grade?: string | null
publisher?: string | null
}
}
export function TextbookFormFields({ defaultValues }: TextbookFormFieldsProps): React.ReactNode {
const t = useTranslations("textbooks")
const isEdit = defaultValues !== undefined
// null → undefined 转换Input/Select 的 defaultValue 不接受 null
const dv = defaultValues ?? {}
const title = dv.title ?? undefined
const subject = dv.subject ?? undefined
const grade = dv.grade ?? undefined
const publisher = dv.publisher ?? undefined
return (
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">
{t("field.title")}
</Label>
<Input
id="title"
name="title"
defaultValue={title}
placeholder={isEdit ? undefined : t("field.titlePlaceholder")}
className="col-span-3"
required
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="subject" className="text-right">
{t("field.subject")}
</Label>
<Select name="subject" defaultValue={subject} required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder={t("field.subjectPlaceholder")} />
</SelectTrigger>
<SelectContent>
{SUBJECTS.map((s) => (
<SelectItem key={s.value} value={s.value}>
{t(`subject.${s.labelKey}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="grade" className="text-right">
{t("field.grade")}
</Label>
<Select name="grade" defaultValue={grade} required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder={t("field.gradePlaceholder")} />
</SelectTrigger>
<SelectContent>
{GRADES.map((g) => (
<SelectItem key={g.value} value={g.value}>
{t(`grade.${g.labelKey}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="publisher" className="text-right">
{t("field.publisher")}
</Label>
<Input
id="publisher"
name="publisher"
defaultValue={publisher}
placeholder={isEdit ? undefined : t("field.publisherPlaceholder")}
className="col-span-3"
/>
</div>
</div>
)
}

View File

@@ -53,12 +53,6 @@ export interface TextbookReaderProps {
* 必传,否则知识点面板将始终为空。
*/
textbookId: string
/**
* 是否可编辑。已废弃——改由内部 usePermission() 自动判断。
* 保留 prop 仅为向后兼容,传入值会被忽略。
* @deprecated 改用权限系统自动判断
*/
canEdit?: boolean
/**
* 题目创建器渲染函数P0-1 解耦)。
* 由页面层注入 questions 模块的 CreateQuestionDialog 实现。
@@ -176,8 +170,11 @@ export function TextbookReader({
const onCreateKnowledgePoint = async (formData: FormData) => {
setIsCreating(true)
await handleCreateKnowledgePoint(formData)
setIsCreating(false)
try {
await handleCreateKnowledgePoint(formData)
} finally {
setIsCreating(false)
}
}
const handleSaveContent = async () => {
@@ -224,19 +221,13 @@ export function TextbookReader({
return highlightKnowledgePoints(effectiveContent, currentChapterKPs)
}, [effectiveContent, currentChapterKPs])
// P2 状态驱动:仅保留 scrollIntoView滚动无法声明式实现
// 视觉高亮已由 TextbookContentPanel 中 isHighlighted 状态驱动,无需命令式 classList 操作
useEffect(() => {
if (!highlightedKpId) return
const el = document.querySelector(`[data-kp-id="${highlightedKpId}"]`)
if (!el) return
el.scrollIntoView({ behavior: "smooth", block: "center" })
el.classList.add("ring-2", "ring-primary", "ring-offset-2")
const timer = setTimeout(() => {
el.classList.remove("ring-2", "ring-primary", "ring-offset-2")
}, 2000)
return () => {
clearTimeout(timer)
}
}, [highlightedKpId])
// P2-4 侧边栏内容(章节/知识点/图谱 Tabs桌面端内联、移动端抽屉复用同一份
@@ -422,24 +413,29 @@ export function TextbookReader({
retryLabel={t("error.retry")}
>
<TextbookContentPanel
selected={selected}
isEditing={isEditing}
editContent={editContent}
setEditContent={setEditContent}
chapter={{ selected, processedContent }}
editing={{
isEditing,
editContent,
setEditContent,
isSaving,
startEditing,
cancelEditing: () => setIsEditing(false),
saveContent: handleSaveContent,
}}
selection={{
contentRef,
onPointerDown: handleContentPointerDown,
onContextMenuChange: handleContextMenuChange,
selectedText,
setCreateDialogOpen,
}}
highlight={{
highlightedKpId,
onHighlight: setHighlightedKpId,
onSwitchToKnowledgeTab: () => setActiveTab("knowledge"),
}}
canEdit={canEdit}
highlightedKpId={highlightedKpId}
onHighlight={setHighlightedKpId}
onSwitchToKnowledgeTab={() => setActiveTab("knowledge")}
contentRef={contentRef}
onPointerDown={handleContentPointerDown}
onContextMenuChange={handleContextMenuChange}
selectedText={selectedText}
setCreateDialogOpen={setCreateDialogOpen}
startEditing={startEditing}
cancelEditing={() => setIsEditing(false)}
saveContent={handleSaveContent}
isSaving={isSaving}
processedContent={processedContent}
/>
</TextbookSectionErrorBoundary>
</div>

View File

@@ -24,26 +24,19 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/shared/components/ui/alert-dialog"
import { Input } from "@/shared/components/ui/input"
import { Label } from "@/shared/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import { updateTextbookAction, deleteTextbookAction } from "../actions"
import { SUBJECTS, GRADES } from "../constants"
import { toast } from "sonner"
import type { Textbook } from "../types"
import { TextbookFormFields } from "./textbook-form-fields"
interface TextbookSettingsDialogProps {
textbook: Textbook
trigger?: React.ReactNode
/** 删除后跳转的 URL默认 "/teacher/textbooks" */
redirectAfterDelete?: string
}
export function TextbookSettingsDialog({ textbook, trigger }: TextbookSettingsDialogProps) {
export function TextbookSettingsDialog({ textbook, trigger, redirectAfterDelete = "/teacher/textbooks" }: TextbookSettingsDialogProps) {
const t = useTranslations("textbooks")
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
@@ -77,7 +70,7 @@ export function TextbookSettingsDialog({ textbook, trigger }: TextbookSettingsDi
if (result.success) {
toast.success(result.message)
router.push("/teacher/textbooks")
router.push(redirectAfterDelete)
} else {
toast.error(result.message)
}
@@ -107,65 +100,14 @@ export function TextbookSettingsDialog({ textbook, trigger }: TextbookSettingsDi
</DialogHeader>
<form action={handleUpdate}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">
{t("field.title")}
</Label>
<Input
id="title"
name="title"
defaultValue={textbook.title}
className="col-span-3"
required
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="subject" className="text-right">
{t("field.subject")}
</Label>
<Select name="subject" defaultValue={textbook.subject} required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder={t("field.subjectPlaceholder")} />
</SelectTrigger>
<SelectContent>
{SUBJECTS.map((s) => (
<SelectItem key={s.value} value={s.value}>
{t(`subject.${s.labelKey}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="grade" className="text-right">
{t("field.grade")}
</Label>
<Select name="grade" defaultValue={textbook.grade || undefined} required>
<SelectTrigger className="col-span-3">
<SelectValue placeholder={t("field.gradePlaceholder")} />
</SelectTrigger>
<SelectContent>
{GRADES.map((g) => (
<SelectItem key={g.value} value={g.value}>
{t(`grade.${g.labelKey}`)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="publisher" className="text-right">
{t("field.publisher")}
</Label>
<Input
id="publisher"
name="publisher"
defaultValue={textbook.publisher || ""}
className="col-span-3"
/>
</div>
</div>
<TextbookFormFields
defaultValues={{
title: textbook.title,
subject: textbook.subject,
grade: textbook.grade,
publisher: textbook.publisher,
}}
/>
<DialogFooter className="flex justify-between sm:justify-between">
<Button