PageSourceSearch

https://english-step-by-step.com/js/features/vocabulary-trainer/repository.js?v=20260923-account-3

js english-step-by-step.com collected 2026-10-02 00:49:14 UTC 4,690 bytes, 169 lines download raw bytes

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.