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}