Initial working commit
Some checks failed
Automated Container Build / build-and-push (push) Failing after 7s
Some checks failed
Automated Container Build / build-and-push (push) Failing after 7s
This commit is contained in:
parent
666ceb7325
commit
b7ce314f01
105 changed files with 35510 additions and 11 deletions
187
src/components/import/CreateTab.tsx
Normal file
187
src/components/import/CreateTab.tsx
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface CreateTabProps {
|
||||
classId: string;
|
||||
onCreated: () => void;
|
||||
}
|
||||
|
||||
export function CreateTab({ classId, onCreated }: CreateTabProps) {
|
||||
const [deckName, setDeckName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [cards, setCards] = useState([
|
||||
{ id: "1", front: "", back: "" },
|
||||
{ id: "2", front: "", back: "" },
|
||||
{ id: "3", front: "", back: "" },
|
||||
]);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function addCard() {
|
||||
setCards([
|
||||
...cards,
|
||||
{ id: Date.now().toString(), front: "", back: "" },
|
||||
]);
|
||||
}
|
||||
|
||||
function removeCard(id: string) {
|
||||
if (cards.length <= 1) return;
|
||||
setCards(cards.filter((c) => c.id !== id));
|
||||
}
|
||||
|
||||
function updateCard(id: string, field: "front" | "back", value: string) {
|
||||
setCards(
|
||||
cards.map((c) => (c.id === id ? { ...c, [field]: value } : c))
|
||||
);
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
setError(null);
|
||||
if (!deckName.trim()) {
|
||||
setError("Deck title is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
const validCards = cards.filter((c) => c.front.trim() && c.back.trim());
|
||||
if (validCards.length === 0) {
|
||||
setError("Please add at least one complete card.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch("/api/decks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
classId,
|
||||
name: deckName,
|
||||
data: {
|
||||
type: "flashcards",
|
||||
deckName: deckName,
|
||||
description: description || undefined,
|
||||
cards: validCards.map((c) => ({
|
||||
front: c.front,
|
||||
back: c.back,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to create deck");
|
||||
}
|
||||
|
||||
onCreated();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{error && (
|
||||
<div className="p-3 bg-error-bg text-error rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deck Metadata */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-heading mb-1">
|
||||
Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={deckName}
|
||||
onChange={(e) => setDeckName(e.target.value)}
|
||||
placeholder="Enter a title, e.g. Biology Ch. 1"
|
||||
className="w-full px-4 py-2.5 rounded-lg border border-border bg-bg-surface-alt/50 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-heading mb-1">
|
||||
Description (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Add a description..."
|
||||
className="w-full px-4 py-2.5 rounded-lg border border-border bg-bg-surface-alt/50 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards */}
|
||||
<div className="space-y-4">
|
||||
{cards.map((card, index) => (
|
||||
<div
|
||||
key={card.id}
|
||||
className="bg-bg-surface-alt rounded-xl border border-border-light overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-border-light bg-bg-base/50">
|
||||
<span className="font-semibold text-text-muted">{index + 1}</span>
|
||||
<button
|
||||
onClick={() => removeCard(card.id)}
|
||||
disabled={cards.length <= 1}
|
||||
className="p-1.5 text-text-muted hover:text-error transition-colors disabled:opacity-30 cursor-pointer"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={card.front}
|
||||
onChange={(e) => updateCard(card.id, "front", e.target.value)}
|
||||
placeholder="Enter term"
|
||||
className="w-full px-0 py-2 bg-transparent border-0 border-b-2 border-border-light focus:border-primary focus:ring-0 text-sm transition-colors outline-none"
|
||||
/>
|
||||
<span className="block mt-1 text-[10px] font-semibold tracking-wider text-text-muted uppercase">
|
||||
Term
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={card.back}
|
||||
onChange={(e) => updateCard(card.id, "back", e.target.value)}
|
||||
placeholder="Enter definition"
|
||||
className="w-full px-0 py-2 bg-transparent border-0 border-b-2 border-border-light focus:border-primary focus:ring-0 text-sm transition-colors outline-none"
|
||||
/>
|
||||
<span className="block mt-1 text-[10px] font-semibold tracking-wider text-text-muted uppercase">
|
||||
Definition
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-4 pt-4">
|
||||
<button
|
||||
onClick={addCard}
|
||||
className="px-6 py-2.5 rounded-full bg-bg-surface-alt border border-border-light text-sm font-medium text-text-heading hover:bg-bg-callout transition-colors cursor-pointer"
|
||||
>
|
||||
+ Add a card
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
disabled={saving}
|
||||
className="w-full md:w-auto px-8 py-3 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Creating..." : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
108
src/components/import/GenerateTab.tsx
Normal file
108
src/components/import/GenerateTab.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function GenerateTab({ importType }: { importType: "flashcards" | "quizzes" }) {
|
||||
const [instructions, setInstructions] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/settings/llm-instructions?type=${importType}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setInstructions(data.value))
|
||||
.finally(() => setLoading(false));
|
||||
}, [importType]);
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
try {
|
||||
await fetch(`/api/settings/llm-instructions?type=${importType}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ value: instructions }),
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReset() {
|
||||
if (!confirm("Reset to default instructions?")) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch(`/api/settings/llm-instructions?type=${importType}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ value: "__RESET__" }),
|
||||
});
|
||||
const data = await res.json();
|
||||
setInstructions(data.value);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopy() {
|
||||
await navigator.clipboard.writeText(instructions);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="animate-subtle-pulse text-text-muted">Loading instructions...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-text-secondary">
|
||||
Copy these instructions and paste them into an LLM chat along with your study material. The LLM will generate JSON you can paste into the Import tab.
|
||||
</p>
|
||||
|
||||
<textarea
|
||||
value={instructions}
|
||||
onChange={(e) => setInstructions(e.target.value)}
|
||||
rows={14}
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-bg-surface-alt/50 text-sm text-text-body font-mono leading-relaxed focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200 resize-y"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
||||
</svg>
|
||||
Copy to Clipboard
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 rounded-lg border border-border text-sm text-text-secondary hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Saving..." : "Save Changes"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 rounded-lg text-sm text-text-muted hover:text-error hover:bg-error-bg transition-all duration-200 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
Reset to Default
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
src/components/import/ImportModal.tsx
Normal file
112
src/components/import/ImportModal.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { GenerateTab } from "./GenerateTab";
|
||||
import { ImportTab } from "./ImportTab";
|
||||
import { CreateTab } from "./CreateTab";
|
||||
|
||||
interface ImportModalProps {
|
||||
classId: string;
|
||||
importType: "flashcards" | "quizzes";
|
||||
onClose: () => void;
|
||||
onImported: () => void;
|
||||
}
|
||||
|
||||
export function ImportModal({
|
||||
classId,
|
||||
importType,
|
||||
onClose,
|
||||
onImported,
|
||||
}: ImportModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<"generate" | "import" | "create">("import");
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40 backdrop-blur-sm animate-fade-in"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative w-full max-w-2xl max-h-[90vh] bg-bg-surface rounded-2xl shadow-[var(--shadow-modal)] animate-slide-up flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 pt-5 pb-0">
|
||||
<h2 className="text-lg font-semibold text-text-heading">
|
||||
Import {importType === "flashcards" ? "Flashcard Deck" : "Quiz"}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="px-6 pt-4">
|
||||
<div className="flex gap-0 border-b border-border-light">
|
||||
<button
|
||||
onClick={() => setActiveTab("generate")}
|
||||
className={`relative px-4 py-2.5 text-sm font-medium transition-colors cursor-pointer ${
|
||||
activeTab === "generate"
|
||||
? "text-primary"
|
||||
: "text-text-muted hover:text-text-heading"
|
||||
}`}
|
||||
>
|
||||
Generate
|
||||
{activeTab === "generate" && (
|
||||
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-t-full" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("import")}
|
||||
className={`relative px-4 py-2.5 text-sm font-medium transition-colors cursor-pointer ${
|
||||
activeTab === "import"
|
||||
? "text-primary"
|
||||
: "text-text-muted hover:text-text-heading"
|
||||
}`}
|
||||
>
|
||||
Import
|
||||
{activeTab === "import" && (
|
||||
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-t-full" />
|
||||
)}
|
||||
</button>
|
||||
{importType === "flashcards" && (
|
||||
<button
|
||||
onClick={() => setActiveTab("create")}
|
||||
className={`relative px-4 py-2.5 text-sm font-medium transition-colors cursor-pointer ${
|
||||
activeTab === "create"
|
||||
? "text-primary"
|
||||
: "text-text-muted hover:text-text-heading"
|
||||
}`}
|
||||
>
|
||||
Create
|
||||
{activeTab === "create" && (
|
||||
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-t-full" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 md:p-8">
|
||||
{activeTab === "generate" && <GenerateTab importType={importType} />}
|
||||
{activeTab === "import" && (
|
||||
<ImportTab
|
||||
classId={classId}
|
||||
importType={importType}
|
||||
onImported={onImported}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "create" && importType === "flashcards" && (
|
||||
<CreateTab classId={classId} onCreated={onImported} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
223
src/components/import/ImportTab.tsx
Normal file
223
src/components/import/ImportTab.tsx
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { parseAndRepairJson } from "@/lib/jsonRepair";
|
||||
import {
|
||||
flashcardImportSchema,
|
||||
quizImportSchema,
|
||||
} from "@/lib/validation/importSchemas";
|
||||
|
||||
interface ImportTabProps {
|
||||
classId: string;
|
||||
importType: "flashcards" | "quizzes";
|
||||
onImported: () => void;
|
||||
}
|
||||
|
||||
interface PreviewData {
|
||||
type: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
itemCount: number;
|
||||
categories?: string[];
|
||||
wasRepaired: boolean;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export function ImportTab({ classId, importType, onImported }: ImportTabProps) {
|
||||
const [jsonText, setJsonText] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [preview, setPreview] = useState<PreviewData | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
function handleJsonChange(text: string) {
|
||||
setJsonText(text);
|
||||
setError("");
|
||||
setPreview(null);
|
||||
|
||||
if (!text.trim()) return;
|
||||
|
||||
// Run the parse → repair → validate pipeline
|
||||
const result = parseAndRepairJson(text);
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.error || "Failed to parse JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate with Zod
|
||||
const schema =
|
||||
importType === "flashcards" ? flashcardImportSchema : quizImportSchema;
|
||||
const validated = schema.safeParse(result.data);
|
||||
|
||||
if (!validated.success) {
|
||||
const issues = validated.error.issues
|
||||
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
||||
.join("\n");
|
||||
setError(`Validation errors:\n${issues}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build preview
|
||||
const data = validated.data;
|
||||
if (data.type === "flashcards") {
|
||||
setPreview({
|
||||
type: "flashcards",
|
||||
name: data.deckName,
|
||||
description: data.description,
|
||||
itemCount: data.cards.length,
|
||||
wasRepaired: result.wasRepaired,
|
||||
data: data,
|
||||
});
|
||||
if (!name) setName(data.deckName);
|
||||
} else {
|
||||
const categories = [
|
||||
...new Set(data.questions.map((q) => q.category.trim().toLowerCase())),
|
||||
];
|
||||
setPreview({
|
||||
type: "quiz",
|
||||
name: data.quizName,
|
||||
description: data.description,
|
||||
itemCount: data.questions.length,
|
||||
categories,
|
||||
wasRepaired: result.wasRepaired,
|
||||
data: data,
|
||||
});
|
||||
if (!name) setName(data.quizName);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
if (!preview || !name.trim()) return;
|
||||
setImporting(true);
|
||||
|
||||
try {
|
||||
const endpoint =
|
||||
importType === "flashcards" ? "/api/decks" : "/api/quizzes";
|
||||
const res = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
classId,
|
||||
data: preview.data,
|
||||
name: name.trim(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
setError(err.error || "Import failed");
|
||||
return;
|
||||
}
|
||||
|
||||
onImported();
|
||||
} catch {
|
||||
setError("Network error during import");
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Name field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1.5">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder={
|
||||
importType === "flashcards" ? "Deck name..." : "Quiz name..."
|
||||
}
|
||||
className="w-full px-3.5 py-2.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* JSON paste area */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-1.5">
|
||||
Paste JSON
|
||||
</label>
|
||||
<textarea
|
||||
value={jsonText}
|
||||
onChange={(e) => handleJsonChange(e.target.value)}
|
||||
rows={10}
|
||||
placeholder="Paste the JSON output from your LLM here..."
|
||||
className="w-full px-4 py-3 rounded-lg border border-border bg-bg-surface-alt/50 text-sm text-text-body font-mono leading-relaxed focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200 resize-y"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error display */}
|
||||
{error && (
|
||||
<div className="bg-error-bg border border-error/20 rounded-lg px-4 py-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg
|
||||
className="w-4 h-4 text-error flex-shrink-0 mt-0.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<pre className="text-sm text-error whitespace-pre-wrap font-mono">
|
||||
{error}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview */}
|
||||
{preview && (
|
||||
<div className="bg-bg-callout border border-primary/10 rounded-lg px-4 py-4 space-y-3">
|
||||
{preview.wasRepaired && (
|
||||
<div className="flex items-center gap-2 text-xs text-primary bg-primary/5 rounded px-2.5 py-1.5">
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
JSON was auto-repaired (minor syntax fixes)
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-text-heading font-medium">
|
||||
Preview: {preview.itemCount}{" "}
|
||||
{preview.type === "flashcards" ? "cards" : "questions"}
|
||||
</div>
|
||||
|
||||
{preview.description && (
|
||||
<p className="text-sm text-text-secondary">{preview.description}</p>
|
||||
)}
|
||||
|
||||
{preview.categories && preview.categories.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{preview.categories.map((cat) => (
|
||||
<span
|
||||
key={cat}
|
||||
className="inline-flex px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium"
|
||||
>
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Import button */}
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={!preview || !name.trim() || importing}
|
||||
className="w-full py-2.5 px-4 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
{importing ? "Importing..." : "Confirm Import"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue