Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat select dictionary variant #10

Merged
merged 12 commits into from
Mar 26, 2024
8 changes: 7 additions & 1 deletion public/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@
"message": "Data in storage changed, can't get new data"
},
"SELECTED_LANGUAGES_SAVED": {
"message": "Selected languages succesfully saved"
"message": "Selected languages were succesfully saved"
},
"SELECT_LANGUAGES_FORM_TITLE": {
"message": "Select languages to learn"
},
"SELECT_DICTIONARY_VARIANT_SAVED": {
"message": "Your choice was succesfully saved"
},
"ERROR_CAN_NOT_FIND_DICTIONARY_TO_SELECT_VARIANT": {
"message": "Can't find dictionary"
}
}
2 changes: 2 additions & 0 deletions src/app/popup/Popup.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SelectDictionaryList } from '@features/dictionary/SelectDictionaryVariant'
import { SelectLanguages } from '@features/language/SelectLanguages'

import { LANGUAGES } from '@entities/language'
Expand All @@ -7,6 +8,7 @@ import { Toasts } from '@shared/ui/Toast'
export const Popup = () => {
return (
<main>
<SelectDictionaryList />
<SelectLanguages languages={LANGUAGES} />
<Toasts />
</main>
Expand Down
2 changes: 2 additions & 0 deletions src/entities/dictionary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ui'
export * from './model'
122 changes: 122 additions & 0 deletions src/entities/dictionary/model/__tests__/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { allTasks, cleanStores, keepMount } from 'nanostores'
import { afterEach, describe, expect, test, vi } from 'vitest'

import { Result } from '@shared/libs/operationResult'
import { getErrorToastMock } from '@shared/ui/Toast/helpers/__mock__/getErrorToast'
import { addToastMock } from '@shared/ui/Toast/model/__mock__/store'

import { waitFor } from '@tests/testUtils'

import { DICTIONARIES } from '../dictionaries'
import { $dictionaries, DictionaryStorage, selectVariant } from '../store'
import { TDictionaries } from '../types'

describe('dictionary store', () => {
const error = {
type: `ERROR`,
error: null,
}

afterEach(async () => {
await chrome.storage.local.clear()
cleanStores($dictionaries)
$dictionaries.set(DICTIONARIES)
vi.clearAllMocks()
})

describe('commmon', () => {
test('should set value from DictionaryStorage', async () => {
const newDictionaries: TDictionaries = {
...DICTIONARIES,
en: { ...DICTIONARIES['en'], label: 'Enlgish New Name' },
}
await DictionaryStorage.set(newDictionaries)

keepMount($dictionaries)
await allTasks()

expect($dictionaries.get()).toEqual(newDictionaries)
expect(getErrorToastMock).toBeCalledTimes(0)
})

test('should set empty array and show toast with error when DictionaryStorage returns error', async () => {
vi.spyOn(DictionaryStorage, 'get').mockResolvedValueOnce(
Result.Error(error),
)

keepMount($dictionaries)
await allTasks()

expect($dictionaries.get()).toEqual(DICTIONARIES)
expect(getErrorToastMock).toBeCalledWith(error)
})
})

describe('selectVariant', () => {
const dictionaryId = 'en_CambridgeDictionary'

test('should set selected variants to DictionaryStorage and show operation success information', async () => {
keepMount($dictionaries)
await allTasks()

await waitFor(async () => {
await selectVariant('en', dictionaryId, 'us')

const call = addToastMock.mock.calls[0]
expect(call[0].type).toBe('success')
expect(call[0].title).toBeDefined()
expect(getErrorToastMock).toBeCalledTimes(0)

const dictionary = $dictionaries
.get()
.en.dictionaries.find((dictionary) => {
dictionary.id === dictionaryId
})

if (dictionary && 'variants' in dictionary) {
expect(dictionary.activeVariant).toBe('us')
}
})
})

test('should show error if can not set to DictionaryStorage and keep previous variant', async () => {
vi.spyOn(DictionaryStorage, 'set').mockResolvedValue(Result.Error(error))

keepMount($dictionaries)
await allTasks()

await waitFor(async () => {
await selectVariant('en', dictionaryId, 'us')

expect(getErrorToastMock).toBeCalledWith(error)

const dictionary = $dictionaries
.get()
.en.dictionaries.find((dictionary) => dictionary.id === dictionaryId)

if (dictionary && 'variants' in dictionary) {
expect(dictionary.activeVariant).toEqual('uk')
} else {
throw Error(
`Dictionary with id ${dictionaryId} and variants option not found`,
)
}
})
})

test('should show error if dictionary not found', async () => {
keepMount($dictionaries)
await allTasks()

await waitFor(async () => {
await selectVariant('en', 'NOT_EXISTING_ID', 'us')

const call = getErrorToastMock.mock.calls[0]
expect(call[0].type).toBe(
'ERROR_CAN_NOT_FIND_DICTIONARY_TO_SELECT_VARIANT',
)
expect(call[0].error).toBeDefined()
})
})
})
})
101 changes: 101 additions & 0 deletions src/entities/dictionary/model/dictionaries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { TDictionaries } from './types'

export const DICTIONARIES: TDictionaries = {
en: {
label: 'English',
value: 'en',
dictionaries: [
{
id: 'en_CambridgeDictionary',
name: 'Cambridge Dictionary',
url: 'https://dictionary.cambridge.org/',
variants: [
{
label: 'American',
value: 'us',
},
{
label: 'British',
value: 'uk',
},
],
activeVariant: 'uk',
},
{
id: 'en_MerriamWebster',
name: 'Merriam-Webster',
url: 'https://www.merriam-webster.com/dictionary',
},
{
id: 'en_Collins',
name: 'Collins',
url: 'https://www.collinsdictionary.com/dictionary/english',
variants: [
{
label: 'American',
value: 'us',
},
{
label: 'British',
value: 'uk',
},
],
activeVariant: 'uk',
},
{
id: 'en_Wordreference',
name: 'Wordreference',
url: 'https://www.wordreference.com/definition',
variants: [
{
label: 'American',
value: 'us',
},
{
label: 'British',
value: 'uk',
},
],
activeVariant: 'uk',
},
{
id: 'en_GoogleTranslate',
name: 'Google Translate',
url: 'https://translate.google.com/',
},
],
},
jp: {
label: 'Japanese',
value: 'jp',
dictionaries: [
{
id: 'jp_Jisho',
name: 'Jisho',
url: 'https://jisho.org/',
},
],
},
pt: {
label: 'Portuguese',
value: 'pt',
dictionaries: [
{
id: 'jp_Jisho',
name: 'Jisho',
url: 'https://jisho.org/',
},
],
},
ko: {
label: 'Korean',
value: 'ko',
dictionaries: [
{
id: 'jp_Jisho',
name: 'Jisho',
url: 'https://jisho.org/',
},
],
},
}
2 changes: 2 additions & 0 deletions src/entities/dictionary/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { IDictionary, IDictionaryWithVariants } from './types'
export * as dictionaryStore from './store'
79 changes: 79 additions & 0 deletions src/entities/dictionary/model/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { atom, onMount, task } from 'nanostores'

import { TLanguageCode } from '@entities/language'
import { Storage } from '@entities/storage'

import { browser } from '@shared/browser'
import { Result } from '@shared/libs/operationResult'
import { addToast, getErrorToast } from '@shared/ui/Toast'

import { DICTIONARIES } from './dictionaries'
import { TDictionaries } from './types'

export const DictionaryStorage = new Storage<TDictionaries>(
'dictionaries',
DICTIONARIES,
)

export const $dictionaries = atom<TDictionaries>(DICTIONARIES)

onMount($dictionaries, () => {
task(async () => {
const getResult = await DictionaryStorage.get()
if (getResult.data) {
$dictionaries.set(getResult.data)
} else {
addToast(getErrorToast(getResult.error))
}
})
})

export const selectVariant = async (
languageCode: TLanguageCode,
dictionaryId: string,
variant: string,
) => {
const dictionaries = $dictionaries.get()

const dictionary = dictionaries[languageCode].dictionaries.find(
(dictionary) => dictionary.id === dictionaryId,
)

if (dictionary && 'variants' in dictionary) {
const newDictionaries = {
...dictionaries,
[languageCode]: {
...dictionaries[languageCode],
dictionaries: dictionaries[languageCode].dictionaries.map(
(dictionary) => {
if (dictionary.id === dictionaryId && 'variants' in dictionary) {
return { ...dictionary, activeVariant: variant }
} else {
return { ...dictionary }
}
},
),
},
}
const setResult = await DictionaryStorage.set(newDictionaries)
if (setResult.data) {
$dictionaries.set(newDictionaries)
addToast({
type: 'success',
title: browser.i18n.getMessage('SELECT_DICTIONARY_VARIANT_SAVED'),
})
} else {
addToast(getErrorToast(setResult.error))
}
} else {
const resultError = Result.Error({
type: 'ERROR_CAN_NOT_FIND_DICTIONARY_TO_SELECT_VARIANT',
error: new Error(
`Dictionary with id ${dictionaryId} and variants option not found, available dictionaries: /n ${JSON.stringify(dictionaries)}`,
),
})
if (resultError.error) {
addToast(getErrorToast(resultError.error))
}
}
}
24 changes: 24 additions & 0 deletions src/entities/dictionary/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ILanguage, TLanguageCode } from '@entities/language'

interface IDictionaryCommonFields {
id: string
name: string
url: string
}

export interface IDictionaryWithVariants extends IDictionaryCommonFields {
variants: {
label: string
value: string
}[]
activeVariant: string
}

export type IDictionary = IDictionaryCommonFields | IDictionaryWithVariants

export type TDictionaries = Record<
TLanguageCode,
ILanguage & {
dictionaries: Array<IDictionary>
}
>
21 changes: 21 additions & 0 deletions src/entities/dictionary/ui/DictionaryCard/DictionaryCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { JSXInternal } from 'node_modules/preact/src/jsx'

import styles from './styles.module.scss'

interface Props {
url: string
name: string
actions?: JSXInternal.Element
}

export const DictionaryCard = ({ url, name, actions }: Props) => (
<li className={styles.dictionary}>
<a className={styles.dictionary__url} href={url} target="_blank" />

<header>
<span className={`h2 ${styles.dictionary__title}`}>{name}</span>
</header>

<footer className={styles.dictionary__actions}>{actions}</footer>
</li>
)
1 change: 1 addition & 0 deletions src/entities/dictionary/ui/DictionaryCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DictionaryCard } from './DictionaryCard'
Loading
Loading