1import { 2 createTrainerWord, 3 deleteTrainerWord, 4 fetchTrainerWords, 5 toggleTrainerWord, 6 updateTrainerWord, 7} from './api.js?v=20260923-account-3'; 8import { t } from '../../i18n.js'; 9import { 10 buildWordSignature, 11 createClientWordId, 12 isSameWordId, 13 normalizeTrainerWord, 14 sanitizeTrainerWordInput, 15 sortTrainerWords, 16} from './helpers.js'; 17import { loadGuestTrainerWords, saveGuestTrainerWords } from './local-storage.js'; 18 19function upsertLocalWord(words, input, lang, currentId = null) { 20 const sanitized = sanitizeTrainerWordInput(input, lang); 21 const now = new Date().toISOString(); 22 const existing = words.find((item) => !isSameWordId(item.id, currentId) && buildWordSignature(item) === buildWordSignature(sanitized)); 23 24 if (!currentId) { 25 if (existing) { 26 const merged = normalizeTrainerWord({ 27 ...existing, 28 ...sanitized, 29 inTraining: existing.inTraining || sanitized.inTraining, 30 updatedAt: now, 31 }); 32 33 return sortTrainerWords(words.map((item) => (isSameWordId(item.id, existing.id) ? merged : item))); 34 } 35 36 return sortTrainerWords([ 37 normalizeTrainerWord({ 38 id: createClientWordId(), 39 ...sanitized, 40 addedAt: now, 41 updatedAt: now, 42 }), 43 ...words, 44 ]); 45 } 46 47 const current = words.find((item) => isSameWordId(item.id, currentId)); 48 if (!current) { 49 throw new Error(t('trainerWordNotFoundError', lang)); 50 } 51 52 if (existing) { 53 const merged = normalizeTrainerWord({ 54 ...existing, 55 ...sanitized, 56 inTraining: existing.inTraining || sanitized.inTraining, 57 updatedAt: now, 58 }); 59 60 return sortTrainerWords( 61 words 62 .filter((item) => !isSameWordId(item.id, currentId)) 63 .map((item) => (isSameWordId(item.id, existing.id) ? merged : item)) 64 ); 65 } 66 67 return sortTrainerWords( 68 words.map((item) => ( 69 isSameWordId(item.id, currentId) 70 ? normalizeTrainerWord({ 71 ...item, 72 ...sanitized, 73 addedAt: item.addedAt || now, 74 updatedAt: now, 75 }) 76 : item 77 )) 78 ); 79} 80 81function updateLocalStore(mutator) { 82 const currentWords = loadGuestTrainerWords(); 83 const nextWords = mutator(currentWords); 84 saveGuestTrainerWords(nextWords); 85 return nextWords; 86} 87 88export function createTrainerRepository({ authenticated }) { 89 let useRemote = !!authenticated; 90 91 async function withRemoteFallback(remoteAction, fallbackAction) { 92 if (!useRemote) { 93 return fallbackAction(); 94 } 95 96 try { 97 return await remoteAction(); 98 } catch (error) { 99 if (error?.status !== 401) { 100 throw error; 101 } 102 103 window.location.assign('/login?next=' + encodeURIComponent(location.pathname)); 104 throw error; 105 } 106 } 107 108 return { 109 async listWords() { 110 return withRemoteFallback( 111 () => fetchTrainerWords(), 112 () => loadGuestTrainerWords() 113 ); 114 }, 115 116 async addWord(input, lang) { 117 return withRemoteFallback( 118 () => createTrainerWord(input, lang), 119 () => { 120 const words = updateLocalStore((currentWords) => upsertLocalWord(currentWords, input, lang)); 121 return words[0] || null; 122 } 123 ); 124 }, 125 126 async updateWord(id, input, lang) { 127 return withRemoteFallback( 128 () => updateTrainerWord(id, input, lang), 129 () => { 130 const words = updateLocalStore((currentWords) => upsertLocalWord(currentWords, input, lang, id)); 131 return words.find((item) => isSameWordId(item.id, id)) || words[0] || null; 132 } 133 ); 134 }, 135 136 async deleteWord(id) { 137 return withRemoteFallback( 138 () => deleteTrainerWord(id), 139 () => { 140 updateLocalStore((currentWords) => currentWords.filter((item) => !isSameWordId(item.id, id))); 141 return true; 142 } 143 ); 144 }, 145 146 async toggleWord(id, inTraining) { 147 return withRemoteFallback( 148 () => toggleTrainerWord(id, inTraining), 149 () => { 150 const words = updateLocalStore((currentWords) => ( 151 sortTrainerWords( 152 currentWords.map((item) => ( 153 isSameWordId(item.id, id) 154 ? normalizeTrainerWord({ 155 ...item, 156 inTraining: !!inTraining, 157 updatedAt: new Date().toISOString(), 158 }) 159 : item 160 )) 161 ) 162 )); 163 164 return words.find((item) => isSameWordId(item.id, id)) || null; 165 } 166 ); 167 }, 168 }; 169}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.