@@ -3,6 +3,13 @@ import { FileNode, FolderNode } from "../types";
33import { api } from "../services/api" ;
44import "./PageList.css" ;
55
6+ type TemplateOption = {
7+ path : string ;
8+ template : string ;
9+ autoname : boolean ;
10+ content : string ;
11+ } ;
12+
613type SortField = "name" | "createdAt" | "modifiedAt" ;
714type SortDirection = "asc" | "desc" ;
815
@@ -44,6 +51,10 @@ export const PageList: React.FC<PageListProps> = ({
4451 const [ newPageName , setNewPageName ] = useState ( "" ) ;
4552 const [ movingPage , setMovingPage ] = useState < string | null > ( null ) ;
4653 const [ isCreating , setIsCreating ] = useState ( false ) ;
54+ const [ showTemplatePicker , setShowTemplatePicker ] = useState ( false ) ;
55+ const [ templates , setTemplates ] = useState < TemplateOption [ ] > ( [ ] ) ;
56+ const [ templatesLoading , setTemplatesLoading ] = useState ( false ) ;
57+ const [ templatesError , setTemplatesError ] = useState < string | null > ( null ) ;
4758 const [ isDeleting , setIsDeleting ] = useState < string | null > ( null ) ;
4859 const [ isMoving , setIsMoving ] = useState ( false ) ;
4960 const [ sortField , setSortField ] = useState < SortField > ( "name" ) ;
@@ -141,27 +152,113 @@ export const PageList: React.FC<PageListProps> = ({
141152 }
142153 } ;
143154
144- const handleCreatePage = async ( ) => {
145- // Allow creating pages in root (selectedFolder can be null or "/")
155+ const getSelectedFolderPath = ( ) : string => {
156+ return ! selectedFolder || selectedFolder === "/" ? "" : selectedFolder ;
157+ } ;
158+
159+ const promptForPageName = ( ) : string | null => {
146160 const pageName = prompt ( "Enter page name (without .md extension):" ) ;
147- if ( pageName ) {
148- setIsCreating ( true ) ;
149- try {
150- const fileName = pageName . endsWith ( ".md" ) ? pageName : `${ pageName } .md` ;
151- const pagePath =
152- ! selectedFolder || selectedFolder === "/" ? fileName : `${ selectedFolder } /${ fileName } ` ;
153- await api . createPage ( pagePath , `# ${ pageName } \n\nStart writing...` ) ;
154- loadPages ( ) ;
155- onRefresh ( ) ;
156- } catch ( err ) {
157- console . error ( "Failed to create page:" , err ) ;
158- setError ( "Failed to create page. Please try again." ) ;
159- } finally {
160- setIsCreating ( false ) ;
161- }
161+ if ( ! pageName ) return null ;
162+ return pageName . trim ( ) ? pageName . trim ( ) : null ;
163+ } ;
164+
165+ const buildPagePath = ( folderPath : string , fileName : string ) : string => {
166+ return folderPath ? `${ folderPath } /${ fileName } ` : fileName ;
167+ } ;
168+
169+ const formatTimestamp = ( date : Date ) : string => {
170+ const pad2 = ( n : number ) => String ( n ) . padStart ( 2 , "0" ) ;
171+ const y = date . getFullYear ( ) ;
172+ const m = pad2 ( date . getMonth ( ) + 1 ) ;
173+ const d = pad2 ( date . getDate ( ) ) ;
174+ const hh = pad2 ( date . getHours ( ) ) ;
175+ const mm = pad2 ( date . getMinutes ( ) ) ;
176+ return `${ y } -${ m } -${ d } -${ hh } -${ mm } ` ;
177+ } ;
178+
179+ const slugFromTemplateName = ( name : string ) : string => {
180+ return name
181+ . trim ( )
182+ . replace ( / \s + / g, "-" )
183+ . replace ( / [ ^ A - Z a - z 0 - 9 . _ - ] / g, "" )
184+ . replace ( / - + / g, "-" ) ;
185+ } ;
186+
187+ const ensureMd = ( name : string ) : string => {
188+ return name . endsWith ( ".md" ) ? name : `${ name } .md` ;
189+ } ;
190+
191+ const openTemplatePicker = async ( ) => {
192+ setShowTemplatePicker ( true ) ;
193+ setTemplatesError ( null ) ;
194+ setTemplatesLoading ( true ) ;
195+ try {
196+ const data = await api . getTemplates ( ) ;
197+ setTemplates ( data ) ;
198+ } catch ( err ) {
199+ console . error ( "Failed to load templates:" , err ) ;
200+ setTemplatesError ( "Failed to load templates." ) ;
201+ setTemplates ( [ ] ) ;
202+ } finally {
203+ setTemplatesLoading ( false ) ;
204+ }
205+ } ;
206+
207+ const createFromBlank = async ( ) => {
208+ const pageName = promptForPageName ( ) ;
209+ if ( ! pageName ) return ;
210+
211+ setIsCreating ( true ) ;
212+ try {
213+ const folderPath = getSelectedFolderPath ( ) ;
214+ const fileName = ensureMd ( pageName ) ;
215+ const pagePath = buildPagePath ( folderPath , fileName ) ;
216+ await api . createPage ( pagePath , `# ${ pageName } \n\nStart writing...` ) ;
217+ await loadPages ( ) ;
218+ onRefresh ( ) ;
219+ onSelectPage ( pagePath ) ;
220+ } catch ( err ) {
221+ console . error ( "Failed to create page:" , err ) ;
222+ setError ( "Failed to create page. Please try again." ) ;
223+ } finally {
224+ setIsCreating ( false ) ;
225+ }
226+ } ;
227+
228+ const createFromTemplate = async ( t : TemplateOption ) => {
229+ const folderPath = getSelectedFolderPath ( ) ;
230+
231+ let fileName : string | null ;
232+ if ( t . autoname ) {
233+ const slug = slugFromTemplateName ( t . template ) ;
234+ const stamp = formatTimestamp ( new Date ( ) ) ;
235+ fileName = ensureMd ( `${ slug } -${ stamp } ` ) ;
236+ } else {
237+ const pageName = promptForPageName ( ) ;
238+ if ( ! pageName ) return ;
239+ fileName = ensureMd ( pageName ) ;
240+ }
241+
242+ const pagePath = buildPagePath ( folderPath , fileName ) ;
243+
244+ setIsCreating ( true ) ;
245+ try {
246+ await api . createPage ( pagePath , t . content ) ;
247+ await loadPages ( ) ;
248+ onRefresh ( ) ;
249+ onSelectPage ( pagePath ) ;
250+ } catch ( err ) {
251+ console . error ( "Failed to create page from template:" , err ) ;
252+ setError ( "Failed to create page. Please try again." ) ;
253+ } finally {
254+ setIsCreating ( false ) ;
162255 }
163256 } ;
164257
258+ const handleCreatePage = async ( ) => {
259+ await openTemplatePicker ( ) ;
260+ } ;
261+
165262 const handleDeletePage = async ( path : string , e : React . MouseEvent ) => {
166263 e . stopPropagation ( ) ;
167264 if ( confirm ( `Delete page "${ path } "?` ) ) {
@@ -474,6 +571,70 @@ export const PageList: React.FC<PageListProps> = ({
474571 </ div >
475572 </ div >
476573 ) }
574+
575+ { /* Template Picker Modal */ }
576+ { showTemplatePicker && (
577+ < div
578+ className = "modal-overlay"
579+ onClick = { ( ) => ! isCreating && setShowTemplatePicker ( false ) }
580+ role = "dialog"
581+ aria-modal = "true"
582+ aria-label = "Create page from template"
583+ >
584+ < div className = "modal" onClick = { ( e ) => e . stopPropagation ( ) } >
585+ < h3 > Create New Page</ h3 >
586+ < p > Choose a template</ p >
587+
588+ { templatesLoading ? (
589+ < div className = "modal-loading" role = "status" aria-label = "Loading templates" >
590+ < div className = "loading-spinner" aria-hidden = "true" > </ div >
591+ < span > Loading templates...</ span >
592+ </ div >
593+ ) : templatesError ? (
594+ < div className = "error-state" role = "alert" >
595+ < span className = "error-icon" aria-hidden = "true" >
596+ ⚠️
597+ </ span >
598+ < span > { templatesError } </ span >
599+ </ div >
600+ ) : (
601+ < div className = "folder-list" role = "list" >
602+ < button
603+ className = "folder-option"
604+ onClick = { async ( ) => {
605+ setShowTemplatePicker ( false ) ;
606+ await createFromBlank ( ) ;
607+ } }
608+ disabled = { isCreating }
609+ >
610+ Blank page
611+ </ button >
612+ { templates . map ( ( t ) => (
613+ < button
614+ key = { t . path }
615+ className = "folder-option"
616+ onClick = { async ( ) => {
617+ setShowTemplatePicker ( false ) ;
618+ await createFromTemplate ( t ) ;
619+ } }
620+ disabled = { isCreating }
621+ >
622+ { t . template }
623+ </ button >
624+ ) ) }
625+ </ div >
626+ ) }
627+
628+ < button
629+ className = "cancel-btn"
630+ onClick = { ( ) => setShowTemplatePicker ( false ) }
631+ disabled = { isCreating }
632+ >
633+ Cancel
634+ </ button >
635+ </ div >
636+ </ div >
637+ ) }
477638 </ nav >
478639 ) ;
479640} ;
0 commit comments