1- import { useCallback , useEffect , useState } from 'react' ;
1+ import { useCallback , useEffect , useRef , useState } from 'react' ;
22import { Editor } from './components/Editor' ;
3- import { getFile , putFile , FileApiError } from './api/client' ;
3+ import { Sidebar } from './components/Sidebar' ;
4+ import { useHashRoute } from './hooks/useHashRoute' ;
5+ import { useTree } from './hooks/useTree' ;
46import { useDebouncedEffect } from './hooks/useDebouncedEffect' ;
7+ import { buildTree , todayDailyPath , todayHeading } from './utils/tree' ;
8+ import {
9+ getFile ,
10+ putFile ,
11+ deleteFile ,
12+ moveFile ,
13+ fileExists ,
14+ FileApiError
15+ } from './api/client' ;
516
6- const HARDCODED_PATH = 'scratch.md' ;
717const SAVE_DEBOUNCE_MS = 500 ;
18+ const DEFAULT_NEW_NOTE_PATH = 'pages/untitled.md' ;
819
920type SaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error' ;
1021
1122export function App ( ) {
23+ const [ path , navigate ] = useHashRoute ( ) ;
24+ const { entries, error : treeError , reload : reloadTree } = useTree ( ) ;
25+
1226 const [ content , setContent ] = useState < string > ( '' ) ;
1327 const [ loaded , setLoaded ] = useState ( false ) ;
1428 const [ loadError , setLoadError ] = useState < string | null > ( null ) ;
1529 const [ saveStatus , setSaveStatus ] = useState < SaveStatus > ( 'idle' ) ;
1630 const [ saveError , setSaveError ] = useState < string | null > ( null ) ;
1731
32+ // Track which path the in-memory `content` belongs to. Without this,
33+ // a hash change can fire a stale debounced save against the new path.
34+ const loadedPathRef = useRef < string | null > ( null ) ;
35+
36+ // First-load default: navigate to today's daily note.
1837 useEffect ( ( ) => {
38+ if ( path === '' ) navigate ( todayDailyPath ( ) ) ;
39+ // Only on mount.
40+ // eslint-disable-next-line react-hooks/exhaustive-deps
41+ } , [ ] ) ;
42+
43+ // Load the file whenever the path changes.
44+ useEffect ( ( ) => {
45+ if ( ! path ) return ;
1946 let cancelled = false ;
47+ setLoaded ( false ) ;
48+ setLoadError ( null ) ;
49+ setSaveStatus ( 'idle' ) ;
2050 ( async ( ) => {
2151 try {
22- const { content : loadedContent } = await getFile ( HARDCODED_PATH ) ;
23- if ( ! cancelled ) {
24- setContent ( loadedContent ) ;
25- setLoaded ( true ) ;
26- }
52+ const { content : c } = await getFile ( path ) ;
53+ if ( cancelled ) return ;
54+ loadedPathRef . current = path ;
55+ setContent ( c ) ;
56+ setLoaded ( true ) ;
2757 } catch ( err ) {
2858 if ( cancelled ) return ;
2959 if ( err instanceof FileApiError && err . status === 404 ) {
30- setContent ( '# Welcome to BizerOS Knowledge\n\nStart typing — your changes auto-save.\n' ) ;
60+ // Missing file: seed with today's heading for daily notes, otherwise empty.
61+ const seed = path === todayDailyPath ( ) ? todayHeading ( ) : '' ;
62+ loadedPathRef . current = path ;
63+ setContent ( seed ) ;
3164 setLoaded ( true ) ;
65+ if ( seed !== '' ) setSaveStatus ( 'pending' ) ;
3266 } else {
3367 setLoadError ( err instanceof Error ? err . message : String ( err ) ) ;
3468 }
@@ -37,7 +71,7 @@ export function App() {
3771 return ( ) => {
3872 cancelled = true ;
3973 } ;
40- } , [ ] ) ;
74+ } , [ path ] ) ;
4175
4276 const handleChange = useCallback ( ( next : string ) => {
4377 setContent ( next ) ;
@@ -47,15 +81,18 @@ export function App() {
4781 useDebouncedEffect (
4882 ( ) => {
4983 if ( ! loaded || saveStatus !== 'pending' ) return ;
84+ const targetPath = loadedPathRef . current ;
85+ if ( ! targetPath || targetPath !== path ) return ;
5086 let cancelled = false ;
87+ const wasNew = entries ?. some ( ( e ) => e . path === targetPath ) === false ;
5188 ( async ( ) => {
5289 setSaveStatus ( 'saving' ) ;
5390 try {
54- await putFile ( HARDCODED_PATH , content ) ;
55- if ( ! cancelled ) {
56- setSaveStatus ( 'saved' ) ;
57- setSaveError ( null ) ;
58- }
91+ await putFile ( targetPath , content ) ;
92+ if ( cancelled ) return ;
93+ setSaveStatus ( 'saved' ) ;
94+ setSaveError ( null ) ;
95+ if ( wasNew ) reloadTree ( ) ;
5996 } catch ( err ) {
6097 if ( cancelled ) return ;
6198 setSaveStatus ( 'error' ) ;
@@ -66,33 +103,106 @@ export function App() {
66103 cancelled = true ;
67104 } ;
68105 } ,
69- [ content , loaded , saveStatus ] ,
106+ [ content , loaded , saveStatus , path ] ,
70107 SAVE_DEBOUNCE_MS
71108 ) ;
72109
73- if ( loadError ) {
74- return (
75- < div className = "app-error" >
76- < h2 > Failed to load { HARDCODED_PATH } </ h2 >
77- < pre > { loadError } </ pre >
78- </ div >
79- ) ;
80- }
110+ const handleNewNote = useCallback ( async ( ) => {
111+ const input = window . prompt ( 'New note path (relative to /brain):' , DEFAULT_NEW_NOTE_PATH ) ;
112+ if ( ! input ) return ;
113+ const target = input . trim ( ) . replace ( / ^ \/ + / , '' ) ;
114+ if ( ! target ) return ;
115+ const finalPath = target . endsWith ( '.md' ) ? target : `${ target } .md` ;
116+ try {
117+ const exists = await fileExists ( finalPath ) ;
118+ if ( ! exists ) await putFile ( finalPath , '' ) ;
119+ await reloadTree ( ) ;
120+ navigate ( finalPath ) ;
121+ } catch ( err ) {
122+ window . alert ( `Failed to create: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
123+ }
124+ } , [ navigate , reloadTree ] ) ;
81125
82- if ( ! loaded ) {
83- return < div className = "app-loading" > Loading…</ div > ;
84- }
126+ const handleTodayNote = useCallback ( async ( ) => {
127+ const target = todayDailyPath ( ) ;
128+ try {
129+ const exists = await fileExists ( target ) ;
130+ if ( ! exists ) {
131+ await putFile ( target , todayHeading ( ) ) ;
132+ await reloadTree ( ) ;
133+ }
134+ navigate ( target ) ;
135+ } catch ( err ) {
136+ window . alert ( `Failed to open today's note: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
137+ }
138+ } , [ navigate , reloadTree ] ) ;
139+
140+ const handleRename = useCallback (
141+ async ( oldPath : string ) => {
142+ const input = window . prompt ( `Rename "${ oldPath } " to:` , oldPath ) ;
143+ if ( ! input ) return ;
144+ const target = input . trim ( ) . replace ( / ^ \/ + / , '' ) ;
145+ if ( ! target || target === oldPath ) return ;
146+ try {
147+ await moveFile ( oldPath , target ) ;
148+ await reloadTree ( ) ;
149+ if ( path === oldPath ) navigate ( target ) ;
150+ } catch ( err ) {
151+ window . alert ( `Rename failed: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
152+ }
153+ } ,
154+ [ navigate , path , reloadTree ]
155+ ) ;
156+
157+ const handleDelete = useCallback (
158+ async ( target : string ) => {
159+ if ( ! window . confirm ( `Delete "${ target } "? This cannot be undone.` ) ) return ;
160+ try {
161+ await deleteFile ( target ) ;
162+ await reloadTree ( ) ;
163+ if ( path === target ) navigate ( '' ) ;
164+ } catch ( err ) {
165+ window . alert ( `Delete failed: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
166+ }
167+ } ,
168+ [ navigate , path , reloadTree ]
169+ ) ;
170+
171+ const tree = entries === null ? null : buildTree ( entries ) ;
85172
86173 return (
87174 < div className = "app" >
88175 < header className = "app-header" >
89176 < span className = "app-title" > BizerOS Knowledge</ span >
90- < span className = "app-path" > /brain/{ HARDCODED_PATH } </ span >
177+ < span className = "app-path" > /brain/{ path || '(no file selected)' } </ span >
91178 < SaveIndicator status = { saveStatus } error = { saveError } />
92179 </ header >
93- < main className = "app-main" >
94- < Editor value = { content } onChange = { handleChange } />
95- </ main >
180+ < div className = "app-body" >
181+ < Sidebar
182+ tree = { tree }
183+ treeError = { treeError }
184+ currentPath = { path }
185+ onSelect = { ( p ) => navigate ( p ) }
186+ onNewNote = { handleNewNote }
187+ onTodayNote = { handleTodayNote }
188+ onRename = { handleRename }
189+ onDelete = { handleDelete }
190+ />
191+ < main className = "app-main" >
192+ { loadError ? (
193+ < div className = "app-error" >
194+ < h2 > Failed to load { path } </ h2 >
195+ < pre > { loadError } </ pre >
196+ </ div >
197+ ) : ! path ? (
198+ < div className = "app-empty" > Pick a note from the sidebar, or click "Today".</ div >
199+ ) : ! loaded ? (
200+ < div className = "app-loading" > Loading…</ div >
201+ ) : (
202+ < Editor value = { content } onChange = { handleChange } />
203+ ) }
204+ </ main >
205+ </ div >
96206 </ div >
97207 ) ;
98208}
0 commit comments