diff --git a/app/Editor.jsx b/app/Editor.jsx index 385f8a7..ba65ee0 100644 --- a/app/Editor.jsx +++ b/app/Editor.jsx @@ -1,6 +1,6 @@ "use client"; import {useEffect, useRef, useState} from "react"; -import {Play, Square, RefreshCcw} from "lucide-react"; +import {Play, Square, RefreshCcw, Copy} from "lucide-react"; import {Tooltip} from "react-tooltip"; import {createEditor} from "../editor/index.js"; import {cn} from "./cn.js"; @@ -16,6 +16,7 @@ export function Editor({ autoRun = true, toolBarStart = null, pinToolbar = true, + onDuplicate = null, }) { const containerRef = useRef(null); const editorRef = useRef(null); @@ -109,6 +110,16 @@ export function Editor({ > + {onDuplicate && ( + + )}
diff --git a/app/EditorPage.jsx b/app/EditorPage.jsx index 6efd533..6bcc46b 100644 --- a/app/EditorPage.jsx +++ b/app/EditorPage.jsx @@ -1,9 +1,9 @@ "use client"; import {useState, useEffect, useRef, useCallback, useSyncExternalStore} from "react"; -import {notFound} from "next/navigation"; +import {notFound, useRouter} from "next/navigation"; import {Pencil} from "lucide-react"; import {Editor} from "./Editor.jsx"; -import {getNotebookById, createNotebook, addNotebook, saveNotebook, getNotebooks} from "./api.js"; +import {getNotebookById, createNotebook, addNotebook, saveNotebook, getNotebooks, duplicateNotebook} from "./api.js"; import {isDirtyStore, countStore} from "./store.js"; import {cn} from "./cn.js"; import {SafeLink} from "./SafeLink.jsx"; @@ -11,6 +11,7 @@ import {SafeLink} from "./SafeLink.jsx"; const UNSET = Symbol("UNSET"); export function EditorPage({id: initialId}) { + const router = useRouter(); const [notebook, setNotebook] = useState(UNSET); const [notebookList, setNotebookList] = useState([]); const [showInput, setShowInput] = useState(false); @@ -143,6 +144,12 @@ export function EditorPage({id: initialId}) { }, 100); } + function onDuplicate() { + const duplicated = duplicateNotebook(notebook); + addNotebook(duplicated); + router.push(`/notebooks/${duplicated.id}`); + } + return (
{!isAdded && notebookList.length > 0 && ( @@ -203,6 +210,7 @@ export function EditorPage({id: initialId}) { onUserInput={onUserInput} onBeforeEachRun={onBeforeEachRun} autoRun={autoRun} + onDuplicate={isAdded ? onDuplicate : null} toolBarStart={
{!isAdded && ( diff --git a/app/api.js b/app/api.js index c21b40a..dfc2160 100644 --- a/app/api.js +++ b/app/api.js @@ -84,3 +84,26 @@ export function saveNotebook(notebook) { const newNotebooks = notebooks.map((f) => (f.id === notebook.id ? updatedNotebook : f)); saveNotebooks(newNotebooks); } + +export function generateDuplicateName(originalName) { + // Handle names with extension like "[NAME].js" -> "[NAME] copy.js" + const lastDotIndex = originalName.lastIndexOf("."); + if (lastDotIndex > 0) { + const name = originalName.substring(0, lastDotIndex); + const extension = originalName.substring(lastDotIndex); + return `${name} copy${extension}`; + } + // Handle names without extension like "NAME" -> "NAME copy" + return `${originalName} copy`; +} + +export function duplicateNotebook(sourceNotebook) { + const newNotebook = createNotebook(); + const duplicatedTitle = generateDuplicateName(sourceNotebook.title); + return { + ...newNotebook, + title: duplicatedTitle, + content: sourceNotebook.content, + autoRun: sourceNotebook.autoRun, + }; +} diff --git a/app/examples/ExampleEditor.jsx b/app/examples/ExampleEditor.jsx new file mode 100644 index 0000000..d70cb63 --- /dev/null +++ b/app/examples/ExampleEditor.jsx @@ -0,0 +1,40 @@ +"use client"; +import {useRouter} from "next/navigation"; +import {Editor} from "../Editor.jsx"; +import {cn} from "../cn.js"; +import {duplicateNotebook, addNotebook} from "../api.js"; + +export function ExampleEditor({example, initialCode}) { + const router = useRouter(); + + function onDuplicate() { + const sourceNotebook = { + title: example.title, + content: initialCode, + autoRun: true, + }; + const duplicated = duplicateNotebook(sourceNotebook); + addNotebook(duplicated); + router.push(`/notebooks/${duplicated.id}`); + } + + return ( + + + Comment + +
+ } + /> + ); +} diff --git a/app/examples/[slug]/page.jsx b/app/examples/[slug]/page.jsx index 66c2581..efcaddb 100644 --- a/app/examples/[slug]/page.jsx +++ b/app/examples/[slug]/page.jsx @@ -1,8 +1,8 @@ import {notFound} from "next/navigation"; import {getAllJSExamples, removeJSMeta} from "../../utils.js"; -import {Editor} from "../../Editor.jsx"; import {cn} from "../../cn.js"; import {Meta} from "../../Meta.js"; +import {ExampleEditor} from "../ExampleEditor.jsx"; export async function generateStaticParams() { return getAllJSExamples().map((example) => ({slug: example.slug})); @@ -21,27 +21,13 @@ export default async function Page({params}) { const {slug} = await params; const example = getAllJSExamples().find((example) => example.slug === slug); if (!example) notFound(); + const initialCode = removeJSMeta(example.content); return (
- - - Comment - -
- } - /> +
); }