jevstrudel.git / website / src / user_pattern_utils.mjs
1import { atom, computed } from 'nanostores';
2import { useStore } from '@nanostores/react';
3import { logger } from '@strudel/core';
4import { nanoid } from 'nanoid';
5import { settingsMap } from './settings.mjs';
6import { confirmDialog, parseJSON, supabase } from './repl/util.mjs';
7
8export let $publicPatterns = atom([]);
9export let $featuredPatterns = atom([]);
10
11const patternQueryLimit = 20;
12export const patternFilterName = {
13  public: 'latest',
14  featured: 'featured',
15  user: 'user',
16  // stock: 'stock examples',
17};
18
19const sessionAtom = (name, initial = undefined) => {
20  const storage = typeof sessionStorage !== 'undefined' ? sessionStorage : {};
21  const store = atom(typeof storage[name] !== 'undefined' ? storage[name] : initial);
22  store.listen((newValue) => {
23    if (typeof newValue === 'undefined') {
24      delete storage[name];
25    } else {
26      storage[name] = newValue;
27    }
28  });
29  return store;
30};
31
32export let $viewingPatternData = sessionAtom('viewingPatternData', {
33  id: '',
34  code: '',
35  collection: patternFilterName.user,
36  created_at: Date.now(),
37});
38
39const $viewingPatterns = computed($viewingPatternData, (state) => parseJSON(state));
40export const useViewingPatternData = () => useStore($viewingPatterns);
41export const getViewingPatternData = () => $viewingPatterns.get();
42
43export const setViewingPatternData = (data) => {
44  $viewingPatternData.set(JSON.stringify(data));
45};
46
47function parsePageNum(page) {
48  return isNaN(page) ? 0 : page;
49}
50export function loadPublicPatterns(page) {
51  page = parsePageNum(page);
52  const offset = page * patternQueryLimit;
53  return supabase
54    .from('code_v1')
55    .select()
56    .eq('public', true)
57    .range(offset, offset + patternQueryLimit)
58    .order('id', { ascending: false });
59}
60
61export function loadFeaturedPatterns(page = 0) {
62  page = parsePageNum(page);
63  const offset = page * patternQueryLimit;
64  return supabase
65    .from('code_v1')
66    .select()
67    .eq('featured', true)
68    .range(offset, offset + patternQueryLimit)
69    .order('id', { ascending: false });
70}
71
72export async function loadAndSetPublicPatterns(page) {
73  const p = await loadPublicPatterns(page);
74  const data = p?.data;
75  const pats = {};
76  data?.forEach((data, key) => (pats[data.id ?? key] = data));
77  $publicPatterns.set(pats);
78}
79export async function loadAndSetFeaturedPatterns(page) {
80  const p = await loadFeaturedPatterns(page);
81  const data = p?.data;
82  const pats = {};
83  data?.forEach((data, key) => (pats[data.id ?? key] = data));
84  $featuredPatterns.set(pats);
85}
86
87export async function loadDBPatterns() {
88  try {
89    await loadAndSetPublicPatterns();
90    await loadAndSetFeaturedPatterns();
91  } catch (err) {
92    console.error('error loading patterns', err);
93  }
94}
95
96// reason: https://codeberg.org/uzu/strudel/issues/857
97const $activePattern = sessionAtom('activePattern', '');
98
99export function setActivePattern(key) {
100  $activePattern.set(key);
101}
102export function getActivePattern() {
103  return $activePattern.get();
104}
105export function useActivePattern() {
106  return useStore($activePattern);
107}
108
109export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
110
111export const defaultCode = '';
112export const userPattern = {
113  collection: patternFilterName.user,
114  getAll() {
115    const patterns = parseJSON(settingsMap.get().userPatterns);
116    return patterns ?? {};
117  },
118  getPatternData(id) {
119    const userPatterns = this.getAll();
120    return userPatterns[id];
121  },
122  exists(id) {
123    return this.getPatternData(id) != null;
124  },
125  isValidID(id) {
126    return id != null && id.length > 0;
127  },
128
129  create() {
130    const newID = createPatternID();
131    const code = defaultCode;
132    const data = { code, created_at: Date.now(), id: newID, collection: this.collection };
133    return { id: newID, data };
134  },
135  createAndAddToDB() {
136    const newPattern = this.create();
137    return this.update(newPattern.id, newPattern.data);
138  },
139
140  update(id, data) {
141    const userPatterns = this.getAll();
142    data = { ...data, id, collection: this.collection };
143    setUserPatterns({ ...userPatterns, [id]: data });
144    return { id, data };
145  },
146  duplicate(data) {
147    const newPattern = this.create();
148    return this.update(newPattern.id, { ...newPattern.data, code: data.code });
149  },
150  clearAll() {
151    confirmDialog(`This will delete all your patterns. Are you really sure?`).then((r) => {
152      if (r == false) {
153        return;
154      }
155      const viewingPatternData = getViewingPatternData();
156      setUserPatterns({});
157
158      if (viewingPatternData.collection !== this.collection) {
159        return { id: viewingPatternData.id, data: viewingPatternData };
160      }
161      setActivePattern(null);
162      return this.create();
163    });
164  },
165  delete(id) {
166    const userPatterns = this.getAll();
167    delete userPatterns[id];
168    if (getActivePattern() === id) {
169      setActivePattern(null);
170    }
171    setUserPatterns(userPatterns);
172    const viewingPatternData = getViewingPatternData();
173    const viewingID = viewingPatternData?.id;
174    if (viewingID === id) {
175      return { id: null, data: { code: defaultCode } };
176    }
177    return { id: viewingID, data: userPatterns[viewingID] };
178  },
179};
180
181function setUserPatterns(obj) {
182  return settingsMap.setKey('userPatterns', JSON.stringify(obj));
183}
184
185export const createPatternID = () => {
186  return nanoid(12);
187};
188
189export async function importPatterns(fileList) {
190  const files = Array.from(fileList);
191  await Promise.all(
192    files.map(async (file, i) => {
193      const content = await file.text();
194      if (file.type === 'application/json') {
195        const userPatterns = userPattern.getAll();
196        setUserPatterns({ ...userPatterns, ...parseJSON(content) });
197      } else if (['text/x-markdown', 'text/plain'].includes(file.type)) {
198        const id = file.name.replace(/\.[^/.]+$/, '');
199        userPattern.update(id, { code: content });
200      }
201    }),
202  );
203  logger(`import done!`);
204}
205
206export async function exportPatterns() {
207  const userPatterns = userPattern.getAll();
208  const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' });
209  const downloadLink = document.createElement('a');
210  const prefix = window.location.hostname.split('.').join('_');
211  downloadLink.href = window.URL.createObjectURL(blob);
212  const date = new Date().toISOString().split('T')[0];
213  downloadLink.download = `${prefix}_patterns_${date}.json`;
214  document.body.appendChild(downloadLink);
215  downloadLink.click();
216  document.body.removeChild(downloadLink);
217}