diff --git a/002_source/ionic_mobile/.env.development b/002_source/ionic_mobile/.env.development new file mode 100644 index 0000000..d1a9c2d --- /dev/null +++ b/002_source/ionic_mobile/.env.development @@ -0,0 +1,4 @@ +# +# POCKETBASE running in wsl2 +# +VITE_POCKETBASE_URL=http://192.168.222.199:8090 diff --git a/002_source/ionic_mobile/.gitignore b/002_source/ionic_mobile/.gitignore index e428692..4c3d9a8 100644 --- a/002_source/ionic_mobile/.gitignore +++ b/002_source/ionic_mobile/.gitignore @@ -1,6 +1,7 @@ .env **/*.log **/*.del +**/*.draft **/*.bak **/*del diff --git a/002_source/ionic_mobile/scripts/001_build.sh b/002_source/ionic_mobile/scripts/001_build.sh new file mode 100755 index 0000000..aca5dfd --- /dev/null +++ b/002_source/ionic_mobile/scripts/001_build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -ex + +npm run lint +npx nodemon --ext ts,tsx --exec "npm run build" diff --git a/002_source/ionic_mobile/src/RouteConfig.tsx b/002_source/ionic_mobile/src/RouteConfig.tsx index 101e496..cb6d3d8 100644 --- a/002_source/ionic_mobile/src/RouteConfig.tsx +++ b/002_source/ionic_mobile/src/RouteConfig.tsx @@ -28,7 +28,7 @@ import { AuthSignUp } from './pages/auth/SignUp'; import Lesson from './pages/Lesson/index'; // NOTES: old version using json file -import LessonWordPageByDb from './pages/Lesson/LessonWordPageByDb'; +// import LessonWordPageByDb from './pages/Lesson/LessonWordPageByDb'; import WordPage from './pages/Lesson/WordPage'; // import ListeningPractice from './pages/ListeningPractice'; diff --git a/002_source/ionic_mobile/src/components/CustomField/index.tsx b/002_source/ionic_mobile/src/components/CustomField/index.tsx index 0032083..c974f6b 100644 --- a/002_source/ionic_mobile/src/components/CustomField/index.tsx +++ b/002_source/ionic_mobile/src/components/CustomField/index.tsx @@ -7,7 +7,23 @@ interface CustomFieldProps { label: string; required: boolean; input: { - props: { type: string; placeholder: string }; + props: { + type: + | 'date' + | 'email' + | 'number' + | 'password' + | 'search' + | 'tel' + | 'text' + | 'url' + | 'time' + | 'week' + | 'month' + | 'datetime-local'; + placeholder: string; + // + }; state: { value: string; reset: (newValue: any) => void; @@ -20,8 +36,8 @@ interface CustomFieldProps { } function CustomField({ field, errors }: CustomFieldProps): React.JSX.Element { - const error = errors && errors.filter((e) => e.id === field.id)[0]; - const errorMessage = error && errors.filter((e) => e.id === field.id)[0].message; + const error = errors && errors.filter((e: { id: string }) => e.id === field.id)[0]; + const errorMessage = error && errors.filter((e: { id: string }) => e.id === field.id)[0].message; return ( <> @@ -30,7 +46,12 @@ function CustomField({ field, errors }: CustomFieldProps): React.JSX.Element { {field.label} {error &&

{errorMessage}

} - + ); diff --git a/002_source/ionic_mobile/src/contexts/I18nProvider.tsx b/002_source/ionic_mobile/src/contexts/I18nProvider.tsx new file mode 100644 index 0000000..212f9ba --- /dev/null +++ b/002_source/ionic_mobile/src/contexts/I18nProvider.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; + +import '../i18n'; + +export interface I18nProviderProps { + children: React.ReactNode; + language?: string; +} + +export function I18nProvider({ children, language = 'en' }: I18nProviderProps): React.JSX.Element { + const { i18n } = useTranslation(); + + React.useEffect(() => { + // + }, [i18n, language]); + + return <>{children}; +} diff --git a/002_source/ionic_mobile/src/contexts/index.tsx b/002_source/ionic_mobile/src/contexts/index.tsx index 4626b2a..016007f 100644 --- a/002_source/ionic_mobile/src/contexts/index.tsx +++ b/002_source/ionic_mobile/src/contexts/index.tsx @@ -1,6 +1,7 @@ import { PocketBaseProvider } from '../hooks/usePocketBase'; import { AppStateProvider } from './AppState'; import { UserProvider } from './auth/user-context'; +import { I18nProvider } from './I18nProvider'; import { MyIonFavoriteProvider } from './MyIonFavorite'; import { MyIonMetricProvider } from './MyIonMetric'; import { MyIonQuizProvider } from './MyIonQuiz'; @@ -12,24 +13,26 @@ const queryClient = new QueryClient(); const ContextMeta = ({ children }: { children: React.ReactNode }) => { return ( <> - - - - - - - - - {children} - {/* */} - - - - - - - - + + + + + + + + + + {children} + {/* */} + + + + + + + + + ); }; diff --git a/002_source/ionic_mobile/src/data/utils.tsx b/002_source/ionic_mobile/src/data/utils.tsx index 0388ed6..4d14215 100644 --- a/002_source/ionic_mobile/src/data/utils.tsx +++ b/002_source/ionic_mobile/src/data/utils.tsx @@ -3,23 +3,23 @@ import { useState } from 'react'; export const useFormInput = (initialValue = '') => { const [value, setValue] = useState(initialValue); - const handleChange = async (e) => { + const handleChange = async (e: React.ChangeEvent) => { const tempValue = await e.currentTarget.value; setValue(tempValue); }; return { value, - reset: (newValue) => setValue(newValue), + reset: (newValue: string) => setValue(newValue), onIonChange: handleChange, onKeyUp: handleChange, }; }; -export const validateForm = (fields) => { - let errors = []; +export const validateForm = (fields: { required: boolean; id: string; input: { state: { value: string } } }[]) => { + let errors: { id: string; message: string }[] = []; - fields.forEach((field) => { + fields.forEach((field: { required: boolean; id: string; input: { state: { value: string } } }) => { if (field.required) { const fieldValue = field.input.state.value; diff --git a/002_source/ionic_mobile/src/hooks/fetchMFQuestions.tsx b/002_source/ionic_mobile/src/hooks/fetchMFQuestions.tsx index 29f0507..e667778 100644 --- a/002_source/ionic_mobile/src/hooks/fetchMFQuestions.tsx +++ b/002_source/ionic_mobile/src/hooks/fetchMFQuestions.tsx @@ -1,5 +1,3 @@ -import { QuizMFQuestion } from '../types/QuizMFQuestion'; -import { usePocketBase } from './usePocketBase'; import { QueryClient } from '@tanstack/react-query'; const queryClient = new QueryClient({ @@ -10,12 +8,12 @@ const queryClient = new QueryClient({ }, }); -const fetchMFQuestions = async (cat_id: string, pb: any) => { +async function fetchMFQuestions(cat_id: string, pb: any) { const response = await queryClient.fetchQuery({ queryKey: ['fetchData'], staleTime: 60 * 1000, queryFn: async () => { - return await pb.collection('QuizMFQuestions').getList(1, 9999, { + return await pb.collection('QuizMFQuestions').getList(1, 9999, { filter: `cat_id = "${cat_id}"`, $autoCancel: false, }); @@ -23,6 +21,6 @@ const fetchMFQuestions = async (cat_id: string, pb: any) => { }); return response; -}; +} export default fetchMFQuestions; diff --git a/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx b/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx index bb08fe2..61ad056 100644 --- a/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx +++ b/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx @@ -1,9 +1,7 @@ -import { usePocketBase } from './usePocketBase.tsx'; -import type LessonsTypes from '../types/LessonsTypes'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; -import Vocabularies from '../types/Vocabularies.tsx'; -const useGetVocabularyRoute = (lessonType: string, catId: string) => { +function useGetVocabularyRoute(lessonType: string, catId: string) { const { user, pb } = usePocketBase(); return useQuery({ queryKey: ['useGetVocabularyRoute', lessonType, catId, 'feeds', 'all', user?.id || ''], @@ -14,7 +12,7 @@ const useGetVocabularyRoute = (lessonType: string, catId: string) => { queryKey: ['useGetVocabularyRoute', string, string, 'feeds', 'all', string | null]; }) => { console.log('calling useGetLessonCategoriesRoute'); - return await pb.collection('Vocabularies').getList(1, 9999, { + return await pb.collection('Vocabularies').getList(1, 9999, { // TODO: sort by field -> pos sort: 'id', filter: `lesson_type_id = "${lessonType}" && cat_id = "${catId}"`, @@ -24,6 +22,6 @@ const useGetVocabularyRoute = (lessonType: string, catId: string) => { }, // enabled: !!user?.id, }); -}; +} export default useGetVocabularyRoute; diff --git a/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx b/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx index 484aa63..18ac053 100644 --- a/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx +++ b/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx @@ -1,4 +1,4 @@ -import { usePocketBase } from './usePocketBase.tsx'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; import { LessonsType } from '../types/LessonsTypes'; diff --git a/002_source/ionic_mobile/src/hooks/useListQuizCRCategories.tsx b/002_source/ionic_mobile/src/hooks/useListQuizCRCategories.tsx index 3b2d02f..ed14cdd 100644 --- a/002_source/ionic_mobile/src/hooks/useListQuizCRCategories.tsx +++ b/002_source/ionic_mobile/src/hooks/useListQuizCRCategories.tsx @@ -1,7 +1,7 @@ // CR = ConnectiveRevision -import { usePocketBase } from './usePocketBase.tsx'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; -import IListeningPracticeCategory from '../interfaces/IListeningPracticeCategory.tsx'; +import IListeningPracticeCategory from '../interfaces/IListeningPracticeCategory'; const useListQuizCRCategories = () => { const { user, pb } = usePocketBase(); diff --git a/002_source/ionic_mobile/src/hooks/useListQuizCRQuestionByCRCategoryId.tsx b/002_source/ionic_mobile/src/hooks/useListQuizCRQuestionByCRCategoryId.tsx index 41dbb04..663e393 100644 --- a/002_source/ionic_mobile/src/hooks/useListQuizCRQuestionByCRCategoryId.tsx +++ b/002_source/ionic_mobile/src/hooks/useListQuizCRQuestionByCRCategoryId.tsx @@ -1,6 +1,6 @@ -import { usePocketBase } from './usePocketBase.tsx'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; -import { QuizCRQuestion } from '../types/QuizCRQuestion.ts/index.ts'; +import { QuizCRQuestion } from '../types/QuizCRQuestion'; const useListQuizCRQuestionByCRCategoryId = (CRCategoryId: string) => { const { user, pb } = usePocketBase(); diff --git a/002_source/ionic_mobile/src/hooks/useListQuizLPQuestionByLPCategoryId.tsx b/002_source/ionic_mobile/src/hooks/useListQuizLPQuestionByLPCategoryId.tsx index 9afe476..adcffad 100644 --- a/002_source/ionic_mobile/src/hooks/useListQuizLPQuestionByLPCategoryId.tsx +++ b/002_source/ionic_mobile/src/hooks/useListQuizLPQuestionByLPCategoryId.tsx @@ -1,4 +1,4 @@ -import { usePocketBase } from './usePocketBase.tsx'; +import { usePocketBase } from './usePocketBase'; import { useQuery } from '@tanstack/react-query'; // import { QuizLPQuestion } from '../types/QuizLPQuestion'; diff --git a/002_source/ionic_mobile/src/hooks/useListQuizMFQuestionsByCategoryId.tsx b/002_source/ionic_mobile/src/hooks/useListQuizMFQuestionsByCategoryId.tsx index 093819b..65b67e5 100644 --- a/002_source/ionic_mobile/src/hooks/useListQuizMFQuestionsByCategoryId.tsx +++ b/002_source/ionic_mobile/src/hooks/useListQuizMFQuestionsByCategoryId.tsx @@ -1,6 +1,6 @@ import { QuizMFQuestion } from '../types/QuizMFQuestion'; import { usePocketBase } from './usePocketBase'; -import { QueryClient } from '@tanstack/react-query'; +import { QueryClient, useQuery } from '@tanstack/react-query'; const queryClient = new QueryClient({ defaultOptions: { diff --git a/002_source/ionic_mobile/src/hooks/usePocketBase.tsx b/002_source/ionic_mobile/src/hooks/usePocketBase.tsx index c856f89..a734432 100644 --- a/002_source/ionic_mobile/src/hooks/usePocketBase.tsx +++ b/002_source/ionic_mobile/src/hooks/usePocketBase.tsx @@ -25,7 +25,12 @@ export const PocketBaseProvider = ({ children }: { children: any }) => { const logout = useCallback(() => pb.authStore.clear(), [pb.authStore]); - return {children}; + return ( + + {/* */} + {children} + + ); }; export const usePocketBase = () => { diff --git a/002_source/ionic_mobile/src/i18n copy.ts b/002_source/ionic_mobile/src/i18n copy.ts new file mode 100644 index 0000000..15f520b --- /dev/null +++ b/002_source/ionic_mobile/src/i18n copy.ts @@ -0,0 +1,107 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +// the translations +// (tip move them in a JSON file and import them) +const resources = { + en: { + translation: { + 'Lesson': 'Lesson', + 'Quiz': 'Quiz', + 'Favorite': 'Favorite', + 'Loading': 'Loading', + 'menu.link-home': 'Home', + 'menu.link-stats': 'Stats', + 'home.title': 'Remove duplicate songs from your Spotify library.', + 'home.description': + "Spotify Dedup cleans up your playlists and liked songs from your Spotify account. It's easy and fast.", + 'home.review': + 'Read what {{-supportersCount}} supporters think about Spotify Dedup on {{- linkOpen}}Buy Me a Coffee{{- linkClose}}', + 'home.login-button': 'Log in with Spotify', + 'meta.title': 'Spotify Dedup - Remove duplicate songs from your Spotify library', + 'meta.description': + 'Delete repeated songs from your Spotify playlists and liked songs automatically. Fix your music library. Quickly and easy.', + 'features.find-remove.header': 'Find & remove', + 'features.find-remove.body': + 'Dedup checks your playlists and liked songs in {{- strongOpen}}your Spotify library{{- strongClose}}. Once Dedup finds duplicates you can remove them on a per-playlist basis.', + 'features.safer.header': 'Safer', + 'features.safer.body': + 'Dedup will only remove {{- strongOpen}}duplicate songs{{- strongClose}}, leaving the rest of the playlist and liked songs untouched.', + 'features.open-source.header': 'Open Source', + 'features.open-source.body': + "You might want to have a look at the {{- linkGithubOpen}}source code on GitHub{{- linkGithubClose}}. This web app uses the {{- linkWebApiOpen}}Spotify Web API{{- linkWebApiClose}} to manage user's playlists and liked songs.", + 'reviews.title': 'This is what users are saying', + 'footer.author': 'Made with ♥ by {{- linkOpen}}JMPerez 👨‍💻{{- linkClose}}', + 'footer.github': 'Check out the {{- linkOpen}}code on GitHub 📃{{- linkClose}}', + 'footer.bmc': 'Support the project {{- linkOpen}}buying a coffee ☕{{- linkClose}}', + 'bmc.button': 'Would you buy me a coffee?', + 'result.duplicate.reason-same-id': 'Duplicate', + 'result.duplicate.reason-same-data': 'Duplicate (same name, artist and duration)', + 'result.duplicate.track': '<0>{{trackName}} <2>by <4>{{trackArtistName}}', + 'process.status.finding': 'Finding duplicates in your playlists and liked songs…', + 'process.status.complete': 'Processing complete!', + 'process.status.complete.body': 'Your playlists and liked songs have been processed!', + 'process.status.complete.dups.body': + 'Click on the {{- strongOpen}}Remove duplicates{{- strongClose}} button to get rid of duplicates in that playlist or liked songs collection.', + 'process.status.complete.nodups.body': "Congrats! You don't have duplicates in your playlists nor liked songs.", + 'process.reading-library': 'Going through your library, finding the playlists you own and your liked songs…', + 'process.processing_one': 'Searching for duplicate songs, wait a sec. Still to process {{count}} playlist…', + 'process.processing_other': 'Searching for duplicate songs, wait a sec. Still to process {{count}} playlists…', + 'process.saved.title': 'liked songs in your library', + 'process.saved.duplicates_one': 'This collection has {{count}} duplicate song', + 'process.saved.duplicates_other': 'This collection has {{count}} duplicate songs', + 'process.saved.remove-button': 'Remove duplicates from your liked songs', + 'process.playlist.duplicates_one': 'This playlist has {{count}} duplicate song', + 'process.playlist.duplicates_other': 'This playlist has {{count}} duplicate songs', + 'process.playlist.remove-button': 'Remove duplicates from this playlist', + 'process.items.removed': 'Duplicates removed', + 'faq.section-title': 'Frequently asked questions', + 'faq.question-1': 'What does this web application do?', + 'faq.answer-1': + 'Spotify Dedup helps you clean up your music libraries on Spotify by identifying and deleting duplicate songs across playlists and liked songs.', + 'faq.question-2': 'How does it find duplicates?', + 'faq.answer-2': + "Dedup finds duplicates based on the songs identifier, title, artist, and duration similarity. It identifies duplicates that Spotify's application does not catch.", + 'faq.question-3': "How is Dedup better than Spotify's built-in duplicate detection?", + 'faq.answer-3': + "Spotify's applications only warn about duplicates when adding a song to a playlit or liked songs with the exact same song identifier. However, the same song can have multiple identifiers on Spotify that both in the same release or in several ones. Dedup detects duplicates based on title, artist, and duration similarity.", + 'faq.question-4': 'When duplicates are found, which songs are removed?', + 'faq.answer-4': 'Dedup will keep the first song within a group of duplicate songs, and will remove the rest.', + 'faq.question-5': 'Is my data safe with this web application?', + 'faq.answer-5': + 'Yes, this web application does not store any user data on its servers. It only requests the minimum set of permissions necessary to process your library.', + 'faq.question-6': 'What permissions does this web application require?', + 'faq.answer-6': + "This web application uses Spotify's authentication service to access your liked songs and playlists in your library.", + 'faq.question-7': 'How has this tool been tested?', + 'faq.answer-7': + 'This tool has been battle-tested by thousands of users who have used it to identify duplicates in millions of playlists since 2014.', + 'faq.question-8': 'Can this tool delete duplicates across multiple playlists?', + 'faq.answer-8': + "This tool can identify and delete duplicates on all playlists in a library, but doesn't detect duplicates of a song across multiple playlists.", + 'faq.question-9': 'How can I revoke the permissions granted to this web application?', + 'faq.answer-9': + "You can revoke the permissions granted to this web application at any time on your Spotify account, under the 'Apps' section.", + 'faq.question-10': 'Does this tool work with other music streaming services?', + 'faq.answer-10': "No, this tool only works with Spotify through Spotify's Web API.", + }, + }, +}; + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + // the translations + // (tip move them in a JSON file and import them, + // or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui) + // if you're using a language detector, do not define the lng option + lng: 'en', + fallbackLng: 'en', + // debug: true, + interpolation: { + escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape + }, + resources, + }); + +export default i18n; diff --git a/002_source/ionic_mobile/src/i18n.ts b/002_source/ionic_mobile/src/i18n.ts index ed63dab..1d328aa 100644 --- a/002_source/ionic_mobile/src/i18n.ts +++ b/002_source/ionic_mobile/src/i18n.ts @@ -1,92 +1,6 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; - -// the translations -// (tip move them in a JSON file and import them) -const resources = { - en: { - translation: { - 'Lesson': 'Lesson', - 'Quiz': 'Quiz', - 'Favorite': 'Favorite', - 'Loading': 'Loading', - 'menu.link-home': 'Home', - 'menu.link-stats': 'Stats', - 'home.title': 'Remove duplicate songs from your Spotify library.', - 'home.description': - "Spotify Dedup cleans up your playlists and liked songs from your Spotify account. It's easy and fast.", - 'home.review': - 'Read what {{-supportersCount}} supporters think about Spotify Dedup on {{- linkOpen}}Buy Me a Coffee{{- linkClose}}', - 'home.login-button': 'Log in with Spotify', - 'meta.title': 'Spotify Dedup - Remove duplicate songs from your Spotify library', - 'meta.description': - 'Delete repeated songs from your Spotify playlists and liked songs automatically. Fix your music library. Quickly and easy.', - 'features.find-remove.header': 'Find & remove', - 'features.find-remove.body': - 'Dedup checks your playlists and liked songs in {{- strongOpen}}your Spotify library{{- strongClose}}. Once Dedup finds duplicates you can remove them on a per-playlist basis.', - 'features.safer.header': 'Safer', - 'features.safer.body': - 'Dedup will only remove {{- strongOpen}}duplicate songs{{- strongClose}}, leaving the rest of the playlist and liked songs untouched.', - 'features.open-source.header': 'Open Source', - 'features.open-source.body': - "You might want to have a look at the {{- linkGithubOpen}}source code on GitHub{{- linkGithubClose}}. This web app uses the {{- linkWebApiOpen}}Spotify Web API{{- linkWebApiClose}} to manage user's playlists and liked songs.", - 'reviews.title': 'This is what users are saying', - 'footer.author': 'Made with ♥ by {{- linkOpen}}JMPerez 👨‍💻{{- linkClose}}', - 'footer.github': 'Check out the {{- linkOpen}}code on GitHub 📃{{- linkClose}}', - 'footer.bmc': 'Support the project {{- linkOpen}}buying a coffee ☕{{- linkClose}}', - 'bmc.button': 'Would you buy me a coffee?', - 'result.duplicate.reason-same-id': 'Duplicate', - 'result.duplicate.reason-same-data': 'Duplicate (same name, artist and duration)', - 'result.duplicate.track': '<0>{{trackName}} <2>by <4>{{trackArtistName}}', - 'process.status.finding': 'Finding duplicates in your playlists and liked songs…', - 'process.status.complete': 'Processing complete!', - 'process.status.complete.body': 'Your playlists and liked songs have been processed!', - 'process.status.complete.dups.body': - 'Click on the {{- strongOpen}}Remove duplicates{{- strongClose}} button to get rid of duplicates in that playlist or liked songs collection.', - 'process.status.complete.nodups.body': "Congrats! You don't have duplicates in your playlists nor liked songs.", - 'process.reading-library': 'Going through your library, finding the playlists you own and your liked songs…', - 'process.processing_one': 'Searching for duplicate songs, wait a sec. Still to process {{count}} playlist…', - 'process.processing_other': 'Searching for duplicate songs, wait a sec. Still to process {{count}} playlists…', - 'process.saved.title': 'liked songs in your library', - 'process.saved.duplicates_one': 'This collection has {{count}} duplicate song', - 'process.saved.duplicates_other': 'This collection has {{count}} duplicate songs', - 'process.saved.remove-button': 'Remove duplicates from your liked songs', - 'process.playlist.duplicates_one': 'This playlist has {{count}} duplicate song', - 'process.playlist.duplicates_other': 'This playlist has {{count}} duplicate songs', - 'process.playlist.remove-button': 'Remove duplicates from this playlist', - 'process.items.removed': 'Duplicates removed', - 'faq.section-title': 'Frequently asked questions', - 'faq.question-1': 'What does this web application do?', - 'faq.answer-1': - 'Spotify Dedup helps you clean up your music libraries on Spotify by identifying and deleting duplicate songs across playlists and liked songs.', - 'faq.question-2': 'How does it find duplicates?', - 'faq.answer-2': - "Dedup finds duplicates based on the songs identifier, title, artist, and duration similarity. It identifies duplicates that Spotify's application does not catch.", - 'faq.question-3': "How is Dedup better than Spotify's built-in duplicate detection?", - 'faq.answer-3': - "Spotify's applications only warn about duplicates when adding a song to a playlit or liked songs with the exact same song identifier. However, the same song can have multiple identifiers on Spotify that both in the same release or in several ones. Dedup detects duplicates based on title, artist, and duration similarity.", - 'faq.question-4': 'When duplicates are found, which songs are removed?', - 'faq.answer-4': 'Dedup will keep the first song within a group of duplicate songs, and will remove the rest.', - 'faq.question-5': 'Is my data safe with this web application?', - 'faq.answer-5': - 'Yes, this web application does not store any user data on its servers. It only requests the minimum set of permissions necessary to process your library.', - 'faq.question-6': 'What permissions does this web application require?', - 'faq.answer-6': - "This web application uses Spotify's authentication service to access your liked songs and playlists in your library.", - 'faq.question-7': 'How has this tool been tested?', - 'faq.answer-7': - 'This tool has been battle-tested by thousands of users who have used it to identify duplicates in millions of playlists since 2014.', - 'faq.question-8': 'Can this tool delete duplicates across multiple playlists?', - 'faq.answer-8': - "This tool can identify and delete duplicates on all playlists in a library, but doesn't detect duplicates of a song across multiple playlists.", - 'faq.question-9': 'How can I revoke the permissions granted to this web application?', - 'faq.answer-9': - "You can revoke the permissions granted to this web application at any time on your Spotify account, under the 'Apps' section.", - 'faq.question-10': 'Does this tool work with other music streaming services?', - 'faq.answer-10': "No, this tool only works with Spotify through Spotify's Web API.", - }, - }, -}; +import resources from './locale/resources'; i18n .use(initReactI18next) // passes i18n down to react-i18next @@ -94,11 +8,16 @@ i18n // the translations // (tip move them in a JSON file and import them, // or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui) - resources, + // + // TODO: need to troubleshoot resource for building webpage + // resources, + // // if you're using a language detector, do not define the lng option lng: 'en', fallbackLng: 'en', - + // + // debug: true, + // interpolation: { escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape }, diff --git a/002_source/ionic_mobile/src/locale/en.ts b/002_source/ionic_mobile/src/locale/en.ts new file mode 100644 index 0000000..f7dd738 --- /dev/null +++ b/002_source/ionic_mobile/src/locale/en.ts @@ -0,0 +1,11 @@ +// the translations +// (tip move them in a JSON file and import them) +const en = { + translation: { + hello: 'world', + Loading: 'loading', + lesson: 'lesson', + }, +}; + +export default en; diff --git a/002_source/ionic_mobile/src/locale/resources.ts b/002_source/ionic_mobile/src/locale/resources.ts new file mode 100644 index 0000000..f74d0b7 --- /dev/null +++ b/002_source/ionic_mobile/src/locale/resources.ts @@ -0,0 +1,5 @@ +import en from './en'; + +export default { + en, +}; diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx index aac79e5..53ffa93 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx @@ -49,32 +49,30 @@ const LessonContainer: React.FC = ({ lesson_type_id: lesson_type }} > {selected_content.map((content: any, cat_idx: number) => ( - <> - { - // TODO: review the layout type `v` and `c` - router.push(`/lesson_word_page/v/${lesson_type_id}/${content.id}/0`, undefined, 'replace'); - }} - > -
-
- {content.cat_name} -
-
- + { + // TODO: review the layout type `v` and `c` + router.push(`/lesson_word_page/v/${lesson_type_id}/${content.id}/0`, undefined, 'replace'); + }} + > +
+
+ {content.cat_name} +
+
))} {/* */} diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx deleted file mode 100644 index 8fe3e79..0000000 --- a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { FunctionComponent, useEffect } from 'react'; -import { useGlobalAudioPlayer } from 'react-use-audio-player'; -import { useListeningPracticeTimeSpent } from '../../../contexts/MyIonMetric/ListeningPracticeTimeSpent'; - -export const AudioControls: FunctionComponent<{ audio_src: string }> = ({ audio_src }) => { - const { play, pause, playing, duration } = useGlobalAudioPlayer(); - const { load, src: loadedSrc } = useGlobalAudioPlayer(); - let { myIonMetricIncListeningPracticeTimeSpent } = useListeningPracticeTimeSpent(); - - useEffect(() => { - if (audio_src) { - load(audio_src); - } - }, [audio_src]); - - useEffect(() => { - if (loadedSrc) { - } - }, [loadedSrc]); - - useEffect(() => { - if (playing) { - myIonMetricIncListeningPracticeTimeSpent(duration); - } - }, [playing]); - - return <>; -}; diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/index.tsx deleted file mode 100644 index a6ca557..0000000 --- a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/index.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { IonButton, IonButtons, IonContent, IonIcon, IonModal, IonPage, IonToolbar, useIonRouter } from '@ionic/react'; -import './style.css'; -import { - arrowBackCircleOutline, - chevronBack, - chevronForward, - close, - heart, - heartOutline, - play, - volumeHighOutline, -} from 'ionicons/icons'; -import { useEffect, useRef, useState } from 'react'; -// -import Markdown from 'react-markdown'; -import { useParams } from 'react-router'; -import { useGlobalAudioPlayer } from 'react-use-audio-player'; -import remarkGfm from 'remark-gfm'; -import { LoadingScreen } from '../../../components/LoadingScreen'; -import RemoveFavoritePrompt from '../../../components/RemoveFavoritePrompt'; -import { LESSON_LINK } from '../../../constants'; -import { useMyIonFavorite } from '../../../contexts/MyIonFavorite'; -// -import { useMyIonStore } from '../../../contexts/MyIonStore'; -import ILesson from '../../../interfaces/ILesson'; -import ILessonCategory from '../../../interfaces/ILessonCategory'; -import IWordCard from '../../../interfaces/IWordCard'; -import { getFavLessonVocabularyLink, getLessonVocabularyLink } from '../getLessonWordLink'; -import { AudioControls } from './AudioControls'; - -const LessonWordPageByDb: React.FC = () => { - const [loading, setLoading] = useState(true); - - const router = useIonRouter(); - const modal = useRef(null); - const { lesson_idx, cat_idx, word_idx } = useParams<{ lesson_idx: string; cat_idx: string; word_idx: string }>(); - - const [open_remove_modal, setOpenRemoveModal] = useState(false); - const [lesson_info, setLessonInfo] = useState(undefined); - const [cat_info, setCatInfo] = useState(undefined); - const [word_info, setWordInfo] = useState(undefined); - - // const { play: play_word, playing } = useGlobalAudioPlayer(); - const { myIonStoreAddFavoriteVocabulary, myIonStoreRemoveFavoriteVocabulary, myIonStoreFindInFavoriteVocabulary } = - useMyIonFavorite(); - let [favorite_address, setFavoriteAddress] = useState(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - - useEffect(() => { - setFavoriteAddress(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - - // if (lesson_contents.length > 0) { - // let lesson_content: ILesson = lesson_contents[parseInt(lesson_idx)]; - // let category_content: ILessonCategory = lesson_content.content[parseInt(cat_idx)]; - // let word_content: IWordCard = category_content.content[parseInt(word_idx)]; - // setWordInfo(word_content); - // } - }, [lesson_idx, cat_idx, word_idx]); - - function dismiss() { - setOpenRemoveModal(false); - } - const [isOpen, setIsOpen] = useState(false); - - let { lesson_contents } = useMyIonStore(); - - useEffect(() => { - // NOTES: lesson_content == [] during loading - if (lesson_contents.length > 0) { - let lesson_content: ILesson = lesson_contents[parseInt(lesson_idx)]; - let category_content: ILessonCategory = lesson_content.content[parseInt(cat_idx)]; - let word_content: IWordCard = category_content.content[parseInt(word_idx)]; - - setLessonInfo(lesson_content); - setCatInfo(category_content); - setWordInfo(word_content); - - setLoading(false); - } - }, [lesson_contents]); - - let [in_fav, setInFav] = useState(false); - const isInFavorite = async (string_to_search: string) => { - let result = await myIonStoreFindInFavoriteVocabulary(string_to_search); - setInFav(result); - }; - - const addToFavorite = async (string_to_add: string) => { - await myIonStoreAddFavoriteVocabulary(string_to_add); - - await isInFavorite(string_to_add); - setInFav(!in_fav); - }; - - function handleUserRemoveFavorite() { - setOpenRemoveModal(true); - } - - const removeFromFavorite = async (string_to_remove: string) => { - await myIonStoreRemoveFavoriteVocabulary(string_to_remove); - await isInFavorite(string_to_remove); - }; - - useEffect(() => { - (async () => { - await isInFavorite(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - })(); - }, [lesson_idx, cat_idx, word_idx]); - - return <>should not see me; - - if (lesson_info == undefined) return ; - if (!cat_info || !word_info) return ; - - return ( - <> - - -
- { - router.push(`${LESSON_LINK}/a/${lesson_info.name}`); - }} - > - - -
-
-
{cat_info.cat_name}
-
-
- {/* */} -
-
- { - router.push( - getLessonVocabularyLink(lesson_idx, cat_idx, Math.max(0, parseInt(word_idx) - 1).toString()), - ); - }} - > - - -
-
-
- { - router.push( - getLessonVocabularyLink( - lesson_idx, - cat_idx, - Math.min(cat_info.content.length - 1, parseInt(word_idx) + 1).toString(), - ), - ); - }} - > - - -
-
- -
-
- {parseInt(word_idx) + 1} -
-
- -
-
-
- - (playing ? null : play_word())} - > - - -
-
{word_info.word}
-
- { - in_fav ? handleUserRemoveFavorite() : addToFavorite(favorite_address); - }} - > - - -
-
- -
-
{word_info.word_c}
-
-
- -
- {word_info.sample_e} - {word_info.sample_c} -
-
-
-
- {/* */} - - - - - - dismiss()} shape="round" fill="clear"> - - - - -
-
-
Are you sure to remove favorite ?
- -
-
- dismiss()} fill="outline"> - Cancel - -
-
- { - removeFromFavorite(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - setIsOpen(true); - dismiss(); - }} - fill="solid" - color="danger" - > - Remove - -
-
-
-
-
-
- - - - ); -}; - -export default LessonWordPageByDb; diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/style.css b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/style.css deleted file mode 100644 index a76cae3..0000000 --- a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/style.css +++ /dev/null @@ -1,31 +0,0 @@ -.bold { - font-weight: bold; -} - -ion-modal#example-modal { - --height: 33%; - --width: 80%; - --border-radius: 16px; - --box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); -} - -ion-modal#example-modal::part(backdrop) { - /* background: rgba(209, 213, 219); */ - opacity: 1; -} - -ion-modal#example-modal ion-toolbar { - /* --background: rgb(14 116 144); */ - /* --color: white; */ - --color: black; -} - -ion-toast.custom-toast::part(message) { - text-align: center; - font-size: 1.5rem; - color: rgba(0, 0, 0, 0.9); -} - -ion-toast.custom-toast::part(container) { - bottom: 100px; -} diff --git a/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx index a2faf0a..092394e 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx @@ -24,6 +24,7 @@ import IWordCard from '../../../interfaces/IWordCard'; import { getFavLessonVocabularyLink, getLessonVocabularyLinkString } from '../../Lesson/getLessonWordLink'; import { AudioControls } from './AudioControls'; import useGetVocabularyRoute from '../../../hooks/useGetVocabularyRoute'; +import { Vocabulary } from '../../../types/Vocabularies'; const WordPage: React.FC = () => { const [loading, setLoading] = useState(true); @@ -67,12 +68,11 @@ const WordPage: React.FC = () => { let [lastWord, setLastWord] = useState(false); useEffect(() => { - if (tempResult) { + if (tempResult && tempResult.items.length > 0) { + let temp1 = tempResult.items[parseInt(word_idx)] as unknown as Vocabulary; try { - setCatInfo(tempResult.items[parseInt(word_idx)].expand.cat_id as unknown as ILessonCategory); - // - setWordInfo(tempResult.items[parseInt(word_idx)] as unknown as IWordCard); - // + setCatInfo(temp1.expand.cat_id as unknown as ILessonCategory); + setWordInfo(temp1 as unknown as IWordCard); setLastWord(parseInt(word_idx) === tempResult.items.length - 1); } catch (error) { console.error(error); @@ -81,12 +81,11 @@ const WordPage: React.FC = () => { }, [lesson_idx, cat_idx, word_idx]); useEffect(() => { - // console.log({ lesson_idx, cat_idx, word_idx }); - if (tempResult) { + if (tempResult && tempResult.items.length > 0) { + let temp1 = tempResult.items[parseInt(word_idx)] as unknown as Vocabulary; try { - setCatInfo(tempResult.items[parseInt(word_idx)].expand.cat_id as unknown as ILessonCategory); - // - setWordInfo(tempResult.items[parseInt(word_idx)] as unknown as IWordCard); + setCatInfo(temp1.expand.cat_id as unknown as ILessonCategory); + setWordInfo(temp1 as unknown as IWordCard); // setLastWord(parseInt(word_idx) === tempResult.items.length - 1); } catch (error) { diff --git a/002_source/ionic_mobile/src/pages/Lesson/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/index.tsx index 4cc31a1..18e721f 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/index.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/index.tsx @@ -70,7 +70,9 @@ const Lesson: React.FC = () => { ) : ( <> )} - {t('lesson')} + + {t('lesson')} {t('hello')} + diff --git a/002_source/ionic_mobile/src/types/Vocabularies.tsx b/002_source/ionic_mobile/src/types/Vocabularies.tsx index 22376b1..4d448ba 100644 --- a/002_source/ionic_mobile/src/types/Vocabularies.tsx +++ b/002_source/ionic_mobile/src/types/Vocabularies.tsx @@ -1,5 +1,6 @@ // RULES: interface for handling vocabulary record -type Vocabulary = { + +export type Vocabulary = { // id: string; collectionId: string; @@ -14,11 +15,7 @@ type Vocabulary = { image: File[]; sound: File[]; // - expand?: { + expand: { cat_id: LessonCategory; }; }; - -type Vocabularies = Vocabulary[]; - -export default Vocabularies; diff --git a/002_source/ionic_mobile/vite.config.ts b/002_source/ionic_mobile/vite.config.ts index 92e8eea..da932a1 100644 --- a/002_source/ionic_mobile/vite.config.ts +++ b/002_source/ionic_mobile/vite.config.ts @@ -10,6 +10,6 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', - setupFiles: './src/setupTests.ts' - } + setupFiles: './src/setupTests.ts', + }, });