1a9f9618cSEvan Baconimport { FileCode01Icon, LayoutAlt01Icon, FolderIcon } from '@expo/styleguide-icons'; 2c7504584SBartosz Kaszubowskiimport { HTMLAttributes, ReactNode } from 'react'; 3c7504584SBartosz Kaszubowski 4c7504584SBartosz Kaszubowskitype FileTreeProps = HTMLAttributes<HTMLDivElement> & { 5215a1323SEvan Bacon files?: (string | [string, string])[]; 6c7504584SBartosz Kaszubowski}; 7c7504584SBartosz Kaszubowski 8c7504584SBartosz Kaszubowskitype FileObject = { 9215a1323SEvan Bacon name: string; 10215a1323SEvan Bacon note?: string; 11215a1323SEvan Bacon files: FileObject[]; 12c7504584SBartosz Kaszubowski}; 13c7504584SBartosz Kaszubowski 14c7504584SBartosz Kaszubowskiexport function FileTree({ files = [], ...rest }: FileTreeProps) { 15c7504584SBartosz Kaszubowski return ( 16215a1323SEvan Bacon <div 17215a1323SEvan Bacon className="text-xs border border-default rounded-md bg-default mb-4 p-2 pr-4 pb-4 whitespace-nowrap overflow-x-auto" 18215a1323SEvan Bacon {...rest}> 19c7504584SBartosz Kaszubowski {renderStructure(generateStructure(files))} 20c7504584SBartosz Kaszubowski </div> 21c7504584SBartosz Kaszubowski ); 22c7504584SBartosz Kaszubowski} 23c7504584SBartosz Kaszubowski 24215a1323SEvan Bacon/** 25215a1323SEvan Bacon * Given an array of file paths, generate a tree structure. 26215a1323SEvan Bacon * @param files 27215a1323SEvan Bacon * @returns 28215a1323SEvan Bacon */ 29215a1323SEvan Baconfunction generateStructure(files: (string | [string, string])[]): FileObject[] { 30215a1323SEvan Bacon const structure: FileObject[] = []; 31215a1323SEvan Bacon 32215a1323SEvan Bacon function modifyPath(path: string, note?: string) { 33215a1323SEvan Bacon const parts = path.split('/'); 34215a1323SEvan Bacon let currentLevel = structure; 35215a1323SEvan Bacon parts.forEach((part, index) => { 36215a1323SEvan Bacon const existingPath = currentLevel.find(item => item.name === part); 37215a1323SEvan Bacon if (existingPath) { 38215a1323SEvan Bacon currentLevel = existingPath.files; 39215a1323SEvan Bacon } else { 40215a1323SEvan Bacon const newPart: FileObject = { 41215a1323SEvan Bacon name: part, 42215a1323SEvan Bacon files: [], 43215a1323SEvan Bacon }; 44215a1323SEvan Bacon if (note && index === parts.length - 1) { 45215a1323SEvan Bacon newPart.note = note; 46215a1323SEvan Bacon } 47215a1323SEvan Bacon currentLevel.push(newPart); 48215a1323SEvan Bacon currentLevel = newPart.files; 49215a1323SEvan Bacon } 50215a1323SEvan Bacon }); 51215a1323SEvan Bacon } 52215a1323SEvan Bacon 53215a1323SEvan Bacon files.forEach(path => { 54215a1323SEvan Bacon if (Array.isArray(path)) { 55215a1323SEvan Bacon return modifyPath(path[0], path[1]); 56215a1323SEvan Bacon } else { 57215a1323SEvan Bacon return modifyPath(path); 58215a1323SEvan Bacon } 59215a1323SEvan Bacon }); 60215a1323SEvan Bacon 61c7504584SBartosz Kaszubowski return structure; 62c7504584SBartosz Kaszubowski} 63c7504584SBartosz Kaszubowski 64215a1323SEvan Baconfunction renderStructure(structure: FileObject[], level = 0): ReactNode { 65215a1323SEvan Bacon return structure.map(({ name, note, files }, index) => { 66215a1323SEvan Bacon const FileIcon = getIconForFile(name); 67215a1323SEvan Bacon return files.length ? ( 68215a1323SEvan Bacon <div key={name + '_' + index} className="mt-1 pt-1 pl-2 rounded-sm flex flex-col"> 69c7504584SBartosz Kaszubowski <div className="flex items-center"> 70c7504584SBartosz Kaszubowski {' '.repeat(level)} 71215a1323SEvan Bacon <FolderIcon className="text-icon-tertiary mr-2 opacity-60 min-w-[20px]" /> 72215a1323SEvan Bacon <TextWithNote name={name} note={note} className="text-secondary" /> 73c7504584SBartosz Kaszubowski </div> 74215a1323SEvan Bacon {renderStructure(files, level + 1)} 75c7504584SBartosz Kaszubowski </div> 76c7504584SBartosz Kaszubowski ) : ( 77*920d7a63SBartosz Kaszubowski <div key={name + '_' + index} className="mt-1 pt-1 pl-2 rounded-sm flex items-center"> 78215a1323SEvan Bacon {' '.repeat(Math.max(level, 0))} 79215a1323SEvan Bacon <FileIcon className="text-icon-tertiary mr-2 min-w-[20px]" /> 80215a1323SEvan Bacon <TextWithNote name={name} note={note} className="text-default" /> 81c7504584SBartosz Kaszubowski </div> 82c7504584SBartosz Kaszubowski ); 83c7504584SBartosz Kaszubowski }); 84c7504584SBartosz Kaszubowski} 85a9f9618cSEvan Bacon 86215a1323SEvan Baconfunction TextWithNote({ 87215a1323SEvan Bacon name, 88215a1323SEvan Bacon note, 89215a1323SEvan Bacon className, 90215a1323SEvan Bacon}: { 91215a1323SEvan Bacon name: string; 92215a1323SEvan Bacon note?: string; 93215a1323SEvan Bacon className: string; 94215a1323SEvan Bacon}) { 95215a1323SEvan Bacon return ( 96215a1323SEvan Bacon <span className="flex items-center flex-1"> 97215a1323SEvan Bacon {/* File/folder name */} 98215a1323SEvan Bacon <code className={className}>{name}</code> 99215a1323SEvan Bacon 100215a1323SEvan Bacon {note && ( 101215a1323SEvan Bacon <> 102215a1323SEvan Bacon {/* divider pushing */} 103215a1323SEvan Bacon <span className="flex-1 border-b border-default opacity-60 mx-2 md:mx-3 min-w-[2rem]" /> 104215a1323SEvan Bacon {/* Optional note */} 105215a1323SEvan Bacon <code className="text-default">{note}</code> 106215a1323SEvan Bacon </> 107215a1323SEvan Bacon )} 108215a1323SEvan Bacon </span> 109215a1323SEvan Bacon ); 110215a1323SEvan Bacon} 111215a1323SEvan Bacon 112a9f9618cSEvan Baconfunction getIconForFile(filename: string) { 113a9f9618cSEvan Bacon if (/_layout\.[jt]sx?/.test(filename)) { 114a9f9618cSEvan Bacon return LayoutAlt01Icon; 115a9f9618cSEvan Bacon } 116a9f9618cSEvan Bacon return FileCode01Icon; 117a9f9618cSEvan Bacon} 118