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 (
- }
- />
+
);
}