From 4c72861eda105dc5cfe00257bedfd3ddf608f54d Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Mon, 28 Apr 2025 08:03:21 +0800 Subject: [PATCH] update, --- 002_source/ionic_mobile/src/RouteConfig.tsx | 15 +- 002_source/ionic_mobile/src/constants.tsx | 1 + .../ionic_mobile/src/contexts/index.tsx | 12 +- .../src/hooks/useGetVocabularyRoute.tsx | 29 ++ .../ionic_mobile/src/hooks/useHelloworld.tsx | 15 + .../src/hooks/useListAllLessonTypes.tsx | 38 +++ .../ionic_mobile/src/hooks/usePocketBase.tsx | 49 +++ .../ionic_mobile/src/interfaces/IWordCard.tsx | 2 + .../src/pages/Lesson/LessonContainer.tsx | 82 ----- .../pages/Lesson/LessonContainer/Lesson.css | 0 .../pages/Lesson/LessonContainer/index.tsx | 91 +++++ .../LessonWordPageByDb/AudioControls.tsx | 28 ++ .../pages/Lesson/LessonWordPageByDb/index.tsx | 319 ++++++++++++++++++ .../pages/Lesson/LessonWordPageByDb/style.css | 31 ++ .../src/pages/Lesson/WordPage/index.tsx | 281 +++++++-------- .../src/pages/Lesson/getLessonWordLink.tsx | 8 + .../ionic_mobile/src/pages/Lesson/index.tsx | 46 ++- .../src/types/LessonCategory.d.ts | 23 ++ .../ionic_mobile/src/types/LessonsTypes.tsx | 20 ++ .../ionic_mobile/src/types/Vocabularies.tsx | 24 ++ 20 files changed, 877 insertions(+), 237 deletions(-) create mode 100644 002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx create mode 100644 002_source/ionic_mobile/src/hooks/useHelloworld.tsx create mode 100644 002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx create mode 100644 002_source/ionic_mobile/src/hooks/usePocketBase.tsx delete mode 100644 002_source/ionic_mobile/src/pages/Lesson/LessonContainer.tsx create mode 100644 002_source/ionic_mobile/src/pages/Lesson/LessonContainer/Lesson.css create mode 100644 002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx create mode 100644 002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx create mode 100644 002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/index.tsx create mode 100644 002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/style.css create mode 100644 002_source/ionic_mobile/src/types/LessonCategory.d.ts create mode 100644 002_source/ionic_mobile/src/types/LessonsTypes.tsx create mode 100644 002_source/ionic_mobile/src/types/Vocabularies.tsx diff --git a/002_source/ionic_mobile/src/RouteConfig.tsx b/002_source/ionic_mobile/src/RouteConfig.tsx index ad8c4b5..e60b472 100644 --- a/002_source/ionic_mobile/src/RouteConfig.tsx +++ b/002_source/ionic_mobile/src/RouteConfig.tsx @@ -22,7 +22,10 @@ import FavVocabularyPage from './pages/Favorite/Vocabulary'; import FavoriteVocabularyPage from './pages/Favorite/WordPage'; import ConnectivesPage from './pages/Lesson/ConnectivesPage'; import Lesson from './pages/Lesson/index'; -import LessonWordPage from './pages/Lesson/WordPage'; + +// NOTES: old version using json file +import LessonWordPageByDb from './pages/Lesson/LessonWordPageByDb'; +import WordPage from './pages/Lesson/WordPage'; // import ListeningPractice from './pages/ListeningPractice'; import PracticeFinish from './pages/ListeningPractice/Finish'; @@ -58,7 +61,7 @@ function RouteConfig() { - + {/* */} @@ -122,9 +125,17 @@ function RouteConfig() { + {/* TODO: change lesson_idx to lesson_type_idx, need to modify LessonWordPage as well */} + {/* http://localhost:5173/lesson_word_page/v/000000000000001/000000000000003/0 */} + + + + + {/* + */} diff --git a/002_source/ionic_mobile/src/constants.tsx b/002_source/ionic_mobile/src/constants.tsx index 0776849..2316ef3 100644 --- a/002_source/ionic_mobile/src/constants.tsx +++ b/002_source/ionic_mobile/src/constants.tsx @@ -1,3 +1,4 @@ +// CHANGELOG: // 0.0.1 - implement screen // 0.0.2 - implement logic // 0.0.3 - first demo diff --git a/002_source/ionic_mobile/src/contexts/index.tsx b/002_source/ionic_mobile/src/contexts/index.tsx index 6d19c9b..2a8d1b0 100644 --- a/002_source/ionic_mobile/src/contexts/index.tsx +++ b/002_source/ionic_mobile/src/contexts/index.tsx @@ -1,8 +1,12 @@ +import { PocketBaseProvider } from '../hooks/usePocketBase'; import { AppStateProvider } from './AppState'; import { MyIonFavoriteProvider } from './MyIonFavorite'; import { MyIonMetricProvider } from './MyIonMetric'; import { MyIonQuizProvider } from './MyIonQuiz'; import { MyIonStoreProvider } from './MyIonStore'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient(); const ContextMeta = ({ children }: { children: React.ReactNode }) => { return ( @@ -12,8 +16,12 @@ const ContextMeta = ({ children }: { children: React.ReactNode }) => { - {/* */} - {children} + + + {children} + {/* */} + + diff --git a/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx b/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx new file mode 100644 index 0000000..bb08fe2 --- /dev/null +++ b/002_source/ionic_mobile/src/hooks/useGetVocabularyRoute.tsx @@ -0,0 +1,29 @@ +import { usePocketBase } from './usePocketBase.tsx'; +import type LessonsTypes from '../types/LessonsTypes'; +import { useQuery } from '@tanstack/react-query'; +import Vocabularies from '../types/Vocabularies.tsx'; + +const useGetVocabularyRoute = (lessonType: string, catId: string) => { + const { user, pb } = usePocketBase(); + return useQuery({ + queryKey: ['useGetVocabularyRoute', lessonType, catId, 'feeds', 'all', user?.id || ''], + staleTime: 60 * 1000, + queryFn: async ({ + queryKey, + }: { + queryKey: ['useGetVocabularyRoute', string, string, 'feeds', 'all', string | null]; + }) => { + console.log('calling useGetLessonCategoriesRoute'); + return await pb.collection('Vocabularies').getList(1, 9999, { + // TODO: sort by field -> pos + sort: 'id', + filter: `lesson_type_id = "${lessonType}" && cat_id = "${catId}"`, + $autoCancel: false, + expand: 'lesson_type_id,cat_id', + }); + }, + // enabled: !!user?.id, + }); +}; + +export default useGetVocabularyRoute; diff --git a/002_source/ionic_mobile/src/hooks/useHelloworld.tsx b/002_source/ionic_mobile/src/hooks/useHelloworld.tsx new file mode 100644 index 0000000..e2ff7c4 --- /dev/null +++ b/002_source/ionic_mobile/src/hooks/useHelloworld.tsx @@ -0,0 +1,15 @@ +import React from 'react' + +export default function useHelloworld() { + const [count, setCount] = React.useState(0) + + const handleIncrement = () => { + // setCount(count + 1) + console.log("helloworld") + } + + return { + count, + handleIncrement, + } +} diff --git a/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx b/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx new file mode 100644 index 0000000..484aa63 --- /dev/null +++ b/002_source/ionic_mobile/src/hooks/useListAllLessonTypes.tsx @@ -0,0 +1,38 @@ +import { usePocketBase } from './usePocketBase.tsx'; +import { useQuery } from '@tanstack/react-query'; +import { LessonsType } from '../types/LessonsTypes'; + +const useListAllLessonTypes = () => { + const { user, pb } = usePocketBase(); + return useQuery({ + queryKey: [ + 'useListAllLessonTypes', + 'feeds', + 'all', + user?.id || '', + // + ], + staleTime: 60 * 1000, + queryFn: async ({ + queryKey, + }: { + queryKey: [ + 'useListAllLessonTypes', + 'feeds', + 'all', + string | null, + // + ]; + }) => { + console.log('calling useListAllLessonTypes'); + return await pb.collection('LessonsTypes').getFullList({ + // TODO: sort by field -> pos + sort: 'id', + $autoCancel: false, + }); + }, + // enabled: !!user?.id, + }); +}; + +export default useListAllLessonTypes; diff --git a/002_source/ionic_mobile/src/hooks/usePocketBase.tsx b/002_source/ionic_mobile/src/hooks/usePocketBase.tsx new file mode 100644 index 0000000..fe292ed --- /dev/null +++ b/002_source/ionic_mobile/src/hooks/usePocketBase.tsx @@ -0,0 +1,49 @@ +import { useCallback, useState, useEffect, createContext, useContext } from 'react'; +import PocketBase, { AuthRecord } from 'pocketbase'; +type PocketBaseContextValue = { + pb: PocketBase; + user: AuthRecord; + logout: () => void; +}; +const PocketBaseContext = createContext(null); + +const POCKETBASE_URL = import.meta.env.VITE_POCKETBASE_URL; + +export const PocketBaseProvider = ({ children }: { children: any }) => { + const [pb, _] = useState(new PocketBase(POCKETBASE_URL)); + const [user, setUser] = useState(pb.authStore.record); + + useEffect(() => { + // Update user state when auth store changes + const unsubscribe = pb.authStore.onChange((_, model) => { + setUser(model); + }); + + return unsubscribe; + }, [pb.authStore]); + + const logout = useCallback(() => pb.authStore.clear(), [pb.authStore]); + + return {children}; +}; + +export const usePocketBase = () => { + const context = useContext(PocketBaseContext); + if (context === null) { + throw new Error('usePocketBase must be used within a PocketBaseProvider'); + } + return context; +}; + +export const useRequireAuth = () => { + const { pb, user } = usePocketBase(); + const navigate = useNavigate(); + + useEffect(() => { + if (!pb.authStore.isValid) { + navigate(URLS.LOGIN); + } + }, [pb.authStore.isValid, navigate]); + + return user; +}; diff --git a/002_source/ionic_mobile/src/interfaces/IWordCard.tsx b/002_source/ionic_mobile/src/interfaces/IWordCard.tsx index 9091c80..6584409 100644 --- a/002_source/ionic_mobile/src/interfaces/IWordCard.tsx +++ b/002_source/ionic_mobile/src/interfaces/IWordCard.tsx @@ -9,6 +9,8 @@ interface IWordCard { sample_c: string; image_url: string; sound_url: string; + // + id: string; } export default IWordCard; diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonContainer.tsx b/002_source/ionic_mobile/src/pages/Lesson/LessonContainer.tsx deleted file mode 100644 index 21ddcbc..0000000 --- a/002_source/ionic_mobile/src/pages/Lesson/LessonContainer.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { IonButton, useIonRouter } from '@ionic/react'; -import './Lesson.css'; -import { useEffect, useState } from 'react'; -import { LoadingScreen } from '../../components/LoadingScreen'; -import { COLOR_TEXT } from '../../constants'; -import { useMyIonStore } from '../../contexts/MyIonStore'; -import { listLessonCategories } from '../../public_data/listLessonCategories'; -import { getLessonConnectivesLink } from './getLessonConnectivesLink'; -import { getLessonVocabularyLink } from './getLessonWordLink'; - -let lessonLinkProxy = [getLessonVocabularyLink, getLessonConnectivesLink]; - -interface ContainerProps { - test_active_lesson_idx: number; -} - -const LessonContainer: React.FC = ({ test_active_lesson_idx = 1 }) => { - const router = useIonRouter(); - const [loading, setLoading] = useState(true); - const { lesson_contents, setLessonContent } = useMyIonStore(); - const [active_lesson_idx, setActiveLessonIdx] = useState(0); - const [selected_content, setSelectedContent] = useState([]); - - useEffect(() => { - listLessonCategories().then((cats: any) => { - console.log({ cats }); - setLessonContent(cats); - setActiveLessonIdx(test_active_lesson_idx); - setLoading(false); - }); - }, []); - - useEffect(() => { - if (loading) return; - console.log('active_category changed', active_lesson_idx); - let selected_category = lesson_contents[test_active_lesson_idx]; - setSelectedContent(selected_category['content']); - }, [active_lesson_idx, loading, test_active_lesson_idx]); - - if (loading) return ; - - return ( - <> -
- {selected_content.map((content: any, cat_idx: number) => ( - { - router.push( - lessonLinkProxy[test_active_lesson_idx](test_active_lesson_idx.toString(), cat_idx.toString(), '0'), - undefined, - 'replace', - ); - }} - > -
-
- {content.cat_name} -
-
- ))} -
- {/* */} - - ); -}; - -export default LessonContainer; diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/Lesson.css b/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/Lesson.css new file mode 100644 index 0000000..e69de29 diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx new file mode 100644 index 0000000..1a9846e --- /dev/null +++ b/002_source/ionic_mobile/src/pages/Lesson/LessonContainer/index.tsx @@ -0,0 +1,91 @@ +import { IonButton, useIonRouter } from '@ionic/react'; +import './Lesson.css'; +import { useEffect, useState } from 'react'; +import { LoadingScreen } from '../../../components/LoadingScreen'; +import { COLOR_TEXT } from '../../../constants'; +import { useMyIonStore } from '../../../contexts/MyIonStore'; +import { listLessonCategories } from '../../../public_data/listLessonCategories'; +import { getLessonConnectivesLink } from '../getLessonConnectivesLink'; +import { getLessonVocabularyLink } from '../getLessonWordLink'; + +let lessonLinkProxy = [getLessonVocabularyLink, getLessonConnectivesLink]; + +interface ContainerProps { + test_active_lesson_idx: number; + lesson_type_id: string; +} + +const LessonContainer: React.FC = ({ + test_active_lesson_idx = 1, + lesson_type_id = '000000000000001', +}) => { + const router = useIonRouter(); + const [loading, setLoading] = useState(true); + const { lesson_contents, setLessonContent } = useMyIonStore(); + const [active_lesson_idx, setActiveLessonIdx] = useState(0); + const [selected_content, setSelectedContent] = useState([]); + + useEffect(() => { + listLessonCategories().then((cats: any) => { + console.log({ cats }); + setLessonContent(cats); + setActiveLessonIdx(test_active_lesson_idx); + setLoading(false); + }); + }, []); + + useEffect(() => { + if (loading) return; + console.log('active_category changed', active_lesson_idx); + let selected_category = lesson_contents[test_active_lesson_idx]; + setSelectedContent(selected_category['content']); + }, [active_lesson_idx, loading, test_active_lesson_idx]); + + if (loading) return ; + + return ( + <> +
+ {selected_content.map((content: any, cat_idx: number) => ( + <> + { + router.push(`/lesson_word_page/v/${lesson_type_id}/${content.id}/0`, undefined, 'replace'); + }} + > +
+
+ {content.cat_name} +
+
+ + ))} +
+ {/* */} + + ); +}; + +export default LessonContainer; diff --git a/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx new file mode 100644 index 0000000..8fe3e79 --- /dev/null +++ b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/AudioControls.tsx @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..a6ca557 --- /dev/null +++ b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/index.tsx @@ -0,0 +1,319 @@ +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 new file mode 100644 index 0000000..a76cae3 --- /dev/null +++ b/002_source/ionic_mobile/src/pages/Lesson/LessonWordPageByDb/style.css @@ -0,0 +1,31 @@ +.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 21a3552..5bede82 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx @@ -10,8 +10,7 @@ import { play, volumeHighOutline, } from 'ionicons/icons'; -import { useEffect, useRef, useState } from 'react'; -// import { StoreContext, useMyIonStore } from '../../contexts/store'; +import React, { useEffect, useRef, useState } from 'react'; // import Markdown from 'react-markdown'; import { useParams } from 'react-router'; @@ -26,19 +25,25 @@ import { useMyIonStore } from '../../../contexts/MyIonStore'; import ILesson from '../../../interfaces/ILesson'; import ILessonCategory from '../../../interfaces/ILessonCategory'; import IWordCard from '../../../interfaces/IWordCard'; -import { getFavLessonVocabularyLink, getLessonVocabularyLink } from '../../Lesson/getLessonWordLink'; +import { + getFavLessonVocabularyLink, + getLessonVocabularyLink, + getLessonVocabularyLinkString, +} from '../../Lesson/getLessonWordLink'; import { AudioControls } from './AudioControls'; +import useGetVocabularyRoute from '../../../hooks/useGetVocabularyRoute'; +import { UseQueryResult } from '@tanstack/react-query'; +import { ListResult } from 'pocketbase'; +import LessonsTypes from '../../../types/LessonsTypes'; -// - -const LessonWordPage: React.FC = () => { - const router = useIonRouter(); +function WordPage(): React.JSX.Element { const [loading, setLoading] = useState(true); - const [open_remove_modal, setOpenRemoveModal] = useState(false); + 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); @@ -46,78 +51,64 @@ const LessonWordPage: React.FC = () => { const { play: play_word, playing } = useGlobalAudioPlayer(); const { myIonStoreAddFavoriteVocabulary, myIonStoreRemoveFavoriteVocabulary, myIonStoreFindInFavoriteVocabulary } = useMyIonFavorite(); - // - // const lesson_vocab_address = `/lesson_word_page/v/${lesson_idx}/${cat_idx}/${word_idx}`; let [favorite_address, setFavoriteAddress] = useState(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - useEffect(() => { - setFavoriteAddress(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); + let { status, data: tempResult } = useGetVocabularyRoute(lesson_idx, cat_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); + function getFile(recordId: string, fileName: string) { + return `http://127.0.0.1:8090/api/files/Vocabularies/${recordId}/${fileName}`; } - const removeFromFavorite = async (string_to_remove: string) => { - await myIonStoreRemoveFavoriteVocabulary(string_to_remove); - await isInFavorite(string_to_remove); - }; + useEffect(() => { + setLoading(!(status === 'success')); + }, [status]); + + // useEffect(() => { + // // console.log({ lesson_idx, cat_idx, word_idx }); + // if (tempResult) { + // try { + // setCatInfo(tempResult.items[parseInt(word_idx)].expand.cat_id as unknown as ILessonCategory); + // // + // setWordInfo(tempResult.items[parseInt(word_idx)] as unknown as IWordCard); + // } catch (error) { + // console.error(error); + // } + // } + // }, [lesson_idx, cat_idx, word_idx]); + let [lastWord, setLastWord] = useState(false); useEffect(() => { - (async () => { - await isInFavorite(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx)); - })(); + if (tempResult) { + try { + setCatInfo(tempResult.items[parseInt(word_idx)].expand.cat_id as unknown as ILessonCategory); + // + setWordInfo(tempResult.items[parseInt(word_idx)] as unknown as IWordCard); + // + setLastWord(parseInt(word_idx) === tempResult.items.length - 1); + } catch (error) { + console.error(error); + } + } }, [lesson_idx, cat_idx, word_idx]); - // if (loading) return <>loading; - // if (!word_info) return <>loading; - - if (lesson_info == undefined) return ; + useEffect(() => { + // console.log({ lesson_idx, cat_idx, word_idx }); + if (tempResult) { + try { + setCatInfo(tempResult.items[parseInt(word_idx)].expand.cat_id as unknown as ILessonCategory); + // + setWordInfo(tempResult.items[parseInt(word_idx)] as unknown as IWordCard); + // + setLastWord(parseInt(word_idx) === tempResult.items.length - 1); + } catch (error) { + console.error(error); + } + } + }, [tempResult]); if (!cat_info || !word_info) return ; + if (!tempResult) return ; + return ( <> @@ -128,9 +119,8 @@ const LessonWordPage: React.FC = () => { shape="round" fill="clear" color={'dark'} - // href={`${LESSON_LINK}/a/${lesson_info.name}`} onClick={() => { - router.push(`${LESSON_LINK}/a/${lesson_info.name}`); + router.push(`${LESSON_LINK}/a/Vocabulary`); }} > @@ -152,42 +142,67 @@ const LessonWordPage: React.FC = () => { // href={getLessonVocabularyLink(lesson_idx, cat_idx, Math.max(0, parseInt(word_idx) - 1).toString())} onClick={() => { router.push( - getLessonVocabularyLink(lesson_idx, cat_idx, Math.max(0, parseInt(word_idx) - 1).toString()), + getLessonVocabularyLinkString( + lesson_idx, + cat_idx, + Math.max(0, parseInt(word_idx) - 1).toString(), + ), ); }} > -
+ {word_info && word_info?.image != '' ? ( +
+ ) : ( +
+ empty pic +
+ )}
{ router.push( - getLessonVocabularyLink( + getLessonVocabularyLinkString( lesson_idx, cat_idx, - Math.min(cat_info.content.length - 1, parseInt(word_idx) + 1).toString(), + Math.min(tempResult.items.length - 1, parseInt(word_idx) + 1).toString(), ), ); }} @@ -216,10 +231,20 @@ const LessonWordPage: React.FC = () => {
- + {word_info && word_info?.sound != '' ? ( + + ) : ( + <> + )} { >
+
{word_info.word}
{ size="large" fill="clear" // id='open-modal' - onClick={() => { - in_fav ? handleUserRemoveFavorite() : addToFavorite(favorite_address); - }} + // onClick={() => { + // in_fav ? handleUserRemoveFavorite() : addToFavorite(favorite_address); + // }} >
-
+
{word_info.word_c}
@@ -272,55 +307,21 @@ const LessonWordPage: React.FC = () => { marginTop: '2rem', }} > - {word_info.sample_e} - {word_info.sample_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 LessonWordPage; +export default WordPage; diff --git a/002_source/ionic_mobile/src/pages/Lesson/getLessonWordLink.tsx b/002_source/ionic_mobile/src/pages/Lesson/getLessonWordLink.tsx index fb169f5..a02e95b 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/getLessonWordLink.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/getLessonWordLink.tsx @@ -1,5 +1,13 @@ import { FAVORITE_LINK } from '../../constants'; +export function getLessonVocabularyLinkString( + s_active_lesson_idx: string, + s_cat_idx: string, + s_word_idx: string, +): string { + return `/lesson_word_page/v/${s_active_lesson_idx}/${s_cat_idx}/${s_word_idx}`; +} + export function getLessonVocabularyLink(i_active_lesson_idx: string, i_cat_idx: string, i_word_idx: string): string { let s_active_lesson_idx = parseInt(i_active_lesson_idx); let s_cat_idx = parseInt(i_cat_idx); diff --git a/002_source/ionic_mobile/src/pages/Lesson/index.tsx b/002_source/ionic_mobile/src/pages/Lesson/index.tsx index 63281a7..a189692 100644 --- a/002_source/ionic_mobile/src/pages/Lesson/index.tsx +++ b/002_source/ionic_mobile/src/pages/Lesson/index.tsx @@ -25,6 +25,9 @@ import { LESSON_LINK } from '../../constants'; import { useMyIonStore } from '../../contexts/MyIonStore'; import { listLessonCategories } from '../../public_data/listLessonCategories'; import LessonContainer from './LessonContainer'; +import useHelloworld from '../../hooks/useHelloworld'; +import useListAllLessonTypes from '../../hooks/useListAllLessonTypes'; +import LessonsTypes, { LessonsType } from '../../types/LessonsTypes'; const Lesson: React.FC = () => { const { act_category } = useParams<{ act_category: string }>(); @@ -35,14 +38,14 @@ const Lesson: React.FC = () => { let [active_lesson_idx, setActiveLessonIdx] = useState(0); let [selected_content, setSelectedContent] = useState([]); - useEffect(() => { - listLessonCategories().then((cats: any) => { - console.log({ cats }); - setLessonContent(cats); - setActiveLessonIdx(0); - setLoading(false); - }); - }, []); + // useEffect(() => { + // listLessonCategories().then((cats: any) => { + // console.log({ cats }); + // setLessonContent(cats); + // setActiveLessonIdx(0); + // setLoading(false); + // }); + // }, []); useEffect(() => { if (loading) return; @@ -65,7 +68,24 @@ const Lesson: React.FC = () => { } }, [act_category, lesson_content]); + const lessonTypesQuery = useListAllLessonTypes(); + const [lessonTypes, setLessonTypes] = useState([]); + useEffect(() => { + if (lessonTypesQuery.status === 'success') { + const { data: tempLessonTypes } = lessonTypesQuery; + console.log({ tempLessonTypes }); + if (tempLessonTypes) { + setLessonTypes(tempLessonTypes); + } + + setLoading(false); + } + }, [lessonTypesQuery]); + if (loading) return ; + if (lessonTypes.length === 0) return ; + + // return
{JSON.stringify({ t: lessonTypes }, null, 2)}
; return ( @@ -98,6 +118,7 @@ const Lesson: React.FC = () => { + {/* */}
{ setActiveLessonIdx(e.detail.value)}> - {lesson_content.map((category: any, idx: number) => ( + {lessonTypes.map((lessonType: any, idx: number) => ( - {category.name} + {lessonType.name} ))} @@ -119,7 +140,10 @@ const Lesson: React.FC = () => {
{/* */} - + {/* */} diff --git a/002_source/ionic_mobile/src/types/LessonCategory.d.ts b/002_source/ionic_mobile/src/types/LessonCategory.d.ts new file mode 100644 index 0000000..1d3c6fe --- /dev/null +++ b/002_source/ionic_mobile/src/types/LessonCategory.d.ts @@ -0,0 +1,23 @@ +type LessonCategory = { + isEmpty?: boolean; + // + id: string; + collectionId: string; + // + cat_name: string; + cat_image_url?: string; + cat_image?: string; + pos: number; + visible: string; + lesson_id: string; + description: string; + remarks: string; + createdAt: Date; + // + name: string; + avatar: string; + email: string; + phone: string; + quota: number; + status: 'pending' | 'active' | 'blocked' | 'NA'; +}; diff --git a/002_source/ionic_mobile/src/types/LessonsTypes.tsx b/002_source/ionic_mobile/src/types/LessonsTypes.tsx new file mode 100644 index 0000000..cbba748 --- /dev/null +++ b/002_source/ionic_mobile/src/types/LessonsTypes.tsx @@ -0,0 +1,20 @@ +export type LessonsType = { + id: string; + isEmpty?: boolean; + name: string; + type: string; + pos: number; + visible: 'visible' | 'hidden'; + createdAt: Date; + // + // original + // id: string; + // name: string; + // + avatar?: string; + email: string; + phone?: string; + quota: number; + status: 'pending' | 'active' | 'blocked'; + // createdAt: Date; +}; diff --git a/002_source/ionic_mobile/src/types/Vocabularies.tsx b/002_source/ionic_mobile/src/types/Vocabularies.tsx new file mode 100644 index 0000000..22376b1 --- /dev/null +++ b/002_source/ionic_mobile/src/types/Vocabularies.tsx @@ -0,0 +1,24 @@ +// RULES: interface for handling vocabulary record +type Vocabulary = { + // + id: string; + collectionId: string; + // + word: string; + word_c: string; + sample_e: string; + sample_c: string; + cat_id: string; + category: string; + lesson_type_id: string; + image: File[]; + sound: File[]; + // + expand?: { + cat_id: LessonCategory; + }; +}; + +type Vocabularies = Vocabulary[]; + +export default Vocabularies;