1import { 2 exportPatterns, 3 importPatterns, 4 loadAndSetFeaturedPatterns, 5 loadAndSetPublicPatterns, 6 patternFilterName, 7 useActivePattern, 8 useViewingPatternData, 9 userPattern, 10} from '../../../user_pattern_utils.mjs'; 11import { useMemo, useRef } from 'react'; 12import { getMetadata } from '../../../metadata_parser.js'; 13import { useExamplePatterns } from '../../useExamplePatterns.jsx'; 14import { parseJSON, isUdels } from '../../util.mjs'; 15import { useSettings } from '../../../settings.mjs'; 16import { ActionButton } from '../button/action-button.jsx'; 17import { Pagination } from '../pagination/Pagination.jsx'; 18import { useState } from 'react'; 19import { useDebounce } from '../usedebounce.jsx'; 20import cx from '@src/cx.mjs'; 21import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx'; 22 23export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) { 24 const meta = useMemo(() => getMetadata(pattern.code), [pattern]); 25 26 let title = meta.title; 27 if (title == null) { 28 const date = new Date(pattern.created_at); 29 if (!isNaN(date)) { 30 title = date.toLocaleDateString(); 31 } else { 32 title = pattern.id || 'unnamed'; 33 } 34 } 35 36 const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'; 37 return <>{`${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>; 38} 39 40function PatternButton({ showOutline, onClick, pattern, showHiglight }) { 41 return ( 42 <a 43 className={cx( 44 'mr-4 hover:opacity-50 cursor-pointer block', 45 showOutline && 'outline outline-1', 46 showHiglight && 'ring-selection', 47 )} 48 onClick={onClick} 49 > 50 <PatternLabel pattern={pattern} /> 51 </a> 52 ); 53} 54 55function PatternButtons({ patterns, activePattern, onClick, started }) { 56 const viewingPatternData = useViewingPatternData(); 57 const viewingPatternID = viewingPatternData.id; 58 return ( 59 <div className="p-2"> 60 {Object.values(patterns) 61 .reverse() 62 .map((pattern) => { 63 const id = pattern.id; 64 return ( 65 <PatternButton 66 pattern={pattern} 67 key={id} 68 showHiglight={id === viewingPatternID} 69 showOutline={id === activePattern && started} 70 onClick={() => onClick(id)} 71 /> 72 ); 73 })} 74 </div> 75 ); 76} 77 78const updateCodeWindow = (context, patternData, reset = false) => { 79 context.handleUpdate(patternData, reset); 80}; 81 82export function PatternsTab({ context }) { 83 const [search, setSearch] = useState(''); 84 const activePattern = useActivePattern(); 85 const viewingPatternData = useViewingPatternData(); 86 87 const { userPatterns, patternAutoStart } = useSettings(); 88 const viewingPatternID = viewingPatternData?.id; 89 90 const visiblePatterns = useMemo(() => { 91 if (!search) { 92 return userPatterns; 93 } 94 return Object.fromEntries( 95 Object.entries(userPatterns).filter(([_key, pattern]) => { 96 const meta = getMetadata(pattern.code); 97 98 // Search for specific meta keys 99 const searchLowercaseTrimmed = search.trim().toLowerCase(); 100 if (searchLowercaseTrimmed.includes(':')) { 101 const [metaKey, metaSearch] = searchLowercaseTrimmed.split(/:\s*/); 102 if (metaKey !== undefined && metaSearch !== undefined && metaKey in meta) { 103 const metaValues = meta[metaKey]; 104 if (Array.isArray(metaValues)) { 105 return metaValues.some((metaValue) => metaValue.toLowerCase().includes(metaSearch)); 106 } else if (typeof metaValues === 'string') { 107 return metaValues.toLowerCase().includes(metaSearch); 108 } else { 109 return false; 110 } 111 } 112 } 113 const title = meta.title ? meta.title : 'unnamed'; 114 const authors = meta.by ? meta.by : ['anonymous']; 115 const tags = meta.tag ? meta.tag : []; 116 return ( 117 title.toLowerCase().includes(searchLowercaseTrimmed) || 118 authors.some((author) => author.toLowerCase().includes(searchLowercaseTrimmed)) || 119 tags.some((tag) => tag.toLowerCase().includes(searchLowercaseTrimmed)) 120 ); 121 }), 122 ); 123 }, [search, userPatterns]); 124 125 const importRef = useRef(); 126 return ( 127 <div className="w-full h-full text-foreground flex flex-col overflow-hidden"> 128 <Textbox className="w-full border-0" placeholder="Search..." value={search} onChange={setSearch} /> 129 <div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-y border-muted"> 130 <ActionButton 131 label="new" 132 onClick={() => { 133 const { data } = userPattern.createAndAddToDB(); 134 updateCodeWindow(context, data); 135 }} 136 /> 137 <ActionButton 138 label="duplicate" 139 onClick={() => { 140 const { data } = userPattern.duplicate(viewingPatternData); 141 updateCodeWindow(context, data); 142 }} 143 /> 144 <ActionButton 145 label="delete" 146 onClick={() => { 147 const { data } = userPattern.delete(viewingPatternID); 148 updateCodeWindow(context, { ...data, collection: userPattern.collection }); 149 }} 150 /> 151 <input 152 ref={importRef} 153 style={{ display: 'none' }} 154 type="file" 155 multiple 156 accept="text/plain,text/x-markdown,application/json" 157 onChange={(e) => importPatterns(e.target.files)} 158 /> 159 <ActionButton label="import" onClick={() => importRef.current.click()} /> 160 <ActionButton label="export" onClick={exportPatterns} /> 161 162 <ActionButton 163 label="delete-all" 164 onClick={() => { 165 const { data } = userPattern.clearAll(); 166 updateCodeWindow(context, data); 167 }} 168 /> 169 </div> 170 171 <div className="overflow-auto"> 172 {/* bg-background */} 173 {/* {patternFilter === patternFilterName.user && ( */} 174 <PatternButtons 175 onClick={(id) => { 176 updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart); 177 178 if (context.started && activePattern === id) { 179 context.handleEvaluate(); 180 } 181 }} 182 patterns={visiblePatterns} 183 started={context.started} 184 activePattern={activePattern} 185 viewingPatternID={viewingPatternID} 186 /> 187 {/* )} */} 188 </div> 189 </div> 190 ); 191} 192 193function PatternPageWithPagination({ patterns, patternOnClick, context, paginationOnChange, initialPage }) { 194 const [page, setPage] = useState(initialPage); 195 const debouncedPageChange = useDebounce(() => { 196 paginationOnChange(page); 197 }); 198 199 const onPageChange = (pageNum) => { 200 setPage(pageNum); 201 debouncedPageChange(); 202 }; 203 204 const activePattern = useActivePattern(); 205 return ( 206 <div className="flex flex-grow flex-col h-full overflow-hidden justify-between"> 207 <div className="overflow-auto flex flex-col flex-grow bg-background p-2 rounded-md "> 208 <PatternButtons 209 onClick={(id) => patternOnClick(id)} 210 started={context.started} 211 patterns={patterns} 212 activePattern={activePattern} 213 /> 214 </div> 215 <div className="flex items-center gap-2 py-2"> 216 <label htmlFor="pattern pagination">Page</label> 217 <Pagination id="pattern pagination" currPage={page} onPageChange={onPageChange} /> 218 </div> 219 </div> 220 ); 221} 222 223let featuredPageNum = 1; 224function FeaturedPatterns({ context }) { 225 const examplePatterns = useExamplePatterns(); 226 const collections = examplePatterns.collections; 227 const patterns = collections.get(patternFilterName.featured); 228 const { patternAutoStart } = useSettings(); 229 return ( 230 <PatternPageWithPagination 231 patterns={patterns} 232 context={context} 233 initialPage={featuredPageNum} 234 patternOnClick={(id) => { 235 updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.featured }, patternAutoStart); 236 }} 237 paginationOnChange={async (pageNum) => { 238 await loadAndSetFeaturedPatterns(pageNum - 1); 239 featuredPageNum = pageNum; 240 }} 241 /> 242 ); 243} 244 245let latestPageNum = 1; 246function LatestPatterns({ context }) { 247 const examplePatterns = useExamplePatterns(); 248 const collections = examplePatterns.collections; 249 const patterns = collections.get(patternFilterName.public); 250 const { patternAutoStart } = useSettings(); 251 return ( 252 <PatternPageWithPagination 253 patterns={patterns} 254 context={context} 255 initialPage={latestPageNum} 256 patternOnClick={(id) => { 257 updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, patternAutoStart); 258 }} 259 paginationOnChange={async (pageNum) => { 260 await loadAndSetPublicPatterns(pageNum - 1); 261 latestPageNum = pageNum; 262 }} 263 /> 264 ); 265} 266 267function PublicPatterns({ context }) { 268 const { patternFilter } = useSettings(); 269 if (patternFilter === patternFilterName.featured) { 270 return <FeaturedPatterns context={context} />; 271 } 272 return <LatestPatterns context={context} />; 273}