Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/Editor.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -16,6 +16,7 @@ export function Editor({
autoRun = true,
toolBarStart = null,
pinToolbar = true,
onDuplicate = null,
}) {
const containerRef = useRef(null);
const editorRef = useRef(null);
Expand Down Expand Up @@ -109,6 +110,16 @@ export function Editor({
>
<Square className={cn(styles.iconButton)} />
</button>
{onDuplicate && (
<button
onClick={onDuplicate}
data-tooltip-id="action-tooltip"
data-tooltip-content="Duplicate Notebook"
data-tooltip-place="bottom"
>
<Copy className={cn(styles.iconButton)} />
</button>
)}
</div>
</div>
<div ref={containerRef}>
Expand Down
12 changes: 10 additions & 2 deletions app/EditorPage.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
"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";

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);
Expand Down Expand Up @@ -143,6 +144,12 @@ export function EditorPage({id: initialId}) {
}, 100);
}

function onDuplicate() {
const duplicated = duplicateNotebook(notebook);
addNotebook(duplicated);
router.push(`/notebooks/${duplicated.id}`);
}

return (
<div>
{!isAdded && notebookList.length > 0 && (
Expand Down Expand Up @@ -203,6 +210,7 @@ export function EditorPage({id: initialId}) {
onUserInput={onUserInput}
onBeforeEachRun={onBeforeEachRun}
autoRun={autoRun}
onDuplicate={isAdded ? onDuplicate : null}
toolBarStart={
<div className={cn("flex items-center gap-2")}>
{!isAdded && (
Expand Down
23 changes: 23 additions & 0 deletions app/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
40 changes: 40 additions & 0 deletions app/examples/ExampleEditor.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Editor
initialCode={initialCode}
key={example.title}
onDuplicate={onDuplicate}
toolBarStart={
<div className={cn("flex items-center")} key={example.slug}>
<a
href={`https://github.com/recho-dev/recho/pull/${example.pull_request}`}
target="_blank"
rel="noreferrer"
className={cn("bg-green-700 text-white rounded-md px-3 py-1 text-sm hover:bg-green-800")}
>
Comment
</a>
</div>
}
/>
);
}
20 changes: 3 additions & 17 deletions app/examples/[slug]/page.jsx
Original file line number Diff line number Diff line change
@@ -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}));
Expand All @@ -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 (
<div className={cn("max-w-screen-lg lg:mx-auto mx-4 lg:my-10 my-4")}>
<div className={cn("mb-6")}>
<Meta example={example} />
</div>
<Editor
initialCode={removeJSMeta(example.content)}
key={example.title}
toolBarStart={
<div className={cn("flex items-center")} key={example.slug}>
<a
href={`https://github.com/recho-dev/recho/pull/${example.pull_request}`}
target="_blank"
rel="noreferrer"
className={cn("bg-green-700 text-white rounded-md px-3 py-1 text-sm hover:bg-green-800")}
>
Comment
</a>
</div>
}
/>
<ExampleEditor example={example} initialCode={initialCode} />
</div>
);
}