feat(lesson-preparation): add anchor canvas design, new blocks, and textbook content node
- Add anchor injector for canvas-based anchor positioning - Add new block components: blackboard, homework, import, key-point, new-teaching, objective, summary - Add textbook content node for React Flow canvas - Update actions (kp, publish, main), data-access (templates, versions, main) - Update editor, node-editor, block-renderer, and picker components - Update schema, types, hooks, and lib utilities (document-migration, node-summary, rf-mappers)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Tag } from "lucide-react";
|
||||
import type { BlackboardBlockData } from "../../types";
|
||||
import { KnowledgePointPicker } from "../knowledge-point-picker";
|
||||
|
||||
interface Props {
|
||||
data: BlackboardBlockData;
|
||||
textbookId?: string;
|
||||
chapterId?: string;
|
||||
onUpdate: (data: BlackboardBlockData) => void;
|
||||
}
|
||||
|
||||
const LAYOUTS: BlackboardBlockData["layout"][] = ["structure", "mindmap", "text"];
|
||||
|
||||
export function BlackboardBlock({ data, textbookId, chapterId, onUpdate }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
const [showKpPicker, setShowKpPicker] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs text-on-surface-variant">
|
||||
{t("blackboard.hint")}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium block mb-1">
|
||||
{t("blackboard.layoutLabel")}
|
||||
</label>
|
||||
<select
|
||||
value={data.layout}
|
||||
onChange={(e) =>
|
||||
onUpdate({
|
||||
...data,
|
||||
layout: e.target.value as BlackboardBlockData["layout"],
|
||||
})
|
||||
}
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 bg-surface"
|
||||
>
|
||||
{LAYOUTS.map((l) => (
|
||||
<option key={l} value={l}>
|
||||
{t(`blackboard.layout.${l}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium block mb-1">
|
||||
{t("blackboard.contentLabel")}
|
||||
</label>
|
||||
<textarea
|
||||
value={data.content}
|
||||
onChange={(e) => onUpdate({ ...data, content: e.target.value })}
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[120px] font-mono"
|
||||
placeholder={t("blackboard.contentPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{data.knowledgePointIds.length > 0 && (
|
||||
<span className="text-xs text-on-surface-variant">
|
||||
{t("knowledgePoint.linked", { count: data.knowledgePointIds.length })}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowKpPicker(true)}
|
||||
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
<Tag className="w-3 h-3" />
|
||||
{t("knowledgePoint.annotate")}
|
||||
</button>
|
||||
</div>
|
||||
{showKpPicker && (
|
||||
<KnowledgePointPicker
|
||||
textbookId={textbookId}
|
||||
chapterId={chapterId}
|
||||
selectedIds={data.knowledgePointIds}
|
||||
onChange={(ids) => onUpdate({ ...data, knowledgePointIds: ids })}
|
||||
onClose={() => setShowKpPicker(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import type { HomeworkAssignment, HomeworkBlockData } from "../../types";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
|
||||
interface Props {
|
||||
data: HomeworkBlockData;
|
||||
onUpdate: (data: HomeworkBlockData) => void;
|
||||
}
|
||||
|
||||
const TYPES: HomeworkAssignment["type"][] = ["exercise", "reading", "writing"];
|
||||
|
||||
export function HomeworkBlock({ data, onUpdate }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
|
||||
function updateItem(index: number, patch: Partial<HomeworkAssignment>) {
|
||||
const next = data.assignments.map((it, i) =>
|
||||
i === index ? { ...it, ...patch } : it,
|
||||
);
|
||||
onUpdate({ ...data, assignments: next });
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
onUpdate({
|
||||
...data,
|
||||
assignments: [...data.assignments, { type: "exercise", description: "" }],
|
||||
});
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
onUpdate({
|
||||
...data,
|
||||
assignments: data.assignments.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-on-surface-variant">
|
||||
{t("homework.hint")}
|
||||
</div>
|
||||
{data.assignments.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2">
|
||||
<select
|
||||
value={item.type}
|
||||
onChange={(e) =>
|
||||
updateItem(idx, {
|
||||
type: e.target.value as HomeworkAssignment["type"],
|
||||
})
|
||||
}
|
||||
className="text-xs border border-outline-variant rounded px-1 py-1 bg-surface"
|
||||
>
|
||||
{TYPES.map((tp) => (
|
||||
<option key={tp} value={tp}>
|
||||
{t(`homework.type.${tp}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={item.refId ?? ""}
|
||||
onChange={(e) => updateItem(idx, { refId: e.target.value || undefined })}
|
||||
className="w-32 text-sm border border-outline-variant rounded px-2 py-1"
|
||||
placeholder={t("homework.refIdPlaceholder")}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={item.description}
|
||||
onChange={(e) => updateItem(idx, { description: e.target.value })}
|
||||
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1"
|
||||
placeholder={t("homework.descriptionPlaceholder")}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="!p-1 text-error"
|
||||
onClick={() => removeItem(idx)}
|
||||
aria-label={t("action.delete")}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addItem}>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
{t("homework.addItem")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { ImportBlockData } from "../../types";
|
||||
|
||||
interface Props {
|
||||
data: ImportBlockData;
|
||||
onUpdate: (data: ImportBlockData) => void;
|
||||
}
|
||||
|
||||
const METHODS: ImportBlockData["method"][] = ["question", "situation", "review", "other"];
|
||||
|
||||
export function ImportBlock({ data, onUpdate }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs text-on-surface-variant">
|
||||
{t("import.hint")}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium block mb-1">
|
||||
{t("import.methodLabel")}
|
||||
</label>
|
||||
<select
|
||||
value={data.method}
|
||||
onChange={(e) =>
|
||||
onUpdate({ ...data, method: e.target.value as ImportBlockData["method"] })
|
||||
}
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 bg-surface"
|
||||
>
|
||||
{METHODS.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{t(`import.method.${m}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium block mb-1">
|
||||
{t("import.promptLabel")}
|
||||
</label>
|
||||
<textarea
|
||||
value={data.prompt}
|
||||
onChange={(e) => onUpdate({ ...data, prompt: e.target.value })}
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[60px]"
|
||||
placeholder={t("import.promptPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium block mb-1">
|
||||
{t("import.durationLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={30}
|
||||
value={data.durationMin}
|
||||
onChange={(e) =>
|
||||
onUpdate({ ...data, durationMin: Number(e.target.value) || 0 })
|
||||
}
|
||||
className="w-20 text-sm border border-outline-variant rounded px-2 py-1 bg-surface"
|
||||
/>
|
||||
<span className="text-xs text-on-surface-variant ml-2">
|
||||
{t("import.durationUnit")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import type { KeyPointBlockData, KeyPointItem } from "../../types";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
|
||||
interface Props {
|
||||
data: KeyPointBlockData;
|
||||
onUpdate: (data: KeyPointBlockData) => void;
|
||||
}
|
||||
|
||||
const TYPES: KeyPointItem["type"][] = ["key", "difficult"];
|
||||
|
||||
export function KeyPointBlock({ data, onUpdate }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
|
||||
function updateItem(index: number, patch: Partial<KeyPointItem>) {
|
||||
const next = data.keyPoints.map((it, i) =>
|
||||
i === index ? { ...it, ...patch } : it,
|
||||
);
|
||||
onUpdate({ ...data, keyPoints: next });
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
onUpdate({
|
||||
...data,
|
||||
keyPoints: [...data.keyPoints, { type: "key", text: "" }],
|
||||
});
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
onUpdate({
|
||||
...data,
|
||||
keyPoints: data.keyPoints.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-on-surface-variant">
|
||||
{t("keyPoint.hint")}
|
||||
</div>
|
||||
{data.keyPoints.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2">
|
||||
<select
|
||||
value={item.type}
|
||||
onChange={(e) =>
|
||||
updateItem(idx, { type: e.target.value as KeyPointItem["type"] })
|
||||
}
|
||||
className="text-xs border border-outline-variant rounded px-1 py-1 bg-surface"
|
||||
>
|
||||
{TYPES.map((tp) => (
|
||||
<option key={tp} value={tp}>
|
||||
{t(`keyPoint.type.${tp}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<textarea
|
||||
value={item.text}
|
||||
onChange={(e) => updateItem(idx, { text: e.target.value })}
|
||||
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[40px]"
|
||||
placeholder={t("keyPoint.textPlaceholder")}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="!p-1 text-error"
|
||||
onClick={() => removeItem(idx)}
|
||||
aria-label={t("action.delete")}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addItem}>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
{t("keyPoint.addItem")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Plus, Trash2, Tag } from "lucide-react";
|
||||
import type { NewTeachingBlockData, NewTeachingPoint } from "../../types";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { KnowledgePointPicker } from "../knowledge-point-picker";
|
||||
|
||||
interface Props {
|
||||
data: NewTeachingBlockData;
|
||||
textbookId?: string;
|
||||
chapterId?: string;
|
||||
onUpdate: (data: NewTeachingBlockData) => void;
|
||||
}
|
||||
|
||||
export function NewTeachingBlock({ data, textbookId, chapterId, onUpdate }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
const [pickerFor, setPickerFor] = useState<number | null>(null);
|
||||
|
||||
function updatePoint(index: number, patch: Partial<NewTeachingPoint>) {
|
||||
const next = data.teachingPoints.map((it, i) =>
|
||||
i === index ? { ...it, ...patch } : it,
|
||||
);
|
||||
onUpdate({ ...data, teachingPoints: next });
|
||||
}
|
||||
|
||||
function addPoint() {
|
||||
onUpdate({
|
||||
...data,
|
||||
teachingPoints: [
|
||||
...data.teachingPoints,
|
||||
{ knowledgePointIds: [], outline: "", boardNotes: "" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function removePoint(index: number) {
|
||||
onUpdate({
|
||||
...data,
|
||||
teachingPoints: data.teachingPoints.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs text-on-surface-variant">
|
||||
{t("newTeaching.hint")}
|
||||
</div>
|
||||
{data.teachingPoints.map((point, idx) => (
|
||||
<div key={idx} className="border border-outline-variant rounded p-2 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">
|
||||
{t("newTeaching.pointIndex", { index: idx + 1 })}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="!p-1 text-error"
|
||||
onClick={() => removePoint(idx)}
|
||||
aria-label={t("action.delete")}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs block mb-1">
|
||||
{t("newTeaching.outlineLabel")}
|
||||
</label>
|
||||
<textarea
|
||||
value={point.outline}
|
||||
onChange={(e) => updatePoint(idx, { outline: e.target.value })}
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[60px]"
|
||||
placeholder={t("newTeaching.outlinePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs block mb-1">
|
||||
{t("newTeaching.boardNotesLabel")}
|
||||
</label>
|
||||
<textarea
|
||||
value={point.boardNotes}
|
||||
onChange={(e) => updatePoint(idx, { boardNotes: e.target.value })}
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[40px]"
|
||||
placeholder={t("newTeaching.boardNotesPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{point.knowledgePointIds.length > 0 && (
|
||||
<span className="text-xs text-on-surface-variant">
|
||||
{t("knowledgePoint.linked", { count: point.knowledgePointIds.length })}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setPickerFor(idx)}
|
||||
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
<Tag className="w-3 h-3" />
|
||||
{t("knowledgePoint.annotate")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addPoint}>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
{t("newTeaching.addPoint")}
|
||||
</Button>
|
||||
{pickerFor !== null && (
|
||||
<KnowledgePointPicker
|
||||
textbookId={textbookId}
|
||||
chapterId={chapterId}
|
||||
selectedIds={data.teachingPoints[pickerFor]?.knowledgePointIds ?? []}
|
||||
onChange={(ids) => updatePoint(pickerFor, { knowledgePointIds: ids })}
|
||||
onClose={() => setPickerFor(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import type { ObjectiveBlockData, ObjectiveItem } from "../../types";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
|
||||
interface Props {
|
||||
data: ObjectiveBlockData;
|
||||
onUpdate: (data: ObjectiveBlockData) => void;
|
||||
}
|
||||
|
||||
const DIMENSIONS: ObjectiveItem["dimension"][] = ["knowledge", "process", "emotion"];
|
||||
|
||||
export function ObjectiveBlock({ data, onUpdate }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
|
||||
function updateItem(index: number, patch: Partial<ObjectiveItem>) {
|
||||
const next = data.objectives.map((it, i) =>
|
||||
i === index ? { ...it, ...patch } : it,
|
||||
);
|
||||
onUpdate({ ...data, objectives: next });
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
onUpdate({
|
||||
...data,
|
||||
objectives: [
|
||||
...data.objectives,
|
||||
{ dimension: "knowledge", text: "" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
onUpdate({
|
||||
...data,
|
||||
objectives: data.objectives.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-on-surface-variant">
|
||||
{t("objective.hint")}
|
||||
</div>
|
||||
{data.objectives.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2">
|
||||
<select
|
||||
value={item.dimension}
|
||||
onChange={(e) =>
|
||||
updateItem(idx, {
|
||||
dimension: e.target.value as ObjectiveItem["dimension"],
|
||||
})
|
||||
}
|
||||
className="text-xs border border-outline-variant rounded px-1 py-1 bg-surface"
|
||||
>
|
||||
{DIMENSIONS.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
{t(`objective.dimension.${d}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<textarea
|
||||
value={item.text}
|
||||
onChange={(e) => updateItem(idx, { text: e.target.value })}
|
||||
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[40px]"
|
||||
placeholder={t("objective.textPlaceholder")}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="!p-1 text-error"
|
||||
onClick={() => removeItem(idx)}
|
||||
aria-label={t("action.delete")}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addItem}>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
{t("objective.addItem")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { RichTextBlock } from "./rich-text-block";
|
||||
import type { RichTextBlockData } from "../../types";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import type { ReflectionBlockData, ReflectionItem } from "../../types";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
|
||||
interface Props {
|
||||
data: RichTextBlockData;
|
||||
onUpdate: (data: RichTextBlockData) => void;
|
||||
data: ReflectionBlockData;
|
||||
onUpdate: (data: ReflectionBlockData) => void;
|
||||
}
|
||||
|
||||
export function ReflectionBlock(props: Props) {
|
||||
const ASPECTS: ReflectionItem["aspect"][] = ["effectiveness", "problems", "improvements"];
|
||||
|
||||
export function ReflectionBlock({ data, onUpdate }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
// 教学反思在 P1 阶段与普通富文本一致,P3 再扩展学情数据嵌入
|
||||
return <RichTextBlock {...props} hint={t("reflection.hint")} />;
|
||||
|
||||
function updateItem(index: number, patch: Partial<ReflectionItem>) {
|
||||
const next = data.reflection.map((it, i) =>
|
||||
i === index ? { ...it, ...patch } : it,
|
||||
);
|
||||
onUpdate({ ...data, reflection: next });
|
||||
}
|
||||
|
||||
function addItem() {
|
||||
onUpdate({
|
||||
...data,
|
||||
reflection: [...data.reflection, { aspect: "effectiveness", text: "" }],
|
||||
});
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
onUpdate({
|
||||
...data,
|
||||
reflection: data.reflection.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-on-surface-variant">
|
||||
{t("reflection.hint")}
|
||||
</div>
|
||||
{data.reflection.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2">
|
||||
<select
|
||||
value={item.aspect}
|
||||
onChange={(e) =>
|
||||
updateItem(idx, {
|
||||
aspect: e.target.value as ReflectionItem["aspect"],
|
||||
})
|
||||
}
|
||||
className="text-xs border border-outline-variant rounded px-1 py-1 bg-surface"
|
||||
>
|
||||
{ASPECTS.map((a) => (
|
||||
<option key={a} value={a}>
|
||||
{t(`reflection.aspect.${a}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<textarea
|
||||
value={item.text}
|
||||
onChange={(e) => updateItem(idx, { text: e.target.value })}
|
||||
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[60px]"
|
||||
placeholder={t("reflection.textPlaceholder")}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="!p-1 text-error"
|
||||
onClick={() => removeItem(idx)}
|
||||
aria-label={t("action.delete")}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addItem}>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
{t("reflection.addItem")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import type { SummaryBlockData } from "../../types";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
|
||||
interface Props {
|
||||
data: SummaryBlockData;
|
||||
onUpdate: (data: SummaryBlockData) => void;
|
||||
}
|
||||
|
||||
export function SummaryBlock({ data, onUpdate }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
|
||||
function updatePoint(index: number, value: string) {
|
||||
const next = data.summaryPoints.map((p, i) => (i === index ? value : p));
|
||||
onUpdate({ ...data, summaryPoints: next });
|
||||
}
|
||||
|
||||
function addPoint() {
|
||||
onUpdate({
|
||||
...data,
|
||||
summaryPoints: [...data.summaryPoints, ""],
|
||||
});
|
||||
}
|
||||
|
||||
function removePoint(index: number) {
|
||||
onUpdate({
|
||||
...data,
|
||||
summaryPoints: data.summaryPoints.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-on-surface-variant">
|
||||
{t("summary.hint")}
|
||||
</div>
|
||||
{data.summaryPoints.map((point, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2">
|
||||
<span className="text-xs text-on-surface-variant mt-1">{idx + 1}.</span>
|
||||
<input
|
||||
type="text"
|
||||
value={point}
|
||||
onChange={(e) => updatePoint(idx, e.target.value)}
|
||||
className="flex-1 text-sm border border-outline-variant rounded px-2 py-1"
|
||||
placeholder={t("summary.pointPlaceholder")}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="!p-1 text-error"
|
||||
onClick={() => removePoint(idx)}
|
||||
aria-label={t("action.delete")}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addPoint}>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
{t("summary.addPoint")}
|
||||
</Button>
|
||||
<div>
|
||||
<label className="text-xs font-medium block mb-1 mt-2">
|
||||
{t("summary.homeworkPreviewLabel")}
|
||||
</label>
|
||||
<textarea
|
||||
value={data.homeworkPreview}
|
||||
onChange={(e) => onUpdate({ ...data, homeworkPreview: e.target.value })}
|
||||
className="w-full text-sm border border-outline-variant rounded px-2 py-1 resize-y min-h-[40px]"
|
||||
placeholder={t("summary.homeworkPreviewPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user