From c9ea848e1403a170fd71f3650da3ebfe23b21411 Mon Sep 17 00:00:00 2001 From: pearmini Date: Sat, 11 Oct 2025 11:05:51 -0400 Subject: [PATCH 1/2] Add duplicate-button --- app/Editor.jsx | 13 ++++++++++- app/EditorPage.jsx | 19 ++++++++++++++-- app/api.js | 23 +++++++++++++++++++ app/examples/ExampleEditor.jsx | 41 ++++++++++++++++++++++++++++++++++ app/examples/[slug]/page.jsx | 21 ++++------------- 5 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 app/examples/ExampleEditor.jsx 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..23911d9 100644 --- a/app/EditorPage.jsx +++ b/app/EditorPage.jsx @@ -1,9 +1,16 @@ "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 +18,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 +151,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 +217,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..e9db4c3 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 "xxx.js" -> "xxx 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 "xxx" -> "xxx 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..750fb74 --- /dev/null +++ b/app/examples/ExampleEditor.jsx @@ -0,0 +1,41 @@ +"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..edab918 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,14 @@ 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 - -
- } - /> +
); } From 2f65f3172a5f43df3e1b4d603434b39059ece2bf Mon Sep 17 00:00:00 2001 From: pearmini Date: Sat, 11 Oct 2025 11:10:11 -0400 Subject: [PATCH 2/2] Fix coding style --- app/EditorPage.jsx | 9 +-------- app/api.js | 4 ++-- app/examples/ExampleEditor.jsx | 1 - app/examples/[slug]/page.jsx | 1 - 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/EditorPage.jsx b/app/EditorPage.jsx index 23911d9..6bcc46b 100644 --- a/app/EditorPage.jsx +++ b/app/EditorPage.jsx @@ -3,14 +3,7 @@ import {useState, useEffect, useRef, useCallback, useSyncExternalStore} from "re import {notFound, useRouter} from "next/navigation"; import {Pencil} from "lucide-react"; import {Editor} from "./Editor.jsx"; -import { - getNotebookById, - createNotebook, - addNotebook, - saveNotebook, - getNotebooks, - duplicateNotebook, -} 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"; diff --git a/app/api.js b/app/api.js index e9db4c3..dfc2160 100644 --- a/app/api.js +++ b/app/api.js @@ -86,14 +86,14 @@ export function saveNotebook(notebook) { } export function generateDuplicateName(originalName) { - // Handle names with extension like "xxx.js" -> "xxx copy.js" + // 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 "xxx" -> "xxx copy" + // Handle names without extension like "NAME" -> "NAME copy" return `${originalName} copy`; } diff --git a/app/examples/ExampleEditor.jsx b/app/examples/ExampleEditor.jsx index 750fb74..d70cb63 100644 --- a/app/examples/ExampleEditor.jsx +++ b/app/examples/ExampleEditor.jsx @@ -38,4 +38,3 @@ export function ExampleEditor({example, initialCode}) { /> ); } - diff --git a/app/examples/[slug]/page.jsx b/app/examples/[slug]/page.jsx index edab918..efcaddb 100644 --- a/app/examples/[slug]/page.jsx +++ b/app/examples/[slug]/page.jsx @@ -22,7 +22,6 @@ export default async function Page({params}) { const example = getAllJSExamples().find((example) => example.slug === slug); if (!example) notFound(); const initialCode = removeJSMeta(example.content); - return (