11"use client" ;
2- import { useState , useEffect , useRef , useCallback , useSyncExternalStore } from "react" ;
2+ import { useState , useEffect , useRef , useCallback } from "react" ;
33import { notFound , useRouter } from "next/navigation" ;
4- import { Pencil } from "lucide-react" ;
4+ import { Camera } from "lucide-react" ;
55import { Editor } from "./Editor.jsx" ;
6- import { getNotebookById , createNotebook , addNotebook , saveNotebook , getNotebooks , duplicateNotebook } from "./api.js" ;
7- import { isDirtyStore , countStore } from "./store.js" ;
86import { cn } from "./cn.js" ;
9- import { SafeLink } from "./SafeLink.jsx" ;
107import { BASE_PATH } from "./shared.js" ;
11-
12- const UNSET = Symbol ( "UNSET" ) ;
8+ import { SnapshotsDialog } from "../components/notebooks/SnapshotsDialog.tsx" ;
9+ import { useNotebook } from "@/lib/notebooks/hooks.ts" ;
10+ import { NotebookTitle } from "../components/notebooks/NotebookTitle.tsx" ;
11+ import { EditorPageHero } from "../components/notebooks/EditorPageHero.tsx" ;
1312
1413export function EditorPage ( { id : initialId } ) {
1514 const router = useRouter ( ) ;
16- const [ notebook , setNotebook ] = useState ( UNSET ) ;
17- const [ notebookList , setNotebookList ] = useState ( [ ] ) ;
18- const [ showInput , setShowInput ] = useState ( false ) ;
19- const [ autoRun , setAutoRun ] = useState ( false ) ;
20- const [ id , setId ] = useState ( initialId ) ;
21- const [ initialCode , setInitialCode ] = useState ( null ) ;
22- const [ title , setTitle ] = useState ( "" ) ;
23- const titleRef = useRef ( null ) ;
24- const count = useSyncExternalStore ( countStore . subscribe , countStore . getSnapshot , countStore . getServerSnapshot ) ;
25- const isDirty = useSyncExternalStore (
26- isDirtyStore . subscribe ,
27- isDirtyStore . getSnapshot ,
28- isDirtyStore . getServerSnapshot ,
29- ) ;
30- const prevCount = useRef ( id ? count : null ) ; // Last saved count.
31- const isAdded = prevCount . current === count ; // Whether the notebook is added to the storage.
15+ const {
16+ notebook,
17+ isDraft,
18+ isDirty,
19+ initialContent,
20+ saveNotebook,
21+ updateContent,
22+ updateTitle,
23+ createSnapshot,
24+ restoreContent,
25+ } = useNotebook ( initialId ) ;
26+ const [ showSnapshotsDialog , setShowSnapshotsDialog ] = useState ( false ) ;
3227 const timer = useRef ( null ) ;
3328
3429 const onSave = useCallback ( ( ) => {
35- isDirtyStore . setDirty ( false ) ;
36- if ( isAdded ) {
37- saveNotebook ( notebook ) ;
38- } else {
39- addNotebook ( notebook ) ;
40- prevCount . current = count ;
41- const id = notebook . id ;
42- setId ( id ) ; // Force re-render.
43- window . history . pushState ( null , "" , `${ BASE_PATH } /works/${ id } ` ) ; // Just update the url, no need to reload the page.
44- }
45- // eslint-disable-next-line react-hooks/exhaustive-deps
46- } , [ notebook ] ) ;
47-
48- // This effect is triggered when the count changes,
49- // which happens when user clicks the "New" nav link.
50- useEffect ( ( ) => {
51- const initialNotebook = isAdded ? getNotebookById ( id ) : createNotebook ( ) ;
52- setNotebook ( initialNotebook ) ;
53- setInitialCode ( initialNotebook . content ) ;
54- setAutoRun ( initialNotebook . autoRun ) ;
55- setTitle ( initialNotebook . title ) ;
56- // eslint-disable-next-line react-hooks/exhaustive-deps
57- } , [ count ] ) ;
30+ if ( notebook === null ) return ;
31+ saveNotebook ( ) ;
32+ // Just update the url, no need to reload the page.
33+ const url = `${ BASE_PATH } /works/${ notebook . id } ` ;
34+ window . history . pushState ( null , "" , url ) ;
35+ } , [ notebook , saveNotebook ] ) ;
5836
5937 useEffect ( ( ) => {
6038 // Use setTimeout to avoid changing to default title.
61- setTimeout ( ( ) => {
62- document . title = `${ isAdded ? notebook . title : "New" } | Recho Notebook` ;
63- } , 100 ) ;
64- } , [ notebook , isAdded ] ) ;
65-
66- useEffect ( ( ) => {
67- const notebooks = getNotebooks ( ) ;
68- setNotebookList ( notebooks . slice ( 0 , 4 ) ) ;
69- } , [ isAdded ] ) ;
39+ if ( notebook ) {
40+ setTimeout ( ( ) => {
41+ document . title = `${ isDirty ? "* " : "" } ${ isDraft ? "New" : notebook . title } | Recho Notebook` ;
42+ } , 100 ) ;
43+ }
44+ } , [ notebook , isDirty , isDraft ] ) ;
7045
7146 useEffect ( ( ) => {
7247 const onBeforeUnload = ( e ) => {
@@ -84,57 +59,13 @@ export function EditorPage({id: initialId}) {
8459 return ( ) => window . removeEventListener ( "keydown" , onKeyDown ) ;
8560 } , [ onSave ] ) ;
8661
87- useEffect ( ( ) => {
88- if ( showInput ) titleRef . current . focus ( ) ;
89- } , [ showInput ] ) ;
90-
91- if ( notebook === UNSET ) return < div className = { cn ( "max-w-screen-lg mx-auto my-10 editor-page" ) } > Loading...</ div > ;
92-
9362 if ( ! notebook ) return notFound ( ) ;
9463
95- function onUserInput ( code ) {
96- const newNotebook = { ...notebook , content : code } ;
97- if ( isAdded ) {
98- saveNotebook ( newNotebook ) ;
99- setNotebook ( newNotebook ) ;
100- } else {
101- setNotebook ( newNotebook ) ;
102- isDirtyStore . setDirty ( true ) ;
103- }
104- }
105-
106- function onRename ( ) {
107- setShowInput ( true ) ;
108- setTitle ( notebook . title ) ;
109- }
110-
111- // Only submit rename when blur with valid title.
112- function onTitleBlur ( ) {
113- setShowInput ( false ) ;
114- // The title can't be empty.
115- if ( ! title ) return setTitle ( notebook . title ) ;
116- const newNotebook = { ...notebook , title} ;
117- setNotebook ( newNotebook ) ;
118- if ( isAdded ) saveNotebook ( newNotebook ) ;
119- else isDirtyStore . setDirty ( true ) ;
120- }
121-
122- function onTitleChange ( e ) {
123- setTitle ( e . target . value ) ;
124- }
125-
126- function onTitleKeyDown ( e ) {
127- if ( e . key === "Enter" ) {
128- onTitleBlur ( ) ;
129- titleRef . current . blur ( ) ;
130- }
131- }
132-
13364 // If long-running code is detected, set autoRun to false
13465 // to prevent the browser from freezing on infinite loops
13566 // and can't continue to edit the code.
13667 function onBeforeEachRun ( ) {
137- if ( ! isAdded ) return ;
68+ if ( isDraft ) return ;
13869 if ( timer . current ) clearTimeout ( timer . current ) ;
13970 const newNotebook = { ...notebook , autoRun : false } ;
14071 saveNotebook ( newNotebook ) ;
@@ -151,98 +82,54 @@ export function EditorPage({id: initialId}) {
15182 router . push ( `/works/${ duplicated . id } ` ) ;
15283 }
15384
85+ function onRestoreSnapshot ( snapshot ) {
86+ restoreContent ( snapshot . content ) ;
87+ }
88+
15489 return (
15590 < div >
156- { ! isAdded && notebookList . length > 0 && (
157- < div className = { cn ( "flex h-[72px] bg-gray-100 p-2 w-full border-b border-gray-200" ) } >
158- < div
159- className = { cn (
160- "flex items-center justify-between gap-2 h-full max-w-screen-lg lg:mx-auto mx-4 w-full hidden md:flex" ,
161- ) }
162- >
163- { notebookList . map ( ( notebook ) => (
164- < div key = { notebook . id } className = { cn ( "flex items-start flex-col gap-1" ) } >
165- < SafeLink
166- href = { `/works/${ notebook . id } ` }
167- className = { cn (
168- "font-semibold hover:underline text-blue-500 whitespace-nowrap line-clamp-1 max-w-[150px] text-ellipsis" ,
169- ) }
170- >
171- { notebook . title }
172- </ SafeLink >
173- < span
174- className = { cn ( "text-xs text-gray-500 line-clamp-1 whitespace-nowrap max-w-[150px] text-ellipsis" ) }
175- >
176- Created { new Date ( notebook . created ) . toLocaleDateString ( ) }
177- </ span >
178- </ div >
179- ) ) }
180- < SafeLink href = "/works" className = { cn ( "font-semibold text-blue-500 hover:underline" ) } >
181- View your notebooks
182- </ SafeLink >
183- </ div >
184- < div
185- className = { cn (
186- "flex items-center justify-between gap-2 h-full max-w-screen-lg lg:mx-auto mx-4 w-full md:hidden" ,
187- ) }
188- >
189- < SafeLink
190- href = "/works"
191- className = { cn (
192- "font-medium w-full border border-gray-300 rounded-md px-3 py-2 text-sm text-center hover:bg-gray-200" ,
193- ) }
194- >
195- View your notebooks
196- </ SafeLink >
197- </ div >
198- </ div >
199- ) }
200- { ! isAdded && notebookList . length === 0 && (
201- < div className = { cn ( "flex items-center justify-center h-[72px] mt-6 mb-10 lg:mb-0" ) } >
202- < p className = { cn ( "text-3xl text-gray-800 font-light text-center mx-10" ) } >
203- Explore code and art with instant feedback.
204- </ p >
205- </ div >
206- ) }
91+ < EditorPageHero show = { isDraft } />
20792 < div className = { cn ( "max-w-screen-lg lg:mx-auto mx-4 lg:my-10 my-4 editor-page" ) } >
20893 < Editor
209- initialCode = { initialCode }
94+ initialCode = { initialContent }
21095 key = { notebook . id }
211- onUserInput = { onUserInput }
96+ onUserInput = { updateContent }
21297 onBeforeEachRun = { onBeforeEachRun }
213- autoRun = { autoRun }
214- onDuplicate = { isAdded ? onDuplicate : null }
98+ autoRun = { notebook . autoRun }
99+ onDuplicate = { isDraft ? onDuplicate : null }
215100 toolBarStart = {
216- < div className = { cn ( "flex items-center gap-2" ) } >
217- { ! isAdded && (
218- < button
219- onClick = { onSave }
220- className = { cn ( "bg-green-700 text-white rounded-md px-3 py-1 text-sm hover:bg-green-800" ) }
221- >
222- Create
223- </ button >
224- ) }
225- { ! showInput && isAdded && (
226- < button onClick = { onRename } >
227- < Pencil className = "w-4 h-4" />
228- </ button >
229- ) }
230- { showInput || ! isAdded ? (
231- < input
232- type = "text"
233- value = { title }
234- onChange = { onTitleChange }
235- onBlur = { onTitleBlur }
236- onKeyDown = { onTitleKeyDown }
237- ref = { titleRef }
238- className = { cn ( "border border-gray-200 rounded-md px-3 py-1 text-sm bg-white" ) }
239- />
240- ) : (
241- < span className = { cn ( "text-sm py-1 border border-gray-100 rounded-md" ) } > { notebook . title } </ span >
242- ) }
243- </ div >
101+ < NotebookTitle
102+ title = { notebook . title }
103+ setTitle = { updateTitle }
104+ isDraft = { isDraft }
105+ isDirty = { isDirty }
106+ onCreate = { onSave }
107+ />
108+ }
109+ toolBarEnd = {
110+ isDraft ? null : (
111+ < button
112+ onClick = { ( ) => setShowSnapshotsDialog ( true ) }
113+ className = { cn (
114+ "flex items-center gap-1.5 px-3 py-1.5 text-sm border border-gray-300 rounded-md hover:bg-gray-200 transition-colors" ,
115+ ) }
116+ data-tooltip-id = "action-tooltip"
117+ data-tooltip-content = "Manage Snapshots"
118+ data-tooltip-place = "bottom"
119+ >
120+ < Camera className = { cn ( "w-4 h-4" ) } />
121+ < span > Snapshots</ span >
122+ </ button >
123+ )
244124 }
245125 />
126+ < SnapshotsDialog
127+ notebook = { notebook }
128+ open = { showSnapshotsDialog }
129+ createSnapshot = { createSnapshot }
130+ onOpenChange = { setShowSnapshotsDialog }
131+ onRestore = { onRestoreSnapshot }
132+ />
246133 </ div >
247134 </ div >
248135 ) ;
0 commit comments