jevstrudel.git / website / src / repl / components / panel / PatternsTab.jsx
PatternsTab.jsx273 lines · 8.9 KB · raw
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}