init commit,

This commit is contained in:
louiscklaw
2025-04-26 10:08:01 +08:00
parent 7d70b5826b
commit d0ea7e5452
473 changed files with 29989 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
interface IQuestionMeta {
question_idx: number;
question_fh: string;
question_sh: string;
modal_ans: string;
options: string[];
}
interface IQuestionJson {
question_fh: string;
question_sh: string;
modal_ans: string;
options: string[];
}

View File

@@ -0,0 +1,195 @@
import { IonButton, IonIcon } from '@ionic/react';
import { arrowBackCircleOutline } from 'ionicons/icons';
import React, { useEffect, useRef, useState } from 'react';
import ConfirmUserQuitQuiz from '../../components/ConfirmUserQuitQuiz';
import CorrectAnswerToast from '../../components/CorrectAnswerToast';
import QuestionProgress from '../../components/QuestionProgress';
import WrongAnswerToast from '../../components/WrongAnswerToast';
import { CONNECTIVE_REVISION_LINK } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import { useConnectivesRevisionCorrectCount } from '../../contexts/MyIonMetric/ConnectivesRevisionCorrectCount';
interface IQuestionCard {
num_correct: number;
total_questions_num: number;
incNumCorrect: () => void;
// openCorrectToast: () => void;
// openWrongToast: () => void;
//
nextQuestion: () => void;
question_meta: IQuestionMeta;
//
answer_list: string[];
quiz_idx: number;
}
const QuizContent: React.FC<IQuestionCard> = ({
num_correct,
total_questions_num,
nextQuestion,
question_meta,
// openCorrectToast,
// openWrongToast,
incNumCorrect,
answer_list,
quiz_idx,
}) => {
let { question_idx, question_fh, question_sh, modal_ans } = question_meta;
const [ignore_user_tap, setIgnoreUserTap] = useState(false);
const [isOpenCorrectAnswer, setIsOpenCorrectAnswer] = useState(false);
const [isOpenWrongAnswer, setIsOpenWrongAnswer] = useState(false);
const [user_answer, setUserAnswer] = useState<string | undefined>(undefined);
let ref1 = useRef<HTMLIonButtonElement>(null);
let ref2 = useRef<HTMLIonButtonElement>(null);
let ref3 = useRef<HTMLIonButtonElement>(null);
let ref4 = useRef<HTMLIonButtonElement>(null);
let button_refs = [ref1, ref2, ref3, ref4];
const { myIonMetricIncConnectivesRevisionCorrectCount } = useConnectivesRevisionCorrectCount();
const { CONNECTIVES_REVISION_ANWERED_WAIT_S } = useAppStateContext();
function handleUserAnswer(answer: string, ref_button: React.RefObject<HTMLIonButtonElement>) {
if (ignore_user_tap) return;
setUserAnswer(answer);
if (answer.toLowerCase() === modal_ans.toLowerCase()) {
incNumCorrect();
// openCorrectToast();
setIsOpenCorrectAnswer(true);
myIonMetricIncConnectivesRevisionCorrectCount();
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'green';
if (ref_button && ref_button.current) ref_button.current.style.color = 'white';
} else {
// openWrongToast();
setIsOpenWrongAnswer(true);
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'red';
if (ref_button && ref_button.current) ref_button.current.style.color = 'white';
}
// setTimeout(() => {
// nextQuestion();
// ResetButtonsStyle();
// setIgnoreUserTap(false);
// }, CONNECTIVES_REVISION_ANWERED_WAIT_S * 1000);
}
useEffect(() => {
if (user_answer) {
if (!isOpenCorrectAnswer && !isOpenWrongAnswer) {
// assume all toast closed
setTimeout(() => {
nextQuestion();
ResetButtonsStyle();
setIgnoreUserTap(false);
}, 500);
}
}
}, [user_answer, isOpenCorrectAnswer, isOpenWrongAnswer]);
function ResetButtonsStyle() {
button_refs.forEach((ref) => {
if (ref && ref.current) ref.current.style.backgroundColor = 'unset';
if (ref && ref.current) ref.current.style.color = 'unset';
});
}
const [show_confirm_user_exit, setShowConfirmUserExit] = useState<boolean>(false);
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ width: '10vw' }}>
<IonButton color="dark" fill="clear" shape="round" onClick={() => setShowConfirmUserExit(true)}>
<IonIcon slot="icon-only" size="large" icon={arrowBackCircleOutline}></IonIcon>
</IonButton>
</div>
<div></div>
<div style={{ width: '10vw' }}></div>
</div>
{/* */}
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
//
margin: '1rem',
marginTop: '10vh',
}}
>
<QuestionProgress num_rating={num_correct} num_full_rating={total_questions_num} />
<div style={{ fontSize: '1.3rem' }}>
Quiz {quiz_idx} Q.{question_idx + 1}/{total_questions_num}
</div>
<div style={{ marginTop: '1rem', textAlign: 'center' }}>
Read the text below and choose the best connective.
</div>
<div
style={{
marginTop: '1rem',
padding: '3rem',
border: '1px solid black',
borderRadius: '1rem',
}}
>
{question_fh} _____ {question_sh}
</div>
<div
style={{
marginTop: '1rem',
width: '80%',
display: 'flex',
flexDirection: 'column',
gap: '1rem',
}}
>
{answer_list.map((connective, idx) => {
return (
<div>
<IonButton
color={'dark'}
ref={button_refs[idx]}
size={'large'}
fill="outline"
expand="block"
onClick={() => {
setIgnoreUserTap(true);
handleUserAnswer(connective, button_refs[idx]);
}}
>
{connective}
</IonButton>
</div>
);
})}
</div>
</div>
<ConfirmUserQuitQuiz
setShowConfirmUserExit={setShowConfirmUserExit}
show_confirm_user_exit={show_confirm_user_exit}
url_push_after_user_confirm={CONNECTIVE_REVISION_LINK}
/>
<CorrectAnswerToast isOpen={isOpenCorrectAnswer} dismiss={() => setIsOpenCorrectAnswer(false)} />
<WrongAnswerToast
isOpen={isOpenWrongAnswer}
correct_answer={modal_ans}
dismiss={() => setIsOpenWrongAnswer(false)}
/>
</>
);
};
export default QuizContent;

View File

@@ -0,0 +1,147 @@
import { IonContent, IonPage, useIonRouter } from '@ionic/react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
import { LoadingScreen } from '../../components/LoadingScreen';
import { CONNECTIVE_REVISION_LINK, QUIZ_MAIN_MENU_LINK } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import { useMyIonQuizContext } from '../../contexts/MyIonQuiz';
import { listConectivesRevisionContent } from '../../public_data/listConectivesRevisionContent';
import { shuffleArray } from '../../utils/shuffleArray';
import QuizContent from './QuizContent';
function ConnectiveRevisionQuizRun() {
const router = useIonRouter();
const { p_route } = useParams<{ p_route: string }>();
const i_p_route = parseInt(p_route);
const { setTabActive } = useAppStateContext();
const [question_list, setQuestionList] = useState<IQuestionJson[] | []>([]);
const [current_question_meta, setCurrentQuestionMeta] = useState<IQuestionMeta | undefined>(undefined);
const [current_question_idx, setCurrentQuestionIdx] = useState(0);
const [num_correct, setNumCorrect] = useState(0);
const [isOpenCorrectAnswer, setIsOpenCorrectAnswer] = useState(false);
const [isOpenWrongAnswer, setIsOpenWrongAnswer] = useState(false);
const [answer_list, setAnswerList] = useState<string[]>(['but', 'and', 'or', 'of', 'with']);
const {
setConnectiveRevisionCurrentTest,
setConnectiveRevisionProgress,
setConnectiveRevisionScore,
//
} = useMyIonQuizContext();
const { setConnectiveRevisionInProgress } = useAppStateContext();
function openCorrectToast() {
setIsOpenCorrectAnswer(true);
}
function openWrongToast() {
setIsOpenWrongAnswer(true);
}
const nextQuestion = () => {
let next_question_num = current_question_idx + 1;
setCurrentQuestionIdx(next_question_num);
// setCurrentQuestionMeta(question_list[next_question_num]);
let question_meta_current = question_list[next_question_num];
setCurrentQuestionMeta({
question_idx: next_question_num,
...question_meta_current,
});
if (next_question_num >= question_list.length) {
setConnectiveRevisionCurrentTest(i_p_route);
// setConnectiveRevisionProgress(Math.ceil(((num_correct + 1) / question_list.length) * 100));
setConnectiveRevisionScore(Math.ceil((num_correct / question_list.length) * 100));
router.push(`${CONNECTIVE_REVISION_LINK}/finished`, 'none', 'replace');
}
};
const incNumCorrect = () => {
setNumCorrect(num_correct + 1);
};
const [init_answer, setInitAnswer] = useState<string[]>([]);
useEffect(() => {
if (!current_question_meta) return;
// let all_answers = [...new Set([...question_list.map(q => q.modal_ans), ...current_question_meta.options])];
let all_answers = [current_question_meta.modal_ans, ...current_question_meta.options];
let wrong_ans_list = all_answers.filter((a) => a !== current_question_meta.modal_ans);
let sliced_shuffle_array = shuffleArray(wrong_ans_list).slice(0, 2);
let full_array = [...sliced_shuffle_array, current_question_meta.modal_ans];
setAnswerList(shuffleArray(full_array));
}, [current_question_meta]);
useEffect(() => {
(async () => {
const res_json = await listConectivesRevisionContent();
let temp_init_ans = res_json[i_p_route].init_ans;
setInitAnswer(temp_init_ans);
let temp = res_json[i_p_route].content;
let shuffled_temp = shuffleArray(temp);
// let shuffled_temp = temp;
setQuestionList(shuffled_temp);
let question_meta_current = res_json[i_p_route].content[0];
setCurrentQuestionMeta({
question_idx: current_question_idx,
...question_meta_current,
});
})();
setTabActive(QUIZ_MAIN_MENU_LINK);
}, []);
if (!current_question_meta) return <LoadingScreen />;
return (
<>
<IonPage>
<IonContent fullscreen>
<QuizContent
num_correct={num_correct}
incNumCorrect={incNumCorrect}
nextQuestion={nextQuestion}
question_meta={current_question_meta}
// openCorrectToast={openCorrectToast}
// openWrongToast={openWrongToast}
total_questions_num={question_list.length}
answer_list={answer_list}
quiz_idx={i_p_route + 1}
//
/>
{/* */}
{/* <CorrectAnswerToast isOpen={isOpenCorrectAnswer} dismiss={() => setIsOpenCorrectAnswer(false)} /> */}
{/* */}
{/* <WrongAnswerToast
correct_answer={current_question_meta.modal_ans}
isOpen={isOpenWrongAnswer}
dismiss={() => setIsOpenWrongAnswer(false)}
/> */}
{/*
<IonToast
isOpen={isOpenCorrectAnswer}
message='This answer is correct'
onDidDismiss={() => setIsOpenCorrectAnswer(false)}
duration={1000 - 100}
color='success'
></IonToast>
<IonToast
isOpen={isOpenWrongAnswer}
message='This answer is wrong'
onDidDismiss={() => setIsOpenWrongAnswer(false)}
duration={1000 - 100}
color='danger'
></IonToast> */}
</IonContent>
</IonPage>
</>
);
}
export default ConnectiveRevisionQuizRun;

View File

@@ -0,0 +1,72 @@
import { IonButton, IonContent, IonPage, useIonRouter } from '@ionic/react';
import { useEffect } from 'react';
import { CONNECTIVE_REVISION_LINK } from '../../../constants';
import { useAppStateContext } from '../../../contexts/AppState';
import { useMyIonQuizContext } from '../../../contexts/MyIonQuiz';
function ConnectiveRevisionQuizRun() {
const router = useIonRouter();
const {
loadConnectiveRevisionScoreBoard,
saveConnectiveRevisionResultToScoreBoard,
connective_revision_current_test,
connective_revision_progress,
connective_revision_score,
} = useMyIonQuizContext();
const { setConnectiveRevisionInProgress } = useAppStateContext();
useEffect(() => {
setConnectiveRevisionInProgress(false);
(async () => {
let all_old_highest_scores = await loadConnectiveRevisionScoreBoard();
let old_highest_score = all_old_highest_scores[connective_revision_current_test.toString()] || 0;
saveConnectiveRevisionResultToScoreBoard(
connective_revision_current_test.toString(),
Math.max(old_highest_score, connective_revision_score),
);
})();
}, []);
return (
<>
<IonPage>
<IonContent fullscreen>
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div
style={{
margin: '1rem',
marginTop: '5rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div>{'Quiz Result'}</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '3rem' }}>
<div style={{ fontSize: '3rem', marginTop: '1rem' }}>🎉🎉🎉</div>
<div style={{ fontSize: '1.5rem', marginTop: '1rem' }}>{'Congraulations !!!'}</div>
<div style={{ fontSize: '1.5rem', marginTop: '1rem' }}>{'Quiz over'}</div>
</div>
<div style={{ marginTop: '3rem' }}>
<IonButton color={'dark'} fill="outline" onClick={() => router.push(`${CONNECTIVE_REVISION_LINK}/`)}>
{'back to main menu'}
</IonButton>
</div>
</div>
</div>
</IonContent>
</IonPage>
</>
);
}
export default ConnectiveRevisionQuizRun;

View File

@@ -0,0 +1,128 @@
import { IonButton, IonContent, IonHeader, IonIcon, IonPage, IonTitle, IonToolbar, useIonRouter } from '@ionic/react';
import { arrowBackCircleOutline } from 'ionicons/icons';
import { useEffect, useState } from 'react';
import { LoadingScreen } from '../../components/LoadingScreen';
import { CONNECTIVE_REVISION_LINK, QUIZ_MAIN_MENU_LINK } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import { ConnectiveRevisionAllResult } from '../../contexts/ConnectiveRevisionRanking';
import { useMyIonQuizContext } from '../../contexts/MyIonQuiz';
import IConnectivesRevisionCategory from '../../interfaces/IConnectivesRevisionCategory';
import { listConectivesRevisionContent } from '../../public_data/listConectivesRevisionContent';
function ConnectiveRevisionSelectCategory() {
const PAGE_TITLE = 'Connective Revision';
const router = useIonRouter();
let [loading, setLoading] = useState<boolean>(true);
let [categories, setCategories] = useState<IConnectivesRevisionCategory[] | []>([]);
let { setTabActive, setConnectiveRevisionInProgress } = useAppStateContext();
useEffect(() => {
listConectivesRevisionContent().then((res_json) => {
setCategories(res_json);
setLoading(false);
});
}, []);
let { loadConnectiveRevisionScoreBoard } = useMyIonQuizContext();
let [scoreboard_meta, setScoreboardMeta] = useState<ConnectiveRevisionAllResult>();
useEffect(() => {
(async () => {
let temp = await loadConnectiveRevisionScoreBoard();
setScoreboardMeta(temp);
})();
setTabActive(QUIZ_MAIN_MENU_LINK);
}, []);
if (loading) return <LoadingScreen />;
if (!scoreboard_meta) return <LoadingScreen />;
return (
<>
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
<div style={{ width: '15vw' }}>
<IonButton shape={'round'} fill="clear" color="dark" onClick={() => router.push(QUIZ_MAIN_MENU_LINK)}>
<IonIcon size={'large'} slot={'icon-only'} icon={arrowBackCircleOutline}></IonIcon>
</IonButton>
</div>
<IonTitle>
<div style={{ textAlign: 'center' }}>{PAGE_TITLE}</div>
</IonTitle>
<div style={{ width: '15vw' }}></div>
</div>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div>{PAGE_TITLE}</div>
</IonTitle>
</IonToolbar>
</IonHeader>
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div style={{ margin: '1rem 0 1rem' }}>
<div
style={{
width: '50vw',
height: '30vw',
backgroundSize: 'cover',
backgroundImage: `url('/data/Lesson/images/quiz_connectives_revision.jpg')`,
borderRadius: '1rem',
}}
></div>
</div>
<div>{'Question Bank'}</div>
<div style={{ width: '80vw' }}>
{categories
.map((item) => item.cat_name)
.map((item_name, idx) => (
<div style={{ margin: '0.9rem 0 0.9rem' }} key={idx}>
<IonButton
color="dark"
fill="outline"
expand="block"
onClick={() => {
setConnectiveRevisionInProgress(true);
router.push(`${CONNECTIVE_REVISION_LINK}/r/${idx}`, 'none', 'replace');
}}
>
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
}}
>
<div>{item_name}</div>
<div>
{scoreboard_meta[idx.toString()] || 0}
{'%'}
</div>
</div>
</IonButton>
</div>
))}
</div>
</div>
</IonContent>
</IonPage>
</>
);
}
export default ConnectiveRevisionSelectCategory;

View File

@@ -0,0 +1,7 @@
// clear config by key
export function deleteUserConfig(key: string): void {
let current_config_string = localStorage.getItem('user_config') || '{}';
let current_config = JSON.parse(current_config_string);
delete current_config[key];
localStorage.setItem('user_config', JSON.stringify(current_config));
}

View File

@@ -0,0 +1,155 @@
import { IonButton, IonContent, IonPage, useIonRouter } from '@ionic/react';
import './style.css';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useAppUseTime } from '../../contexts/MyIonMetric/AppUseTime';
import { useConnectivesRevisionCorrectCount } from '../../contexts/MyIonMetric/ConnectivesRevisionCorrectCount';
import { useFullmarkCount } from '../../contexts/MyIonMetric/FullmarkCount';
import { useListeningPracticeTimeSpent } from '../../contexts/MyIonMetric/ListeningPracticeTimeSpent';
import { useMatchingFrenzyCorrectCount } from '../../contexts/MyIonMetric/MatchingFrenzyCorrectCount';
import { useMyIonQuizContext } from '../../contexts/MyIonQuiz';
const markdown = `
Just a link: www.nasa.gov.
- [ ] helloworld
__bold__
# h1
## h2
### h3
#### h4
##### h5
###### h6
| test | test | test |
| --- | --- | --- |
| 1 | 1 | 1 |
`;
const DebugPage: React.FC = () => {
// Genius
const { myIonMetricSetFullMarkCount } = useFullmarkCount();
// hardworker
const { myIonMetricSetAppUseTime } = useAppUseTime();
// Attentive ears
const { myIonMetricSetListeningPracticeProgress } = useListeningPracticeTimeSpent();
// matchmaking
const { myIonMetricSetMatchingFrenzyCorrectCount } = useMatchingFrenzyCorrectCount();
// connecives conqueror
const { myIonMetricSetConnectivesRevisionCorrectCount } = useConnectivesRevisionCorrectCount();
//
const { appendToListeningPracticeCorrectionList, setListeningPracticeCorrectionList } = useMyIonQuizContext();
const router = useIonRouter();
return (
<IonPage>
<IonContent fullscreen>
<div>debug page</div>
<div>
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
</div>
<div>
<IonButton
onClick={() => {
setListeningPracticeCorrectionList([
{
test_s: 'test_s',
test_i: 1,
image: 'https://docs-demo.ionic.io/assets/madison.jpg',
sound: '/helloworld.mp3',
word: 'mouse',
word_c: 'T_鍵盤 00',
sample_e: 'T_I buy a keyboard to type 00',
sample_c: "T_我買了一個<span className='bold'>鍵盤</span>來打字",
},
{
test_s: 'test_s',
test_i: 1,
image: 'https://docs-demo.ionic.io/assets/madison.jpg',
sound: '/helloworld.mp3',
word: 'keyboard',
word_c: 'T_鍵盤 00',
sample_e: 'T_I buy a keyboard to type 00',
sample_c: "T_我買了一個<span className='bold'>鍵盤</span>來打字",
},
{
test_s: 'test_s',
test_i: 1,
image: 'https://docs-demo.ionic.io/assets/madison.jpg',
sound: '/helloworld.mp3',
word: 'monitor',
word_c: 'T_鍵盤 00',
sample_e: 'T_I buy a keyboard to type 00',
sample_c: "T_我買了一個<span className='bold'>鍵盤</span>來打字",
},
]);
}}
>
appendToListeningPracticeCorrectionList()
</IonButton>
</div>
<IonButton
onClick={() => {
router.push('/listening_practice/c/0');
}}
>
go to correction
</IonButton>
<div>
<IonButton
onClick={() => {
myIonMetricSetFullMarkCount(1001);
}}
>
set count (Genius)
</IonButton>
</div>
<div>
<IonButton
onClick={() => {
myIonMetricSetAppUseTime(600);
}}
>
set count (hardworker)
</IonButton>
</div>
<div>
<IonButton
onClick={() => {
myIonMetricSetListeningPracticeProgress(610);
}}
>
set count (attentive ears)
</IonButton>
</div>
<div>
<IonButton
onClick={() => {
myIonMetricSetMatchingFrenzyCorrectCount(40);
}}
>
set count (matchmaking)
</IonButton>
</div>
<div>
<IonButton
onClick={() => {
myIonMetricSetConnectivesRevisionCorrectCount(40);
}}
>
set count (connectives)
</IonButton>
</div>
</IonContent>
</IonPage>
);
};
export default DebugPage;

View File

@@ -0,0 +1,3 @@
export function initUserConfig() {
return localStorage.setItem('user_config', JSON.stringify({ version: '0.1' }));
}

View File

@@ -0,0 +1,4 @@
export function listAllUserConfig(): { [key: string]: any } {
let current_config_string = localStorage.getItem('user_config') || '{}';
return JSON.parse(current_config_string);
}

View File

@@ -0,0 +1,5 @@
export function readUserConfig(key: string) {
let current_config_string = localStorage.getItem('user_config') || '{}';
let current_config = JSON.parse(current_config_string);
return current_config[key];
}

View File

@@ -0,0 +1,6 @@
export function writeUserConfig(key: string, value: any) {
let current_config_string = localStorage.getItem('user_config') || '{}';
let current_config = JSON.parse(current_config_string);
let updated_config = { ...current_config, [key]: value };
return localStorage.setItem('user_config', JSON.stringify(updated_config));
}

View File

@@ -0,0 +1,21 @@
import { useIonRouter } from '@ionic/react';
import { useEffect } from 'react';
import { LoadingScreen } from '../../../components/LoadingScreen';
import { useMyIonFavorite } from '../../../contexts/MyIonFavorite';
const FavoriteConnectivesPage: React.FC = () => {
let router = useIonRouter();
let { myIonStoreLoadFavoriteConnectives } = useMyIonFavorite();
useEffect(() => {
(async () => {
let fav_list = await myIonStoreLoadFavoriteConnectives();
let fav_link = fav_list.length > 0 ? fav_list[0] : '';
router.push(fav_link);
})();
}, []);
return <LoadingScreen />;
};
export default FavoriteConnectivesPage;

View File

@@ -0,0 +1,23 @@
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(() => {
load(audio_src);
}, []);
useEffect(() => {
if (loadedSrc) {
// TODO: delete this ?
// play();
myIonMetricIncListeningPracticeTimeSpent(duration);
}
}, [loadedSrc]);
return <></>;
};

View File

@@ -0,0 +1,369 @@
import { IonButton, IonButtons, IonContent, IonIcon, IonModal, IonPage, IonToolbar, useIonRouter } from '@ionic/react';
import './style.css';
import {
arrowBackCircleOutline,
chevronBack,
chevronForward,
closeOutline,
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 { RECORD_LINK } from '../../../constants';
import { useAppStateContext } from '../../../contexts/AppState';
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 { getFavLessonConnectivesLink } from '../getFavLessonConnectivesLink';
// import { StoreContext, useMyIonStore } from '../../contexts/store';
//
const ConnectivesWordPage: React.FC = () => {
const [loading, setLoading] = useState(true);
const router = useIonRouter();
let [fav_cursor, setFavCursor] = useState<number>(0);
const [open_remove_modal, setOpenRemoveModal] = useState(false);
const modal = useRef<HTMLIonModalElement>(null);
const { lesson_idx, cat_idx, word_idx } = useParams<{ lesson_idx: string; cat_idx: string; word_idx: string }>();
const [lesson_info, setLessonInfo] = useState<ILesson | undefined>(undefined);
const [cat_info, setCatInfo] = useState<ILessonCategory | undefined>(undefined);
const [word_info, setWordInfo] = useState<IWordCard | undefined>(undefined);
const { play: play_word, playing } = useGlobalAudioPlayer();
const {
myIonStoreAddFavoriteConnectives,
myIonStoreRemoveFavoriteConnectives,
myIonStoreFindInFavoriteConnectives,
myIonStoreLoadFavoriteConnectives,
} = useMyIonFavorite();
let [favorite_address, setFavoriteAddress] = useState(getFavLessonConnectivesLink(lesson_idx, cat_idx, word_idx));
function dismiss() {
setOpenRemoveModal(false);
}
const [isOpen, setIsOpen] = useState(false);
// const { setShowRemoceFavPrompt } = useAppStateContext();
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, cat_idx, word_idx]);
let [in_fav, setInFav] = useState(false);
const isInFavorite = async (string_to_search: string) => {
let result = await myIonStoreFindInFavoriteConnectives(string_to_search);
setInFav(result);
};
const addToFavorite = async (string_to_add: string) => {
await myIonStoreAddFavoriteConnectives(string_to_add);
await isInFavorite(string_to_add);
};
function handleUserRemoveFavorite() {
setOpenRemoveModal(true);
}
const { setDisableUserTap } = useAppStateContext();
const removeFromFavorite = async (string_to_remove: string) => {
let list_before_remove = await myIonStoreLoadFavoriteConnectives();
await myIonStoreRemoveFavoriteConnectives(string_to_remove);
let temp_list = await myIonStoreLoadFavoriteConnectives();
setFavList(temp_list);
const deleteFirstElement = () => fav_cursor == 0;
const deleteLastElement = () => fav_cursor == list_before_remove.length - 1;
const lastElementNewList = temp_list[temp_list.length - 1];
const firstElementNewList = temp_list[0];
const samePosElementNewList = temp_list[fav_cursor];
if (deleteFirstElement()) {
if (!firstElementNewList) {
setDisableUserTap(true);
setTimeout(() => {
router.push(RECORD_LINK, undefined, 'replace');
}, 1000);
} else {
router.push(`${firstElementNewList}`, undefined, 'replace');
}
} else if (deleteLastElement()) {
if (!lastElementNewList) {
setDisableUserTap(true);
setTimeout(() => {
router.push(RECORD_LINK, undefined, 'replace');
}, 1000);
} else {
router.push(`${lastElementNewList}`, undefined, 'replace');
}
} else {
if (!samePosElementNewList) {
setDisableUserTap(true);
setTimeout(() => {
router.push(RECORD_LINK, undefined, 'replace');
}, 1000);
} else {
router.push(`${samePosElementNewList}`, undefined, 'replace');
}
}
await isInFavorite(string_to_remove);
};
let [fav_list, setFavList] = useState<string[]>([]);
const locateInFavCursor = (string_to_search: string) => setFavCursor(fav_list.indexOf(string_to_search) ?? 0);
useEffect(() => {
if (JSON.stringify(fav_list) === '[]') return;
let temp = getFavLessonConnectivesLink(lesson_idx, cat_idx, word_idx);
if (fav_list.length === 0) {
} else {
locateInFavCursor(temp);
}
}, [fav_list]);
async function handleNextClick() {
let next_fav_link = fav_cursor < fav_list.length - 1 ? fav_list[fav_cursor + 1] : fav_list[fav_cursor];
let temp_fav_list = await myIonStoreLoadFavoriteConnectives();
setFavList(temp_fav_list);
router.push(next_fav_link, undefined, 'replace');
}
async function handlePrevClick() {
let prev_fav_link = fav_cursor > 0 ? fav_list[fav_cursor - 1] : fav_list[fav_cursor];
let temp_fav_list = await myIonStoreLoadFavoriteConnectives();
setFavList(temp_fav_list);
router.push(prev_fav_link, undefined, 'replace');
}
useEffect(() => {
let temp_fav_address = getFavLessonConnectivesLink(lesson_idx, cat_idx, word_idx);
setFavoriteAddress(temp_fav_address);
isInFavorite(temp_fav_address);
}, [lesson_idx, cat_idx, word_idx]);
useEffect(() => {
(async () => {
let temp_fav_list = await myIonStoreLoadFavoriteConnectives();
setFavList(temp_fav_list);
})();
}, []);
// if (loading) return <>loading</>;
// if (!word_info) return <>loading</>;
if (!cat_info || !word_info) return <LoadingScreen />;
return (
<>
<IonPage>
<IonContent fullscreen>
<div style={{ position: 'fixed' }}>
<IonButton size="large" shape="round" fill="clear" color={'dark'} onClick={() => router.push(RECORD_LINK)}>
<IonIcon size="large" icon={arrowBackCircleOutline} />
</IonButton>
</div>
<div style={{ marginTop: '3rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
margin: '1rem 1rem',
justifyContent: 'space-between',
}}
>
<div>
Part {fav_cursor + 1}/{fav_list.length}
</div>
</div>
{/* */}
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: '80vw',
}}
>
<div
style={{
flexGrow: 1,
display: 'inline-flex',
justifyContent: 'center',
fontSize: '1.3rem',
}}
dangerouslySetInnerHTML={{ __html: cat_info.cat_name }}
></div>
</div>
{/* */}
<div style={{ marginTop: '1rem' }}>
<IonButton
shape="round"
color="danger"
fill="clear"
size="large"
onClick={() => {
in_fav ? handleUserRemoveFavorite() : addToFavorite(favorite_address);
}}
>
<IonIcon slot="icon-only" icon={in_fav ? heart : heartOutline}></IonIcon>
</IonButton>
</div>
{/* */}
<div
style={{
display: 'inline-flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: '1rem',
width: '100%',
}}
>
<div>
<IonButton
size="large"
shape="round"
fill="clear"
onClick={handlePrevClick}
color={fav_cursor <= 0 ? 'medium' : 'dark'}
disabled={fav_cursor <= 0}
>
<IonIcon slot="icon-only" size="large" icon={chevronBack}></IonIcon>
</IonButton>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
alignItems: 'center',
fontSize: '1.3rem',
fontWeight: 'bold',
}}
>
<Markdown remarkPlugins={[remarkGfm]}>{word_info.word}</Markdown>
<Markdown remarkPlugins={[remarkGfm]}>{word_info.word_c}</Markdown>
</div>
<div>
<IonButton
shape="round"
fill="clear"
size="large"
color={fav_cursor >= fav_list.length - 1 ? 'medium' : 'dark'}
disabled={fav_cursor >= fav_list.length - 1}
onClick={handleNextClick}
>
<IonIcon slot="icon-only" icon={chevronForward}></IonIcon>
</IonButton>
</div>
</div>
{/* */}
<div style={{ marginTop: '2rem' }}>
<div
style={{
display: 'flex',
flexDirection: 'row',
gap: '1rem',
alignItems: 'center',
margin: '1rem 1rem 1rem',
}}
>
<div>
<IonButton shape="round" fill="clear" color="dark" disabled={playing} onClick={() => play_word()}>
<IonIcon slot="icon-only" icon={playing ? play : volumeHighOutline}></IonIcon>
</IonButton>
</div>
<Markdown remarkPlugins={[remarkGfm]}>{word_info.sample_e}</Markdown>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '1rem' }}>
<Markdown remarkPlugins={[remarkGfm]}>{word_info.sample_c}</Markdown>
</div>
</div>
{/* */}
</div>
</IonContent>
</IonPage>
{/* */}
<IonModal isOpen={open_remove_modal} id="example-modal" ref={modal}>
<IonContent>
<IonToolbar>
<IonButtons slot="end">
<IonButton onClick={() => dismiss()} shape="round" fill="clear">
<IonIcon size="large" slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
<div style={{ marginTop: '2rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div>Are you sure to remove favorite ?</div>
<div style={{ display: 'flex', flexDirection: 'row', gap: '1rem', marginTop: '1rem' }}>
<div>
<IonButton color="dark" onClick={() => dismiss()} fill="outline">
Cancel
</IonButton>
</div>
<div>
<IonButton
onClick={() => {
removeFromFavorite(getFavLessonConnectivesLink(lesson_idx, cat_idx, word_idx));
setIsOpen(true);
dismiss();
}}
fill="solid"
color="danger"
>
Remove
</IonButton>
</div>
</div>
</div>
</div>
</IonContent>
</IonModal>
{/* */}
<RemoveFavoritePrompt open={isOpen} setIsOpen={setIsOpen} />
</>
);
};
export default ConnectivesWordPage;

View File

@@ -0,0 +1,21 @@
.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;
}

View File

@@ -0,0 +1,58 @@
import { IonButton } from '@ionic/react';
import { useEffect, useState } from 'react';
import { LoadingScreen } from '../../../components/LoadingScreen';
import { useMyIonFavorite } from '../../../contexts/MyIonFavorite';
import { useMyIonStore } from '../../../contexts/MyIonStore';
import { listLessonCategories } from '../../../public_data/listLessonCategories';
interface ICardProps {
current_vocab: any;
current_fav_idx: number;
nextFavVocab: () => void;
prevFavVocab: () => void;
}
const Card: React.FC<ICardProps> = ({ current_vocab, current_fav_idx, nextFavVocab, prevFavVocab }) => {
let [loading, setLoading] = useState<boolean>(true);
let { lesson_contents: lesson_content, setLessonContent } = useMyIonStore();
let [active_lesson_idx, setActiveLessonIdx] = useState<number>(0);
let [selected_content, setSelectedContent] = useState<any>([]);
useEffect(() => {
listLessonCategories().then((cats: any) => {
console.log({ cats });
setLessonContent(cats);
setActiveLessonIdx(0);
setLoading(false);
});
}, []);
useEffect(() => {
if (loading) return;
console.log('active_category changed', active_lesson_idx);
let selected_category = lesson_content[active_lesson_idx];
setSelectedContent(selected_category['content']);
}, [active_lesson_idx, loading]);
let { myIonStoreLoadFavoriteVocabulary } = useMyIonFavorite();
let [fav_list, setFavList] = useState<any[] | undefined>(undefined);
useEffect(() => {
(async () => {
let temp = await myIonStoreLoadFavoriteVocabulary();
setFavList(temp);
})();
}, []);
if (loading) return <LoadingScreen />;
return (
<div>
{current_fav_idx}
<IonButton onClick={prevFavVocab}>Prev</IonButton>
<IonButton onClick={nextFavVocab}>Next</IonButton>
</div>
);
};
export default Card;

View File

@@ -0,0 +1,78 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import { useEffect, useState } from 'react';
import { LoadingScreen } from '../../../components/LoadingScreen';
import { useMyIonFavorite } from '../../../contexts/MyIonFavorite';
import { useMyIonStore } from '../../../contexts/MyIonStore';
import { listLessonCategories } from '../../../public_data/listLessonCategories';
const ListEmpty: React.FC = () => {
let [loading, setLoading] = useState<boolean>(true);
let { lesson_contents: lesson_content, setLessonContent } = useMyIonStore();
let [active_lesson_idx, setActiveLessonIdx] = useState<number>(0);
let [selected_content, setSelectedContent] = useState<any>([]);
useEffect(() => {
listLessonCategories().then((cats: any) => {
console.log({ cats });
setLessonContent(cats);
setActiveLessonIdx(0);
setLoading(false);
});
}, []);
useEffect(() => {
if (loading) return;
console.log('active_category changed', active_lesson_idx);
let selected_category = lesson_content[active_lesson_idx];
setSelectedContent(selected_category['content']);
}, [active_lesson_idx, loading]);
let { myIonStoreLoadFavoriteVocabulary } = useMyIonFavorite();
let [fav_list_len, setFavListLen] = useState<number>(0);
let [fav_list, setFavList] = useState<any[] | []>([]);
const [current_vocab, setCurrentVocab] = useState<any>({});
const [current_fav_idx, setCurrentFavIdx] = useState<number>(0);
function nextFavVocab() {
setCurrentFavIdx(Math.min(current_fav_idx + 1, fav_list_len - 1));
}
function prevFavVocab() {
setCurrentFavIdx(Math.max(current_fav_idx - 1, 0));
}
useEffect(() => {
if (!fav_list) return;
setCurrentVocab(fav_list[current_fav_idx]);
}, [current_fav_idx]);
useEffect(() => {
setFavListLen(fav_list.length);
}, [fav_list]);
useEffect(() => {
(async () => {
let temp = await myIonStoreLoadFavoriteVocabulary();
setFavList(temp);
})();
}, []);
if (loading) return <LoadingScreen />;
if (!fav_list || fav_list.length === 0) return <ListEmpty />;
return (
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar>
<IonTitle>
<div>{'Favorite Vocabulary'}</div>
</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<div>list is empty</div>
</IonContent>
</IonPage>
);
};
export default ListEmpty;

View File

@@ -0,0 +1,21 @@
import { useIonRouter } from '@ionic/react';
import { useEffect } from 'react';
import { LoadingScreen } from '../../../components/LoadingScreen';
import { useMyIonFavorite } from '../../../contexts/MyIonFavorite';
const FavVocabularyPage: React.FC = () => {
let router = useIonRouter();
let { myIonStoreLoadFavoriteVocabulary } = useMyIonFavorite();
useEffect(() => {
(async () => {
let fav_list = await myIonStoreLoadFavoriteVocabulary();
let fav_link = fav_list.length > 0 ? fav_list[0] : '';
router.push(fav_link);
})();
}, []);
return <LoadingScreen />;
};
export default FavVocabularyPage;

View File

@@ -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 <></>;
};

View File

@@ -0,0 +1,369 @@
import { IonButton, IonButtons, IonContent, IonIcon, IonModal, IonPage, IonToolbar, useIonRouter } from '@ionic/react';
import './style.css';
import {
arrowBackCircleOutline,
chevronBack,
chevronForward,
closeOutline,
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 { RECORD_LINK } from '../../../constants';
import { useAppStateContext } from '../../../contexts/AppState';
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 } from '../../Lesson/getLessonWordLink';
import { AudioControls } from './AudioControls';
const FavoriteVocabularyPage: React.FC = () => {
const [loading, setLoading] = useState(true);
const router = useIonRouter();
let [fav_cursor, setFavCursor] = useState<number>(0);
const [open_remove_modal, setOpenRemoveModal] = useState(false);
const modal = useRef<HTMLIonModalElement>(null);
const { lesson_idx, cat_idx, word_idx } = useParams<{ lesson_idx: string; cat_idx: string; word_idx: string }>();
const [lesson_info, setLessonInfo] = useState<ILesson | undefined>(undefined);
const [cat_info, setCatInfo] = useState<ILessonCategory | undefined>(undefined);
const [word_info, setWordInfo] = useState<IWordCard | undefined>(undefined);
const { play: play_word, playing } = useGlobalAudioPlayer();
const {
myIonStoreAddFavoriteVocabulary,
myIonStoreRemoveFavoriteVocabulary,
myIonStoreFindInFavoriteVocabulary,
myIonStoreLoadFavoriteVocabulary,
} = useMyIonFavorite();
let [favorite_address, setFavoriteAddress] = useState(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx));
const { setDisableUserTap } = useAppStateContext();
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, cat_idx, word_idx]);
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);
};
function handleUserRemoveFavorite() {
setOpenRemoveModal(true);
}
const removeFromFavorite = async (string_to_remove: string) => {
let list_before_remove = await myIonStoreLoadFavoriteVocabulary();
await myIonStoreRemoveFavoriteVocabulary(string_to_remove);
let temp_list = await myIonStoreLoadFavoriteVocabulary();
setFavList(temp_list);
const deleteFirstElement = () => fav_cursor == 0;
const deleteLastElement = () => fav_cursor == list_before_remove.length - 1;
const lastElementNewList = temp_list[temp_list.length - 1];
const firstElementNewList = temp_list[0];
const samePosElementNewList = temp_list[fav_cursor];
if (deleteFirstElement()) {
if (!firstElementNewList) {
setTimeout(() => {
setDisableUserTap(true);
router.push(RECORD_LINK, undefined, 'replace');
}, 1000);
} else {
router.push(`${firstElementNewList}`, undefined, 'replace');
}
} else if (deleteLastElement()) {
if (!lastElementNewList) {
setTimeout(() => {
setDisableUserTap(true);
router.push(RECORD_LINK, undefined, 'replace');
}, 1000);
} else {
router.push(`${lastElementNewList}`, undefined, 'replace');
}
} else {
if (!samePosElementNewList) {
setDisableUserTap(true);
setTimeout(() => {
router.push(RECORD_LINK, undefined, 'replace');
}, 1000);
} else {
router.push(`${samePosElementNewList}`, undefined, 'replace');
}
}
await isInFavorite(string_to_remove);
};
let [fav_list, setFavList] = useState<string[]>([]);
const locateInFavCursor = (string_to_search: string) => {
console.log({ t: fav_list.indexOf(string_to_search) ?? 0, t1: string_to_search });
setFavCursor(fav_list.indexOf(string_to_search) ?? 0);
};
useEffect(() => {
if (JSON.stringify(fav_list) === '[]') return;
let temp = getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx);
if (fav_list.length === 0) {
} else {
locateInFavCursor(temp);
}
}, [fav_list]);
async function handleNextClick() {
let next_fav_link = fav_cursor < fav_list.length - 1 ? fav_list[fav_cursor + 1] : fav_list[fav_cursor];
let temp_fav_list = await myIonStoreLoadFavoriteVocabulary();
setFavList(temp_fav_list);
router.push(next_fav_link, undefined, 'replace');
}
async function handlePrevClick() {
let prev_fav_link = fav_cursor > 0 ? fav_list[fav_cursor - 1] : fav_list[fav_cursor];
let temp_fav_list = await myIonStoreLoadFavoriteVocabulary();
setFavList(temp_fav_list);
router.push(prev_fav_link, undefined, 'replace');
}
useEffect(() => {
let temp_fav_address = getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx);
setFavoriteAddress(temp_fav_address);
isInFavorite(temp_fav_address);
}, [lesson_idx, cat_idx, word_idx]);
useEffect(() => {
(async () => {
let temp_fav_list = await myIonStoreLoadFavoriteVocabulary();
setFavList(temp_fav_list);
})();
}, []);
// if (loading) return <>loading</>;
// if (!word_info) return <>loading</>;
if (!cat_info || !word_info) return <LoadingScreen />;
return (
<>
<IonPage>
<IonContent fullscreen>
<div style={{ position: 'fixed' }}>
<IonButton size="large" shape="round" fill="clear" color={'dark'} onClick={() => router.push(RECORD_LINK)}>
<IonIcon size="large" icon={arrowBackCircleOutline} />
</IonButton>
</div>
<div style={{ marginTop: '3rem' }}>
<div style={{ textAlign: 'center', fontSize: '1.2rem' }}>{cat_info.cat_name}</div>
</div>
<div style={{ marginTop: '1rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{/* <LessonContainer name='Tab 1 page' /> */}
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div>
<IonButton
size="large"
shape="round"
fill="clear"
onClick={handlePrevClick}
color={fav_cursor <= 0 ? 'medium' : 'dark'}
disabled={fav_cursor <= 0}
>
<IonIcon slot="icon-only" size="large" icon={chevronBack}></IonIcon>
</IonButton>
</div>
<div
style={{
width: '66vw',
height: '66vw',
backgroundImage: `url(${word_info.image})`,
backgroundPosition: 'center',
backgroundSize: 'cover',
borderRadius: '0.5rem',
margin: '.5rem',
}}
></div>
<div>
<IonButton
shape="round"
fill="clear"
size="large"
color={fav_cursor === fav_list.length - 1 ? 'medium' : 'dark'}
disabled={fav_cursor === fav_list.length - 1}
onClick={handleNextClick}
>
<IonIcon slot="icon-only" size="large" icon={chevronForward}></IonIcon>
</IonButton>
</div>
</div>
<div style={{ marginTop: '1rem' }}>
<div
style={{
width: '2rem',
height: '2rem',
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '1rem',
backgroundColor: 'black',
color: 'white',
}}
>
{fav_cursor + 1}
</div>
</div>
<div>
<div
style={{ display: 'flex', flexDirection: 'row', gap: '1rem', alignItems: 'center', marginTop: '1rem' }}
>
<div>
<AudioControls audio_src={word_info.sound} />
<IonButton
size="large"
color="dark"
shape="round"
fill="clear"
disabled={playing}
onClick={() => (playing ? null : play_word())}
>
<IonIcon
size="large"
color="dark"
slot="icon-only"
icon={playing ? play : volumeHighOutline}
></IonIcon>
</IonButton>
</div>
<div style={{ fontWeight: 'bold', fontSize: '1.5rem' }}>{word_info.word}</div>
<div>
<IonButton
color="danger"
shape="round"
size="large"
fill="clear"
onClick={() => {
in_fav ? handleUserRemoveFavorite() : addToFavorite(favorite_address);
}}
>
<IonIcon
size="large"
color="danger"
slot="icon-only"
icon={in_fav ? heart : heartOutline}
></IonIcon>
</IonButton>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '1rem' }}>
<div style={{ fontWeight: 'bold', fontSize: '1.3rem' }}>{word_info.word_c}</div>
</div>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
alignItems: 'center',
marginTop: '2rem',
}}
>
<Markdown remarkPlugins={[remarkGfm]}>{word_info.sample_e}</Markdown>
<Markdown remarkPlugins={[remarkGfm]}>{word_info.sample_c}</Markdown>
</div>
</div>
</IonContent>
</IonPage>
{/* */}
<IonModal isOpen={open_remove_modal} id="example-modal" ref={modal}>
<IonContent>
<IonToolbar>
<IonButtons slot="end">
<IonButton onClick={() => dismiss()} shape="round" fill="clear">
<IonIcon size="large" slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
<div style={{ marginTop: '2rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div>Are you sure to remove favorite ?</div>
<div style={{ display: 'flex', flexDirection: 'row', gap: '1rem', marginTop: '1rem' }}>
<div>
<IonButton color="dark" onClick={() => dismiss()} fill="outline">
{'Cancel'}
</IonButton>
</div>
<div>
<IonButton
onClick={() => {
removeFromFavorite(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx));
setIsOpen(true);
dismiss();
}}
fill="solid"
color="danger"
>
{'Remove'}
</IonButton>
</div>
</div>
</div>
</div>
</IonContent>
</IonModal>
{/* */}
<RemoveFavoritePrompt open={isOpen} setIsOpen={setIsOpen} />
</>
);
};
export default FavoriteVocabularyPage;

View File

@@ -0,0 +1,21 @@
.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;
}

View File

@@ -0,0 +1,10 @@
export function getFavLessonConnectivesLink(
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);
let s_word_idx = parseInt(i_word_idx);
return `/fav/c/${s_active_lesson_idx}/${s_cat_idx}/${s_word_idx}`;
}

View File

@@ -0,0 +1,6 @@
export function getLessonConnectivesLink(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);
let s_word_idx = parseInt(i_word_idx);
return `/lesson_word_page/c/${s_active_lesson_idx}/${s_cat_idx}/${s_word_idx}`;
}

View File

@@ -0,0 +1,75 @@
import { IonButton, useIonRouter } from '@ionic/react';
import './Lesson.css';
import { useEffect, useState } from 'react';
import { LoadingScreen } from '../../components/LoadingScreen';
import { useMyIonStore } from '../../contexts/MyIonStore';
import { listLessonCategories } from '../../public_data/listLessonCategories';
import { getLessonVocabularyLink } from './getLessonWordLink';
const ConnectivesContainer: React.FC = () => {
const router = useIonRouter();
const [loading, setLoading] = useState<boolean>(true);
const { lesson_contents, setLessonContent } = useMyIonStore();
const [active_lesson_idx, setActiveLessonIdx] = useState<number>(0);
const [selected_content, setSelectedContent] = useState<any>([]);
useEffect(() => {
listLessonCategories().then((cats: any) => {
console.log({ cats });
setLessonContent(cats);
setActiveLessonIdx(0);
setLoading(false);
});
}, []);
useEffect(() => {
if (loading) return;
let selected_category = lesson_contents[active_lesson_idx];
setSelectedContent(selected_category['content']);
}, [active_lesson_idx, loading]);
if (loading) return <LoadingScreen />;
return (
<>
<div>Connectives ?</div>
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap' }}>
{selected_content.map((content: any, cat_idx: number) => (
<>
<IonButton
style={{ width: '45vw', height: '45vw' }}
fill="clear"
onClick={() => {
router.push(
getLessonVocabularyLink(active_lesson_idx.toString(), cat_idx.toString(), '0'),
undefined,
'replace',
);
}}
>
<div>
<div
key={cat_idx}
style={{
width: '100px',
height: '100px',
backgroundImage: `url(${content.cat_image})`,
backgroundPosition: 'center',
backgroundSize: 'cover',
borderRadius: '0.5rem',
margin: '.5rem',
}}
></div>
{content.cat_name}
</div>
</IonButton>
</>
))}
</div>
{/* <EndOfList /> */}
</>
);
};
export default ConnectivesContainer;

View File

@@ -0,0 +1,319 @@
import { IonButton, IonButtons, IonContent, IonIcon, IonModal, IonPage, IonToolbar, useIonRouter } from '@ionic/react';
import './style.css';
import {
arrowBackCircleOutline,
chevronBack,
chevronForward,
closeOutline,
heart,
heartOutline,
playOutline,
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 ILesson from '../../../interfaces/ILesson';
import { listLessonContent } from '../../../public_data/listLessonContent';
// import { listLessonContent } from '../../../public_data/index.ts.del';
import { AudioControls } from '../../LessonWord/AudioControls';
import { getFavLessonConnectivesLink, getLessonConnectivesLink } from '../getLessonConnectivesLink';
const ConnectivesPage: React.FC = () => {
let [loading, setLoading] = useState<boolean>(true);
let [all_words_length, setAllWordsLength] = useState<number>(0);
let [show_word, setShowWord] = useState({ word: '', word_c: '', sample_e: '', sample_c: '', sound: '' });
let [cat_name, setCatName] = useState<string>('');
let [fav_active, setFavActive] = useState<boolean>(false);
let { playing, play: sound_play } = useGlobalAudioPlayer();
const router = useIonRouter();
const [lesson_info, setLessonInfo] = useState<ILesson | undefined>(undefined);
let { lesson_idx, cat_idx, word_idx } = useParams<{ lesson_idx: string; cat_idx: string; word_idx: string }>();
let i_lesson_idx = parseInt(lesson_idx);
let i_cat_idx = parseInt(cat_idx);
let i_word_idx = parseInt(word_idx);
const [isOpen, setIsOpen] = useState(false);
const [open_remove_modal, setOpenRemoveModal] = useState(false);
const modal = useRef<HTMLIonModalElement>(null);
// const lesson_vocab_address = `/lesson_word_page/v/${lesson_idx}/${cat_idx}/${word_idx}`;
let [favorite_address, setFavoriteAddress] = useState(getFavLessonConnectivesLink(lesson_idx, cat_idx, word_idx));
function dismiss() {
setOpenRemoveModal(false);
}
function handleUserClickPrev() {
if (i_word_idx > 0) {
router.push(getLessonConnectivesLink(lesson_idx, cat_idx, (i_word_idx - 1).toString()));
}
}
function handleUserClickNext() {
if (i_word_idx < all_words_length - 1) {
router.push(getLessonConnectivesLink(lesson_idx, cat_idx, (i_word_idx + 1).toString()));
}
}
const { myIonStoreAddFavoriteConnectives, myIonStoreRemoveFavoriteConnectives, myIonStoreFindInFavoriteConnectives } =
useMyIonFavorite();
let [in_fav, setInFav] = useState(false);
const isInFavorite = async (string_to_search: string) => {
let result = await myIonStoreFindInFavoriteConnectives(string_to_search);
setInFav(result);
};
const addToFavorite = async (string_to_add: string) => {
await myIonStoreAddFavoriteConnectives(string_to_add);
await isInFavorite(string_to_add);
};
const removeFromFavorite = async (string_to_remove: string) => {
await myIonStoreRemoveFavoriteConnectives(string_to_remove);
await isInFavorite(string_to_remove);
};
function handleUserRemoveFavorite() {
setOpenRemoveModal(true);
}
useEffect(() => {
(async () => {
let lesson_content = await listLessonContent();
let word_cat = lesson_content[i_lesson_idx].content[i_cat_idx];
setCatName(word_cat['cat_name']);
let all_words = lesson_content[i_lesson_idx].content[i_cat_idx].content;
setAllWordsLength(all_words.length);
let show_word = lesson_content[i_lesson_idx].content[i_cat_idx].content[i_word_idx];
setShowWord(show_word);
console.log({ t: lesson_content[i_lesson_idx].content[i_cat_idx].content[i_word_idx] });
setFavoriteAddress(getFavLessonConnectivesLink(lesson_idx, cat_idx, word_idx));
isInFavorite(getFavLessonConnectivesLink(lesson_idx, cat_idx, word_idx));
// console.log({ in_fav, favorite_address });
setLessonInfo(lesson_content[i_lesson_idx]);
setLoading(false);
})();
}, [i_lesson_idx, i_cat_idx, i_word_idx]);
if (lesson_info === undefined) return <LoadingScreen />;
if (loading) return <LoadingScreen />;
return (
<>
<IonPage>
<IonContent fullscreen>
<div style={{ position: 'fixed' }}>
<IonButton
size="large"
shape="round"
fill="clear"
color={'dark'}
// href={`${LESSON_LINK}/a/${lesson_info.name}`}
onClick={() => router.push(`${LESSON_LINK}/a/${lesson_info.name}`)}
>
<IonIcon size="large" icon={arrowBackCircleOutline} />
</IonButton>
</div>
<div style={{ marginTop: '3rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
margin: '1rem 1rem',
justifyContent: 'space-between',
}}
>
<div>
Part {i_word_idx + 1}/{all_words_length}
</div>
</div>
{/* <LessonContainer name='Tab 1 page' /> */}
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: '80vw',
}}
>
<div
style={{
flexGrow: 1,
display: 'inline-flex',
justifyContent: 'center',
fontSize: '1.3rem',
}}
dangerouslySetInnerHTML={{ __html: cat_name }}
></div>
</div>
<div style={{ marginTop: '1rem' }}>
<IonButton
shape="round"
color="danger"
fill="clear"
size="large"
onClick={() => {
in_fav ? handleUserRemoveFavorite() : addToFavorite(favorite_address);
}}
>
<IonIcon slot="icon-only" icon={in_fav ? heart : heartOutline}></IonIcon>
</IonButton>
</div>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: '1rem',
width: '100%',
//
}}
>
<div>
<IonButton
shape="round"
fill="clear"
color="dark"
size="large"
onClick={handleUserClickPrev}
disabled={i_word_idx + 1 <= 1}
>
<IonIcon slot="icon-only" icon={chevronBack}></IonIcon>
</IonButton>
</div>
{/* */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
alignItems: 'center',
marginTop: '2rem',
fontSize: '1.3rem',
fontWeight: 'bold',
}}
>
<div style={{ fontSize: '1.6rem' }} dangerouslySetInnerHTML={{ __html: show_word.word }}></div>
<div style={{ fontSize: '1.6rem' }} dangerouslySetInnerHTML={{ __html: show_word.word_c }}></div>
</div>
{/* */}
<div>
<IonButton
shape="round"
fill="clear"
color="dark"
size="large"
onClick={handleUserClickNext}
disabled={i_word_idx + 1 >= all_words_length}
>
<IonIcon slot="icon-only" icon={chevronForward}></IonIcon>
</IonButton>
</div>
</div>
{/* */}
<div style={{ marginTop: '2rem' }}>
<div
style={{
display: 'flex',
flexDirection: 'row',
gap: '1rem',
alignItems: 'center',
margin: '1rem 1rem 1rem',
}}
>
<div>
<IonButton shape="round" fill="clear" color="dark" disabled={playing} onClick={() => sound_play()}>
<IonIcon slot="icon-only" icon={playing ? playOutline : volumeHighOutline}></IonIcon>
</IonButton>
</div>
{/* <div style={{ fontSize: '1rem' }} dangerouslySetInnerHTML={{ __html: show_word.sample_e }}></div> */}
<div style={{ fontSize: '1rem' }}>
<Markdown remarkPlugins={[remarkGfm]}>{show_word.sample_e}</Markdown>
</div>
</div>
<div
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', margin: '1rem 1rem 0 1rem' }}
>
<div style={{ fontSize: '1.1rem' }}>
<Markdown remarkPlugins={[remarkGfm]}>{show_word.sample_c}</Markdown>
</div>
</div>
</div>
</div>
</IonContent>
</IonPage>
{/* */}
<IonModal isOpen={open_remove_modal} id="example-modal" ref={modal}>
<IonContent>
<IonToolbar>
<IonButtons slot="end">
<IonButton onClick={() => dismiss()} shape="round" fill="clear">
<IonIcon size="large" slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
<div style={{ marginTop: '2rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div>Are you sure to remove favorite ?</div>
<div style={{ display: 'flex', flexDirection: 'row', gap: '1rem', marginTop: '1rem' }}>
<div>
<IonButton color="dark" onClick={() => dismiss()} fill="outline">
Cancel
</IonButton>
</div>
<div>
<IonButton
onClick={() => {
removeFromFavorite(getFavLessonConnectivesLink(lesson_idx, cat_idx, word_idx));
setIsOpen(true);
dismiss();
}}
fill="solid"
color="danger"
>
Remove
</IonButton>
</div>
</div>
</div>
</div>
</IonContent>
</IonModal>
{/* */}
<RemoveFavoritePrompt open={isOpen} setIsOpen={setIsOpen} />
{/* */}
<AudioControls audio_src={show_word.sound} />
</>
);
};
export default ConnectivesPage;

View File

@@ -0,0 +1,3 @@
.bold {
font-weight: bold;
}

View File

@@ -0,0 +1,9 @@
function EndOfList() {
return (
<div style={{ textAlign: 'center', color: 'gray', marginBottom: '3rem' }}>
<p>end of list</p>
</div>
);
}
export default EndOfList;

View File

@@ -0,0 +1,82 @@
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<ContainerProps> = ({ test_active_lesson_idx = 1 }) => {
const router = useIonRouter();
const [loading, setLoading] = useState<boolean>(true);
const { lesson_contents, setLessonContent } = useMyIonStore();
const [active_lesson_idx, setActiveLessonIdx] = useState<number>(0);
const [selected_content, setSelectedContent] = useState<any>([]);
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 <LoadingScreen />;
return (
<>
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center' }}>
{selected_content.map((content: any, cat_idx: number) => (
<IonButton
key={cat_idx}
style={{ width: '45vw', height: '45vw' }}
fill="clear"
// href={lessonLinkProxy[test_active_lesson_idx](test_active_lesson_idx.toString(), cat_idx.toString(), '0')}
onClick={() => {
router.push(
lessonLinkProxy[test_active_lesson_idx](test_active_lesson_idx.toString(), cat_idx.toString(), '0'),
undefined,
'replace',
);
}}
>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div
key={cat_idx}
style={{
width: '100px',
height: '100px',
backgroundImage: `url(${content.cat_image})`,
backgroundPosition: 'center',
backgroundSize: 'cover',
borderRadius: '0.5rem',
margin: '.5rem',
}}
></div>
<span style={{ color: COLOR_TEXT }}>{content.cat_name}</span>
</div>
</IonButton>
))}
</div>
{/* <EndOfList /> */}
</>
);
};
export default LessonContainer;

View File

@@ -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 <></>;
};

View File

@@ -0,0 +1,326 @@
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 { StoreContext, useMyIonStore } from '../../contexts/store';
//
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 '../../Lesson/getLessonWordLink';
import { AudioControls } from './AudioControls';
//
const LessonWordPage: React.FC = () => {
const router = useIonRouter();
const [loading, setLoading] = useState(true);
const [open_remove_modal, setOpenRemoveModal] = useState(false);
const modal = useRef<HTMLIonModalElement>(null);
const { lesson_idx, cat_idx, word_idx } = useParams<{ lesson_idx: string; cat_idx: string; word_idx: string }>();
const [lesson_info, setLessonInfo] = useState<ILesson | undefined>(undefined);
const [cat_info, setCatInfo] = useState<ILessonCategory | undefined>(undefined);
const [word_info, setWordInfo] = useState<IWordCard | undefined>(undefined);
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));
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]);
// if (loading) return <>loading</>;
// if (!word_info) return <>loading</>;
if (lesson_info == undefined) return <LoadingScreen />;
if (!cat_info || !word_info) return <LoadingScreen />;
return (
<>
<IonPage>
<IonContent fullscreen>
<div style={{ position: 'fixed' }}>
<IonButton
size="large"
shape="round"
fill="clear"
color={'dark'}
// href={`${LESSON_LINK}/a/${lesson_info.name}`}
onClick={() => {
router.push(`${LESSON_LINK}/a/${lesson_info.name}`);
}}
>
<IonIcon size="large" icon={arrowBackCircleOutline} />
</IonButton>
</div>
<div style={{ marginTop: '3rem' }}>
<div style={{ textAlign: 'center', fontSize: '1.2rem' }}>{cat_info.cat_name}</div>
</div>
<div style={{ marginTop: '1rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{/* <LessonContainer name='Tab 1 page' /> */}
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div>
<IonButton
size="large"
shape="round"
fill="clear"
color={parseInt(word_idx) === 0 ? 'medium' : 'dark'}
disabled={parseInt(word_idx) === 0}
// 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()),
);
}}
>
<IonIcon slot="icon-only" size="large" icon={chevronBack}></IonIcon>
</IonButton>
</div>
<div
style={{
width: '66vw',
height: '66vw',
backgroundImage: `url(${word_info.image_url})`,
backgroundPosition: 'center',
backgroundSize: 'cover',
borderRadius: '0.5rem',
margin: '.5rem',
}}
></div>
<div>
<IonButton
size="large"
shape="round"
fill="clear"
color={parseInt(word_idx) === cat_info.content.length - 1 ? 'medium' : 'dark'}
disabled={parseInt(word_idx) === cat_info.content.length - 1}
// href={getLessonVocabularyLink(
// lesson_idx,
// cat_idx,
// Math.min(cat_info.content.length - 1, parseInt(word_idx) + 1).toString()
// )}
onClick={() => {
router.push(
getLessonVocabularyLink(
lesson_idx,
cat_idx,
Math.min(cat_info.content.length - 1, parseInt(word_idx) + 1).toString(),
),
);
}}
>
<IonIcon slot="icon-only" size="large" icon={chevronForward}></IonIcon>
</IonButton>
</div>
</div>
<div style={{ marginTop: '1rem' }}>
<div
style={{
width: '2rem',
height: '2rem',
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '1rem',
backgroundColor: 'black',
color: 'white',
}}
>
{parseInt(word_idx) + 1}
</div>
</div>
<div>
<div
style={{ display: 'flex', flexDirection: 'row', gap: '1rem', alignItems: 'center', marginTop: '1rem' }}
>
<div>
<AudioControls audio_src={word_info.sound_url} />
<IonButton
size="large"
color="dark"
shape="round"
fill="clear"
disabled={playing}
onClick={() => (playing ? null : play_word())}
>
<IonIcon
size="large"
color="dark"
slot="icon-only"
icon={playing ? play : volumeHighOutline}
></IonIcon>
</IonButton>
</div>
<div style={{ fontWeight: 'bold', fontSize: '1.5rem' }}>{word_info.word}</div>
<div>
<IonButton
color="danger"
shape="round"
size="large"
fill="clear"
// id='open-modal'
onClick={() => {
in_fav ? handleUserRemoveFavorite() : addToFavorite(favorite_address);
}}
>
<IonIcon
size="large"
color="danger"
slot="icon-only"
icon={in_fav ? heart : heartOutline}
></IonIcon>
</IonButton>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginTop: '1rem' }}>
<div style={{ fontWeight: 'bold', fontSize: '1.3rem' }}>{word_info.word_c}</div>
</div>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
alignItems: 'center',
marginTop: '2rem',
}}
>
<Markdown remarkPlugins={[remarkGfm]}>{word_info.sample_e}</Markdown>
<Markdown remarkPlugins={[remarkGfm]}>{word_info.sample_c}</Markdown>
</div>
</div>
</IonContent>
</IonPage>
{/* */}
<IonModal isOpen={open_remove_modal} id="example-modal" ref={modal}>
<IonContent>
<IonToolbar>
<IonButtons slot="end">
<IonButton onClick={() => dismiss()} shape="round" fill="clear">
<IonIcon size="large" slot="icon-only" icon={close}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
<div style={{ marginTop: '2rem' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div>Are you sure to remove favorite ?</div>
<div style={{ display: 'flex', flexDirection: 'row', gap: '1rem', marginTop: '1rem' }}>
<div>
<IonButton color="dark" onClick={() => dismiss()} fill="outline">
Cancel
</IonButton>
</div>
<div>
<IonButton
onClick={() => {
removeFromFavorite(getFavLessonVocabularyLink(lesson_idx, cat_idx, word_idx));
setIsOpen(true);
dismiss();
}}
fill="solid"
color="danger"
>
Remove
</IonButton>
</div>
</div>
</div>
</div>
</IonContent>
</IonModal>
<RemoveFavoritePrompt open={isOpen} setIsOpen={setIsOpen} />
</>
);
};
export default LessonWordPage;

View File

@@ -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;
}

View File

@@ -0,0 +1,17 @@
export function getLessonConnectivesLink(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);
let s_word_idx = parseInt(i_word_idx);
return `/lesson_word_page/c/${s_active_lesson_idx}/${s_cat_idx}/${s_word_idx}`;
}
export function getFavLessonConnectivesLink(
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);
let s_word_idx = parseInt(i_word_idx);
return `/fav/c/${s_active_lesson_idx}/${s_cat_idx}/${s_word_idx}`;
}

View File

@@ -0,0 +1,34 @@
import { FAVORITE_LINK } from '../../constants';
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);
let s_word_idx = parseInt(i_word_idx);
return `/lesson_word_page/v/${s_active_lesson_idx}/${s_cat_idx}/${s_word_idx}`;
}
export function getFavLessonVocabularyLink(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);
let s_word_idx = parseInt(i_word_idx);
return `${FAVORITE_LINK}/v/${s_active_lesson_idx}/${s_cat_idx}/${s_word_idx}`;
}
export function parseLessonVocabularyLink(s: string):
| {
active_lesson_idx: number;
cat_idx: number;
word_idx: number;
}
| undefined {
const reg = /^\/lesson_word_page\/v\/(\d+)\/(\d+)\/(\d+)$/;
const match = s.match(reg);
if (match) {
return {
active_lesson_idx: parseInt(match[1]),
cat_idx: parseInt(match[2]),
word_idx: parseInt(match[3]),
};
}
return undefined;
}

View File

@@ -0,0 +1,134 @@
import {
IonContent,
IonHeader,
IonItem,
IonList,
IonPage,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar,
useIonRouter,
} from '@ionic/react';
import './Lesson.css';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import ExitButton from '../../components/ExitButton';
import { LoadingScreen } from '../../components/LoadingScreen';
import CongratConnectiveConqueror from '../../components/Modal/Congratulation/ConnectiveConqueror';
import CongratGenius from '../../components/Modal/Congratulation/Genius';
import CongratHardworker from '../../components/Modal/Congratulation/Hardworker';
import CongratListeningProgress from '../../components/Modal/Congratulation/ListeningProgress';
import CongratMatchmaking from '../../components/Modal/Congratulation/Matchmaking';
import { LESSON_LINK } from '../../constants';
import { useMyIonStore } from '../../contexts/MyIonStore';
import { listLessonCategories } from '../../public_data/listLessonCategories';
import LessonContainer from './LessonContainer';
const Lesson: React.FC = () => {
const { act_category } = useParams<{ act_category: string }>();
const { t, i18n } = useTranslation(); // not passing any namespace will use the defaultNS (by default set to 'translation')
let [loading, setLoading] = useState<boolean>(true);
let { lesson_contents: lesson_content, setLessonContent } = useMyIonStore();
let [active_lesson_idx, setActiveLessonIdx] = useState<number>(0);
let [selected_content, setSelectedContent] = useState<any>([]);
useEffect(() => {
listLessonCategories().then((cats: any) => {
console.log({ cats });
setLessonContent(cats);
setActiveLessonIdx(0);
setLoading(false);
});
}, []);
useEffect(() => {
if (loading) return;
console.log('active_category changed', active_lesson_idx);
let selected_category = lesson_content[active_lesson_idx];
setSelectedContent(selected_category['content']);
}, [active_lesson_idx, loading]);
let router = useIonRouter();
useEffect(() => {
if (lesson_content.length > 0) {
if (act_category == undefined) {
router.push(`${LESSON_LINK}/a/${lesson_content[0].name}`, undefined, 'replace');
} else {
setActiveLessonIdx(
lesson_content.findIndex((cat: any) => cat.name.toLowerCase() === act_category?.toLowerCase()),
);
}
} else {
}
}, [act_category, lesson_content]);
if (loading) return <LoadingScreen />;
return (
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar>
<div
style={{
//
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0 0.5rem 0 0.5rem',
}}
>
<IonTitle size="large" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{t('Lesson')}
</IonTitle>
<div>
<ExitButton />
</div>
</div>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div>{'Lesson'}</div>
</IonTitle>
</IonToolbar>
</IonHeader>
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<IonList lines="none">
<IonItem>
<IonSelect value={active_lesson_idx} onIonChange={(e) => setActiveLessonIdx(e.detail.value)}>
{lesson_content.map((category: any, idx: number) => (
<IonSelectOption key={idx} value={idx}>
{category.name}
</IonSelectOption>
))}
</IonSelect>
</IonItem>
</IonList>
</div>
{/* */}
<LessonContainer test_active_lesson_idx={active_lesson_idx} />
{/* */}
<CongratGenius />
<CongratHardworker />
<CongratListeningProgress />
<CongratMatchmaking />
<CongratConnectiveConqueror />
</IonContent>
</IonPage>
);
};
export default Lesson;

View File

@@ -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 <></>;
};

View File

@@ -0,0 +1,21 @@
.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;
}

View File

@@ -0,0 +1,59 @@
import { IonContent, IonPage, useIonRouter } from '@ionic/react';
import { useEffect } from 'react';
import { DEFAULT_FORWARD_TIMEOUT, LISTENING_PRACTICE_LINK, QUIZ_MAIN_MENU_LINK } from '../../../constants';
import { useAppStateContext } from '../../../contexts/AppState';
import './style.css';
const PracticeFinish: React.FC = () => {
const router = useIonRouter();
const { setListeningPracticeInProgress } = useAppStateContext();
const { setDisableUserTap, setTabActive } = useAppStateContext();
useEffect(() => {
setDisableUserTap(true);
setListeningPracticeInProgress(false);
setTimeout(() => {
router.push(`${LISTENING_PRACTICE_LINK}/result`, 'none', 'replace');
}, DEFAULT_FORWARD_TIMEOUT);
setTabActive(QUIZ_MAIN_MENU_LINK);
}, []);
return (
<>
<IonPage>
<IonContent fullscreen>
<div
style={{
height: '85vh',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '2rem',
}}
>
<div style={{ fontSize: '3rem' }}>🎉🎉🎉</div>
<div
style={{
fontSize: '1.5rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div>Quiz finished</div>
<div>Please take a rest</div>
😊
</div>
<div style={{ fontSize: '0.8rem' }}>redirecting you in 3 seconds</div>
</div>
</IonContent>
</IonPage>
</>
);
};
export default PracticeFinish;

View File

@@ -0,0 +1,98 @@
import { IonButton, IonContent, IonPage, useIonRouter } from '@ionic/react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
import { LoadingScreen } from '../../../components/LoadingScreen';
import MarkRating from '../../../components/MarkRating';
import { DEBUG, LISTENING_PRACTICE_LINK, QUIZ_MAIN_MENU_LINK } from '../../../constants';
import { useAppStateContext } from '../../../contexts/AppState';
import { useMyIonQuizContext } from '../../../contexts/MyIonQuiz';
import { listQuizListeningPracticeContent } from '../../../public_data/listQuizListeningPracticeContent';
import './style.css';
const PracticeResult: React.FC = () => {
let router = useIonRouter();
let [loading, setLoading] = useState(true);
const { p_route } = useParams<{ p_route: string }>();
const i_p_route = parseInt(p_route);
const [current_question_meta, setCurrentQuestionMeta] = useState<any>(undefined);
const { listening_practice_result, listening_practice_current_test } = useMyIonQuizContext();
const [num_rating, setNumRating] = useState<number>(0);
const { setTabActive, setDisableUserTap } = useAppStateContext();
useEffect(() => {
(async () => {
const res_json = await listQuizListeningPracticeContent();
setCurrentQuestionMeta(res_json[0]);
// 一颗星: 答对率 1-49%
// 两颗星: 答对率 50-79%
// 三颗星: 答对率 80-100%
if (listening_practice_result < 50) {
setNumRating(1);
} else if (listening_practice_result < 80) {
setNumRating(2);
} else {
setNumRating(3);
}
setLoading(false);
})();
setTabActive(QUIZ_MAIN_MENU_LINK);
setDisableUserTap(false);
}, []);
if (loading) return <LoadingScreen />;
return (
<IonPage>
<IonContent fullscreen>
<div
style={{
marginTop: '5rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
width: '50vw',
height: '30vw',
borderRadius: '1rem',
backgroundSize: 'contain',
backgroundImage: `url(${current_question_meta.cat_image})`,
}}
></div>
<div
style={{
marginTop: '3rem',
fontWeight: 'bold',
fontSize: '2rem',
textDecoration: 'underline',
}}
>
Technology
</div>
<div style={{ fontWeight: 'bold', fontSize: '2rem', marginTop: '2rem' }}>{'Well Done'}</div>
{DEBUG ? <>{JSON.stringify({ listening_practice_result })}</> : <></>}
<div style={{ fontSize: '1.2rem', marginTop: '1rem' }}>Accuracy {listening_practice_result}%</div>
<div style={{ marginTop: '3rem' }}>
{DEBUG ? <>{num_rating}</> : <></>}
<MarkRating num_rating={num_rating} />
</div>
<div style={{ marginTop: '3rem' }}>
<IonButton color={'dark'} fill="outline" onClick={() => router.push(`${LISTENING_PRACTICE_LINK}/`)}>
{'back to Main Menu'}
</IonButton>
</div>
</div>
</IonContent>
</IonPage>
);
};
export default PracticeResult;

View File

@@ -0,0 +1,30 @@
import { FunctionComponent, useEffect } from 'react';
import { useGlobalAudioPlayer } from 'react-use-audio-player';
import { useListeningPracticeTimeSpent } from '../../../contexts/MyIonMetric/ListeningPracticeTimeSpent';
// import { useMyIonMetric } from '../../contexts/MyIonMetric';
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 <></>;
};

View File

@@ -0,0 +1,284 @@
import { IonButton, IonIcon } from '@ionic/react';
import { play, volumeHighOutline } from 'ionicons/icons';
import { useEffect, useRef, useState } from 'react';
import { useGlobalAudioPlayer } from 'react-use-audio-player';
import CorrectAnswerToast from '../../../components/CorrectAnswerToast';
// import { AudioControls } from '../../LessonWord/AudioControls';
import { LoadingScreen } from '../../../components/LoadingScreen';
// import QuestionProgress from './QuestionProgress';
import QuestionProgress from '../../../components/QuestionProgress';
import { SpeakerImage } from '../../../components/SpeakerImage';
import WrongAnswerToast from '../../../components/WrongAnswerToast';
import { COLOR_TEXT, DEBUG } from '../../../constants';
import { useMyIonQuizContext } from '../../../contexts/MyIonQuiz';
import { AudioControls } from './AudioControls';
import './style.css';
import { useAppStateContext } from '../../../contexts/AppState';
import IListeningPracticeQuestion from '../../../interfaces/IListeningPracticeQuestion';
interface QuestionCardProps {
question_num: number;
nextQuestion: () => void;
num_correct: number;
incNumCorrect: () => void;
question_meta: any;
total_questions: number;
answer_list: string[];
question: IListeningPracticeQuestion;
}
const CorrectionCard: React.FC<QuestionCardProps> = ({
num_correct,
question_num,
nextQuestion,
incNumCorrect,
total_questions,
question_meta,
answer_list,
question,
}) => {
const [loading, setLoading] = useState(true);
//
const [isOpenCorrectAnswer, setIsOpenCorrectAnswer] = useState(false);
const [isOpenWrongAnswer, setIsOpenWrongAnswer] = useState(false);
const [user_answer, setUserAnswer] = useState<string | undefined>(undefined);
const [modal_answer, setModalAnswer] = useState(question_meta.modal_answer);
const { play: play_word, playing } = useGlobalAudioPlayer();
const [playing_audio, setPlayingAudio] = useState(true);
let [disable_user_answer, setDisableUserAnswer] = useState(true);
let { listening_practice_correction_list, appendToListeningPracticeCorrectionList } = useMyIonQuizContext();
const [ignore_answer_button, setIgnoreAnswerButton] = useState(false);
function handleUserAnswer(answer: string, e: React.MouseEvent, ref_button: React.RefObject<HTMLIonButtonElement>) {
setUserAnswer(answer);
if (!ignore_answer_button) {
if (answer.toLowerCase() === modal_answer.toLowerCase()) {
// answer is correct
incNumCorrect();
setIsOpenCorrectAnswer(true);
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'green';
if (ref_button && ref_button.current) ref_button.current.style.color = 'white';
} else {
// answer is wrong
if (DEBUG) {
console.log('user answer is wrong');
}
setIsOpenWrongAnswer(true);
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'red';
if (ref_button && ref_button.current) ref_button.current.style.color = 'white';
appendToListeningPracticeCorrectionList(question);
console.log({ listening_practice_correction_list });
}
}
}
useEffect(() => {
if (user_answer) {
if (!isOpenCorrectAnswer && !isOpenWrongAnswer) {
// assume all toast closed
setTimeout(() => {
nextQuestion();
setPlayingAudio(true);
}, 500);
}
}
}, [user_answer, isOpenCorrectAnswer, isOpenWrongAnswer]);
// useEffect(() => {
// if (user_answer) {
// setTimeout(() => {
// console.log({ listening_practice_correction_list });
// nextQuestion();
// setPlayingAudio(true);
// }, 1000);
// }
// }, [user_answer]);
useEffect(() => {
if (playing_audio) {
setDisableUserAnswer(true);
setIgnoreAnswerButton(true);
} else {
setDisableUserAnswer(false);
setIgnoreAnswerButton(false);
}
}, [playing_audio]);
function playAudio() {
setPlayingAudio(true);
play_word();
setTimeout(() => {
setPlayingAudio(false);
}, 1000);
}
let { LISTENING_PRACTICE_ANWERED_WAIT_S } = useAppStateContext();
if (DEBUG) {
console.log({ LISTENING_PRACTICE_ANWERED_WAIT_S });
}
function delayedPlayAudio() {
setPlayingAudio(true);
setTimeout(() => {
playAudio();
}, LISTENING_PRACTICE_ANWERED_WAIT_S * 1000);
}
let ref1 = useRef<HTMLIonButtonElement>(null);
let ref2 = useRef<HTMLIonButtonElement>(null);
let ref3 = useRef<HTMLIonButtonElement>(null);
let ref4 = useRef<HTMLIonButtonElement>(null);
let button_refs = [ref1, ref2, ref3, ref4];
function resetButtonFace(ref_button: React.RefObject<HTMLIonButtonElement>) {
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'unset';
if (ref_button && ref_button.current) ref_button.current.style.color = 'unset';
}
useEffect(() => {
resetButtonFace(ref1);
resetButtonFace(ref2);
resetButtonFace(ref3);
resetButtonFace(ref4);
}, [question_num, ref1, ref2, ref3, ref4]);
function getAnswerList(modal_answer: string) {
let wrong_answer_list = answer_list.filter((a) => a !== modal_answer);
let sliced_shuffle_array = shuffleArray(wrong_answer_list).slice(0, 2);
let full_array = [...sliced_shuffle_array, modal_answer];
return shuffleArray(full_array);
}
let [last_shuffle, setLastShuffle] = useState<string[]>([]);
let [shuffled_answer, setShuffledAnswer] = useState<string[]>([]);
useEffect(() => {
setDisableUserAnswer(true);
let { modal_answer } = question_meta;
let temp: string[] = getAnswerList(modal_answer);
if (last_shuffle.length == 0) {
setLastShuffle(temp);
setShuffledAnswer(temp);
} else {
for (let i = 0; i < 99; i++) {
let temp: string[] = getAnswerList(modal_answer);
if (JSON.stringify(last_shuffle) !== JSON.stringify(temp)) {
setLastShuffle(temp);
setShuffledAnswer(temp);
break;
}
}
}
delayedPlayAudio();
setModalAnswer(modal_answer);
}, [question_num]);
useEffect(() => {
setLoading(false);
}, []);
if (loading) return <LoadingScreen />;
if (!question_meta) return <LoadingScreen />;
return (
<>
<div style={{ marginTop: '5rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ marginTop: '1rem', marginBottom: '1rem' }}>
<QuestionProgress num_rating={num_correct} num_full_rating={total_questions} />
</div>
<div>
{'Correction card'} {question_num + 1}
</div>
<div style={{ margin: '1rem 0rem', width: '33vw', height: '33vw' }}>
<SpeakerImage />
</div>
<IonButton color="dark" fill="clear" disabled={playing_audio} shape="round" onClick={() => playAudio()}>
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: '0.5rem',
}}
>
<IonIcon slot="icon-only" size="large" icon={playing_audio ? play : volumeHighOutline}></IonIcon>
<div>{playing_audio ? 'playing' : 'repeat'}</div>
</div>
</IonButton>
<div
style={{
marginTop: '3rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
minWidth: '50vw',
}}
>
{DEBUG ? <div className="debug">{JSON.stringify(modal_answer)}</div> : <></>}
{playing_audio ? (
<div style={{ fontSize: '1.1rem', fontWeight: 'bold' }}>Listen</div>
) : (
<div style={{ color: COLOR_TEXT }}>Select the word you heard</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginTop: '1rem', minWidth: '50vw' }}>
{shuffled_answer.map((word, index) => {
return (
<div>
<IonButton
color={'dark'}
fill="outline"
ref={button_refs[index]}
disabled={disable_user_answer}
expand="block"
onClick={(e) => {
setIgnoreAnswerButton(true);
handleUserAnswer(word, e, button_refs[index]);
}}
>
{word}
</IonButton>
</div>
);
})}
</div>
</div>
</div>
{/* */}
<CorrectAnswerToast isOpen={isOpenCorrectAnswer} dismiss={() => setIsOpenCorrectAnswer(false)} />
{/* */}
<WrongAnswerToast
isOpen={isOpenWrongAnswer}
dismiss={() => setIsOpenWrongAnswer(false)}
correct_answer={modal_answer}
/>
{/* */}
<AudioControls audio_src={question_meta.sound} />
</>
);
};
function shuffleArray(in_array: string[]) {
for (let i = in_array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = in_array[i];
in_array[i] = in_array[j];
in_array[j] = temp;
}
return in_array;
}
export default CorrectionCard;

View File

@@ -0,0 +1,123 @@
import { IonButton, IonContent, IonPage, useIonRouter } from '@ionic/react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
import {
CORRECTION_PHASE,
DEBUG,
LISTENING_PRACTICE_LINK,
PRESS_START_TO_BEGIN,
QUIZ_MAIN_MENU_LINK,
} from '../../../constants';
import { useAppStateContext } from '../../../contexts/AppState';
import { useMyIonQuizContext } from '../../../contexts/MyIonQuiz';
import IListeningPracticeQuestion from '../../../interfaces/IListeningPracticeQuestion';
import { listQuizListeningPracticeContent } from '../../../public_data/listQuizListeningPracticeContent';
import CorrectionCard from './CorrectionCard';
import './style.css';
const CorrectionRoute: React.FC = () => {
const router = useIonRouter();
const { p_route } = useParams<{ p_route: string }>();
const i_p_route = parseInt(p_route);
const { listening_practice_correction_list, Helloworld, appendToListeningPracticeCorrectionList } =
useMyIonQuizContext();
const { setTabActive } = useAppStateContext();
const [current_question_idx, setCurrentQuestionIdx] = useState(0);
const [question_list, setQuestionList] = useState<IListeningPracticeQuestion[] | []>([]);
const [current_question_meta, setCurrentQuestionMeta] = useState<any | undefined>(undefined);
const [answer_list, setAnswerList] = useState<string[]>([]);
const [num_correct, setNumCorrect] = useState(0);
const [num_correct_correction, setNumCorrectCorrection] = useState(0);
const nextQuestion = () => {
if (current_question_idx >= listening_practice_correction_list.length - 1) {
router.push(`${LISTENING_PRACTICE_LINK}/finished`, 'none', 'replace');
}
let next_question_idx = current_question_idx + 1;
setCurrentQuestionIdx(next_question_idx);
setCurrentQuestionMeta(listening_practice_correction_list[next_question_idx]);
};
const incNumCorrectForCorrection = () => {
setNumCorrectCorrection(num_correct_correction + 1);
};
useEffect(() => {
(async () => {
const res_json = await listQuizListeningPracticeContent();
let init_answer = res_json[i_p_route].init_answer;
let temp = res_json[i_p_route].content;
// let shuffled_temp = shuffleArray(temp);
setQuestionList(listening_practice_correction_list);
setAnswerList([...new Set([...init_answer, ...temp.map((t: IListeningPracticeQuestion) => t.word)])]);
// setCurrentQuestionMeta(res_json[i_p_route].content[current_question_idx]);
setCurrentQuestionMeta(listening_practice_correction_list[current_question_idx]);
})();
setTabActive(QUIZ_MAIN_MENU_LINK);
if (listening_practice_correction_list.length < 1) {
router.push(`${LISTENING_PRACTICE_LINK}`, 'none', 'replace');
}
}, []);
let [show_press_start, setShowPressStart] = useState(true);
if (show_press_start)
return (
<>
<IonPage>
<IonContent fullscreen>
{DEBUG ? <div className="debug">{JSON.stringify({ listening_practice_correction_list })}</div> : <></>}
<div
style={{
width: '100%',
height: '90vh',
display: 'inline-flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div style={{ fontSize: '5rem' }}>📝</div>
<div style={{ marginTop: '1rem', fontSize: '1.2rem', fontWeight: 'bold' }}>{CORRECTION_PHASE}</div>
<div style={{ marginTop: '3rem ' }}>
<IonButton color="dark" fill="outline" onClick={() => setShowPressStart(false)}>
{PRESS_START_TO_BEGIN}
</IonButton>
</div>
</div>
</IonContent>
</IonPage>
</>
);
return (
<IonPage>
<IonContent fullscreen>
{DEBUG ? <div>{JSON.stringify({ current_question_meta })}</div> : <></>}
<CorrectionCard
num_correct={num_correct_correction}
incNumCorrect={incNumCorrectForCorrection}
//
question_num={current_question_idx}
nextQuestion={nextQuestion}
//
total_questions={question_list.length}
//
question_meta={{
sound: current_question_meta.sound,
modal_answer: current_question_meta.word,
}}
answer_list={answer_list}
//
question={current_question_meta}
/>
</IonContent>
</IonPage>
);
};
export default CorrectionRoute;

View File

@@ -0,0 +1,315 @@
import { IonButton, IonIcon } from '@ionic/react';
import { arrowBackCircleOutline, play, volumeHighOutline } from 'ionicons/icons';
import { useEffect, useRef, useState } from 'react';
import { useGlobalAudioPlayer } from 'react-use-audio-player';
import CorrectAnswerToast from '../../../components/CorrectAnswerToast';
// import { AudioControls } from '../../LessonWord/AudioControls';
import { LoadingScreen } from '../../../components/LoadingScreen';
// import QuestionProgress from './QuestionProgress';
import QuestionProgress from '../../../components/QuestionProgress';
import { SpeakerImage } from '../../../components/SpeakerImage';
import WrongAnswerToast from '../../../components/WrongAnswerToast';
import { COLOR_TEXT, DEBUG, LISTENING_PRACTICE_LINK } from '../../../constants';
import { useMyIonQuizContext } from '../../../contexts/MyIonQuiz';
import IListeningPracticeQuestion from '../../../interfaces/IListeningPracticeQuestion';
import { AudioControls } from './AudioControls';
import './style.css';
import ConfirmUserQuitQuiz from '../../../components/ConfirmUserQuitQuiz';
import { useAppStateContext } from '../../../contexts/AppState';
interface QuestionCardProps {
question_num: number;
nextQuestion: () => void;
num_correct: number;
incNumCorrect: () => void;
question_meta: any;
total_questions: number;
answer_list: string[];
question: IListeningPracticeQuestion;
}
const ListeningPracticeQuestionCard: React.FC<QuestionCardProps> = ({
num_correct,
question_num,
nextQuestion,
incNumCorrect,
total_questions,
question_meta,
answer_list,
question,
}) => {
const [loading, setLoading] = useState(true);
//
const [isOpenCorrectAnswer, setIsOpenCorrectAnswer] = useState(false);
const [isOpenWrongAnswer, setIsOpenWrongAnswer] = useState(false);
//
const [ignore_answer_button, setIgnoreAnswerButton] = useState(false);
const [user_answer, setUserAnswer] = useState<string | undefined>(undefined);
const [modal_answer, setModalAnswer] = useState(question_meta.modal_answer);
const { play: play_word, playing } = useGlobalAudioPlayer();
const [playing_audio, setPlayingAudio] = useState(true);
let [disable_user_answer, setDisableUserAnswer] = useState(true);
let { appendToListeningPracticeCorrectionList } = useMyIonQuizContext();
//
let { LISTENING_PRACTICE_ANWERED_WAIT_S } = useAppStateContext();
if (DEBUG) {
console.log({ LISTENING_PRACTICE_ANWERED_WAIT_S });
}
function handleUserAnswer(answer: string, e: React.MouseEvent, ref_button: React.RefObject<HTMLIonButtonElement>) {
setUserAnswer(answer);
if (!ignore_answer_button) {
if (answer.toLowerCase() === modal_answer.toLowerCase()) {
incNumCorrect();
setIsOpenCorrectAnswer(true);
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'green';
if (ref_button && ref_button.current) ref_button.current.style.color = 'white';
} else {
console.log('wrong answer checked');
setIsOpenWrongAnswer(true);
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'red';
if (ref_button && ref_button.current) ref_button.current.style.color = 'white';
appendToListeningPracticeCorrectionList(question);
}
}
}
useEffect(() => {
if (user_answer) {
if (!isOpenCorrectAnswer && !isOpenWrongAnswer) {
// assume all toast closed
setTimeout(() => {
nextQuestion();
setPlayingAudio(true);
}, 500);
}
}
}, [user_answer, isOpenCorrectAnswer, isOpenWrongAnswer]);
useEffect(() => {
if (playing_audio) {
setDisableUserAnswer(true);
setIgnoreAnswerButton(true);
} else {
setDisableUserAnswer(false);
setIgnoreAnswerButton(false);
}
}, [playing_audio]);
function playAudio() {
setPlayingAudio(true);
play_word();
setTimeout(() => {
setPlayingAudio(false);
}, 1000);
}
function delayedPlayAudio() {
setPlayingAudio(true);
setTimeout(() => {
playAudio();
}, 1000);
}
let ref1 = useRef<HTMLIonButtonElement>(null);
let ref2 = useRef<HTMLIonButtonElement>(null);
let ref3 = useRef<HTMLIonButtonElement>(null);
let ref4 = useRef<HTMLIonButtonElement>(null);
let button_refs = [ref1, ref2, ref3, ref4];
function resetButtonFace(ref_button: React.RefObject<HTMLIonButtonElement>) {
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'unset';
if (ref_button && ref_button.current) ref_button.current.style.color = 'unset';
}
useEffect(() => {
resetButtonFace(ref1);
resetButtonFace(ref2);
resetButtonFace(ref3);
resetButtonFace(ref4);
}, [question_num, ref1, ref2, ref3, ref4]);
function getAnswerList(modal_answer: string) {
let wrong_answer_list = answer_list.filter((a) => a !== modal_answer);
let sliced_shuffle_array = shuffleArray(wrong_answer_list).slice(0, 2);
let full_array = [...sliced_shuffle_array, modal_answer];
return shuffleArray(full_array);
}
let [last_shuffle, setLastShuffle] = useState<string[]>([]);
let [shuffled_answer, setShuffledAnswer] = useState<string[]>([]);
useEffect(() => {
setDisableUserAnswer(true);
let { modal_answer } = question_meta;
let temp: string[] = getAnswerList(modal_answer);
if (last_shuffle.length == 0) {
setLastShuffle(temp);
setShuffledAnswer(temp);
} else {
for (let i = 0; i < 99; i++) {
let temp: string[] = getAnswerList(modal_answer);
if (JSON.stringify(last_shuffle) !== JSON.stringify(temp)) {
setLastShuffle(temp);
setShuffledAnswer(temp);
break;
}
}
}
delayedPlayAudio();
setModalAnswer(modal_answer);
}, [question_num]);
const { resetListeningPracticeCorrectionList } = useMyIonQuizContext();
useEffect(() => {
setLoading(false);
resetListeningPracticeCorrectionList();
}, []);
const [show_confirm_user_exit, setShowConfirmUserExit] = useState<boolean>(false);
if (loading) return <LoadingScreen />;
if (!question_meta) return <LoadingScreen />;
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div style={{ width: '10vw' }}>
<IonButton color="dark" fill="clear" shape="round" onClick={() => setShowConfirmUserExit(true)}>
<IonIcon slot="icon-only" size="large" icon={arrowBackCircleOutline}></IonIcon>
</IonButton>
</div>
<div></div>
<div style={{ width: '10vw' }}></div>
</div>
<div
style={{
marginTop: '3rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<div style={{ marginTop: '1rem', marginBottom: '1rem' }}>
<QuestionProgress num_rating={num_correct} num_full_rating={total_questions} />
</div>
<div>Question card {question_num + 1}</div>
<div style={{ margin: '1rem 0rem', width: '33vw', height: '33vw' }}>
<SpeakerImage />
</div>
{DEBUG ? (
<div className="debug">
{JSON.stringify(question_meta)}
{JSON.stringify(question.word)}
</div>
) : (
<></>
)}
<IonButton color={COLOR_TEXT} disabled={playing_audio} shape="round" fill="clear" onClick={() => playAudio()}>
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: '0.5rem',
color: COLOR_TEXT,
}}
>
<IonIcon slot="icon-only" size="large" icon={playing_audio ? play : volumeHighOutline}></IonIcon>
<div>{playing_audio ? 'playing' : 'repeat'}</div>
</div>
</IonButton>
<div
style={{
marginTop: '3rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
minWidth: '50vw',
}}
>
{DEBUG ? <div className="debug">{JSON.stringify(modal_answer)}</div> : <></>}
{playing_audio ? (
<div style={{ fontSize: '1.1rem', fontWeight: 'bold' }}>Listen</div>
) : (
<div style={{ color: COLOR_TEXT }}>Select the word you heard</div>
)}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
marginTop: '1rem',
minWidth: '50vw',
}}
>
{shuffled_answer.map((word, index) => {
return (
<div>
<IonButton
color="dark"
expand="block"
fill="outline"
ref={button_refs[index]}
disabled={disable_user_answer}
onClick={(e) => {
setIgnoreAnswerButton(true);
handleUserAnswer(word, e, button_refs[index]);
}}
>
{word}
</IonButton>
</div>
);
})}
</div>
</div>
</div>
<ConfirmUserQuitQuiz
setShowConfirmUserExit={setShowConfirmUserExit}
show_confirm_user_exit={show_confirm_user_exit}
url_push_after_user_confirm={LISTENING_PRACTICE_LINK}
/>
{/* */}
<CorrectAnswerToast isOpen={isOpenCorrectAnswer} dismiss={() => setIsOpenCorrectAnswer(false)} />
{/* */}
<WrongAnswerToast
isOpen={isOpenWrongAnswer}
dismiss={() => setIsOpenWrongAnswer(false)}
correct_answer={modal_answer}
/>
{/* */}
<AudioControls audio_src={question_meta.sound} />
</>
);
};
function shuffleArray(in_array: string[]) {
for (let i = in_array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = in_array[i];
in_array[i] = in_array[j];
in_array[j] = temp;
}
return in_array;
}
export default ListeningPracticeQuestionCard;

View File

@@ -0,0 +1,112 @@
import { IonContent, IonPage, useIonRouter } from '@ionic/react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
import { LISTENING_PRACTICE_LINK } from '../../../constants';
import './style.css';
import { LoadingScreen } from '../../../components/LoadingScreen';
import { useFullmarkCount } from '../../../contexts/MyIonMetric/FullmarkCount';
import { useMyIonQuizContext } from '../../../contexts/MyIonQuiz';
import IListeningPracticeQuestion from '../../../interfaces/IListeningPracticeQuestion';
import { listQuizListeningPracticeContent } from '../../../public_data/listQuizListeningPracticeContent';
import { shuffleArray } from '../../../utils/shuffleArray';
import ListeningPracticeQuestionCard from './ListeningPracticeQuestionCard';
const QuestionRoute: React.FC = () => {
const { p_route } = useParams<{ p_route: string }>();
const i_p_route = parseInt(p_route);
const [question_list, setQuestionList] = useState<IListeningPracticeQuestion[] | []>([]);
const [current_question_meta, setCurrentQuestionMeta] = useState<IListeningPracticeQuestion | undefined>(undefined);
const [current_question_idx, setCurrentQuestionIdx] = useState(0);
const [num_correct, setNumCorrect] = useState(0);
const { myIonMetricIncFullMarkCount } = useFullmarkCount();
const { listening_practice_result, setListeningPracticeResult, setListeningPracticeCurrentTest } =
useMyIonQuizContext();
const { listening_practice_correction_list, setListeningPracticeCorrectionList } = useMyIonQuizContext();
const [question_finished, setQuestionFinished] = useState(false);
const router = useIonRouter();
const nextQuestion = () => {
if (current_question_idx >= question_list.length - 1) {
setQuestionFinished(true);
// if (isCorrectionNeeded()) {
// // run correction phase
// router.push(`${LISTENING_PRACTICE_LINK}/c/0`, 'none', 'replace');
// } else {
// router.push(`${LISTENING_PRACTICE_LINK}/finished`, 'none', 'replace');
// }
} else {
setCurrentQuestionIdx(current_question_idx + 1);
setCurrentQuestionMeta(question_list[current_question_idx + 1]);
}
};
useEffect(() => {
let isCorrectionNeeded = () => listening_practice_correction_list.length > 0;
if (question_finished) {
if (isCorrectionNeeded()) {
// run correction phase
router.push(`${LISTENING_PRACTICE_LINK}/c/0`, 'none', 'replace');
} else {
router.push(`${LISTENING_PRACTICE_LINK}/finished`, 'none', 'replace');
}
}
}, [question_finished]);
const incNumCorrect = () => {
setNumCorrect(num_correct + 1);
let temp: number = Math.ceil(((num_correct + 1) / question_list.length) * 100);
setListeningPracticeResult(temp);
setListeningPracticeCurrentTest(i_p_route);
myIonMetricIncFullMarkCount();
};
let [answer_list, setAnswerList] = useState<string[]>([]);
useEffect(() => {
(async () => {
const res_json = await listQuizListeningPracticeContent();
let init_answer = res_json[i_p_route].init_answer;
let temp = res_json[i_p_route].content;
let shuffled_temp = shuffleArray(temp);
setQuestionList(shuffled_temp);
setAnswerList([...new Set([...init_answer, ...temp.map((t: IListeningPracticeQuestion) => t.word)])]);
setCurrentQuestionMeta(res_json[i_p_route].content[current_question_idx]);
})();
}, []);
if (!current_question_meta) return <LoadingScreen />;
return (
<IonPage>
<IonContent fullscreen>
<ListeningPracticeQuestionCard
num_correct={num_correct}
incNumCorrect={incNumCorrect}
//
question_num={current_question_idx}
nextQuestion={nextQuestion}
//
total_questions={question_list.length}
//
question_meta={{
//
sound: current_question_meta.sound,
modal_answer: current_question_meta.word,
}}
//
answer_list={answer_list}
//
question={current_question_meta}
/>
</IonContent>
</IonPage>
);
};
export default QuestionRoute;

View File

@@ -0,0 +1,79 @@
import { OverlayEventDetail } from '@ionic/core/components';
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonInput,
IonItem,
IonModal,
IonPage,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { useRef, useState } from 'react';
function Example() {
const modal = useRef<HTMLIonModalElement>(null);
const input = useRef<HTMLIonInputElement>(null);
const [message, setMessage] = useState(
'This modal example uses triggers to automatically open a modal when the button is clicked.',
);
function confirm() {
modal.current?.dismiss(input.current?.value, 'confirm');
}
function onWillDismiss(event: CustomEvent<OverlayEventDetail>) {
if (event.detail.role === 'confirm') {
setMessage(`Hello, ${event.detail.data}!`);
}
}
return <></>;
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Inline Modal</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">
<IonButton id="open-modal" expand="block">
Open
</IonButton>
<p>{message}</p>
<IonModal ref={modal} trigger="open-modal" onWillDismiss={(event) => onWillDismiss(event)}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonButton onClick={() => modal.current?.dismiss()}>Cancel</IonButton>
</IonButtons>
<IonTitle>Welcome</IonTitle>
<IonButtons slot="end">
<IonButton strong={true} onClick={() => confirm()}>
Confirm
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">
<IonItem>
<IonInput
label="Enter your name"
labelPlacement="stacked"
ref={input}
type="text"
placeholder="Your name"
/>
</IonItem>
</IonContent>
</IonModal>
</IonContent>
</IonPage>
);
}
export default Example;

View File

@@ -0,0 +1,110 @@
import { IonButton, IonContent, IonHeader, IonIcon, IonPage, IonTitle, IonToolbar, useIonRouter } from '@ionic/react';
import './style.css';
import { arrowBackCircleOutline } from 'ionicons/icons';
import { useEffect, useState } from 'react';
import { LoadingScreen } from '../../components/LoadingScreen';
import { COLOR_TEXT, DEBUG, LISTENING_PRACTICE_LINK, QUIZ_MAIN_MENU_LINK } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import IListeningPracticeCategory from '../../interfaces/IListeningPracticeCategory';
import { listQuizListeningPracticeContent } from '../../public_data/listQuizListeningPracticeContent';
import { useTranslation } from 'react-i18next';
// import { listQuizListeningPracticeContent } from '../../public_data/listQuizListeningPracticeContent';
const ListeningPractice: React.FC = () => {
const { t } = useTranslation();
let [loading, setLoading] = useState<boolean>(true);
let [categories, setCategories] = useState<IListeningPracticeCategory[] | []>([]);
let { show_confirm_user_exit, setShowConfirmUserExit, setTabActive } = useAppStateContext();
useEffect(() => {
listQuizListeningPracticeContent().then((res_json: any) => {
setCategories(res_json);
DEBUG ? console.log({ res_json }) : '';
setLoading(false);
});
setTabActive(QUIZ_MAIN_MENU_LINK);
}, []);
let { setListeningPracticeInProgress } = useAppStateContext();
let router = useIonRouter();
function startListeningPractice(idx: number) {
let url = `${LISTENING_PRACTICE_LINK}/r/${idx}`;
setListeningPracticeInProgress(true);
router.push(url, 'none', 'replace');
}
let PAGE_TITLE = 'Listening Practice';
if (loading) return <LoadingScreen />;
return (
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
<div style={{ width: '15vw' }}>
<IonButton shape={'round'} fill="clear" color="dark" onClick={() => router.push(QUIZ_MAIN_MENU_LINK)}>
<IonIcon size={'large'} slot={'icon-only'} icon={arrowBackCircleOutline}></IonIcon>
</IonButton>
</div>
<IonTitle>
<div style={{ textAlign: 'center' }}>{PAGE_TITLE}</div>
</IonTitle>
<div style={{ width: '15vw' }}></div>
</div>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div>{PAGE_TITLE}</div>
</IonTitle>
</IonToolbar>
</IonHeader>
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div style={{ margin: '1rem 0 1rem' }}>
<div
style={{
width: '50vw',
height: '30vw',
backgroundSize: 'cover',
backgroundImage: `url('/data/Lesson/images/quiz_listening_practice.jpg')`,
borderRadius: '1rem',
}}
></div>
</div>
<div>{t('Choose the Chapter you want to revise')}</div>
<div style={{ width: '80vw' }}>
{categories
.map((item) => item.cat_name)
.map((item_name, idx) => (
<div style={{ marginTop: '1.5rem' }} key={idx}>
<IonButton
color={COLOR_TEXT}
style={{ color: COLOR_TEXT }}
fill="outline"
expand="block"
onClick={() => startListeningPractice(idx)}
>
{item_name}
</IonButton>
</div>
))}
</div>
</div>
{/* */}
</IonContent>
</IonPage>
);
};
export default ListeningPractice;

View File

@@ -0,0 +1,33 @@
import { useEffect } from 'react';
// import Helloworld from './Helloworld';
import './style.css';
function CountDown({
start_match,
init_time_left_s,
setCountDown_s,
}: {
start_match: boolean;
init_time_left_s: number;
setCountDown_s: (count_down_s: number) => void;
}) {
useEffect(() => {
let time_left = init_time_left_s;
let int_timer: NodeJS.Timeout | undefined = undefined;
if (start_match) {
int_timer = setInterval(() => {
time_left = time_left - 1;
setCountDown_s(Math.max(time_left - 1, 0));
}, 1000);
}
return () => {
clearInterval(int_timer);
};
}, [start_match]);
return <></>;
}
export default CountDown;

View File

@@ -0,0 +1,48 @@
import { IonContent, IonPage, useIonRouter } from '@ionic/react';
import { useEffect } from 'react';
import { DEFAULT_FORWARD_TIMEOUT, MATCHING_FRENZY_LINK, QUIZ_MAIN_MENU_LINK } from '../../../constants';
import { useAppStateContext } from '../../../contexts/AppState';
function MatchingFrenzyMatchFinished() {
const router = useIonRouter();
const { setDisableUserTap, setTabActive, setMatchingFrenzyInProgress } = useAppStateContext();
useEffect(() => {
setDisableUserTap(true);
setMatchingFrenzyInProgress(false);
setTimeout(() => {
router.push(`${MATCHING_FRENZY_LINK}/result`, 'none', 'replace');
}, DEFAULT_FORWARD_TIMEOUT);
setTabActive(QUIZ_MAIN_MENU_LINK);
}, []);
return (
<>
<IonPage>
<IonContent fullscreen>
<div
style={{
width: '100%',
height: '90vh',
display: 'inline-flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
fontSize: '1.2rem',
gap: '1rem',
}}
>
<div style={{ fontSize: '3rem' }}>😜</div>
<div>Congratulations, </div>
<div>Match finished</div>
</div>
</IonContent>
</IonPage>
</>
);
}
export default MatchingFrenzyMatchFinished;

View File

@@ -0,0 +1,215 @@
import { IonButton, IonContent, IonIcon, IonPage, useIonRouter } from '@ionic/react';
import { alarmOutline, arrowBackCircleOutline } from 'ionicons/icons';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
import { LoadingScreen } from '../../components/LoadingScreen';
import { MATCHING_FRENZY_LINK, QUIZ_MAIN_MENU_LINK } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import { useMyIonQuizContext } from '../../contexts/MyIonQuiz';
import IMatchingFrenzyQuestion from '../../interfaces/IMatchingFrenzyQuestion';
import { listMatchingFrenzyContent } from '../../public_data/listMatchingFrenzyContent';
import { shuffleArray } from '../../utils/shuffleArray';
import CountDown from './CountDown';
import MatchingFrenzyCard from './MatchingFrenzyCard';
import PressStartToBegin from './PressStartToBegin';
import './style.css';
import ConfirmUserQuitQuiz from '../../components/ConfirmUserQuitQuiz';
function MatchingFrenzyMatchRun() {
const [loading, setLoading] = useState<boolean>(true);
const router = useIonRouter();
const { p_route } = useParams<{ p_route: string }>();
const i_p_route = parseInt(p_route);
const [question_list, setQuestionList] = useState<IMatchingFrenzyQuestion[] | []>([]);
const [current_question_meta, setCurrentQuestionMeta] = useState<IMatchingFrenzyQuestion | undefined>(undefined);
const [current_question, setCurrentQuestion] = useState(0);
const [num_correct, setNumCorrect] = useState(0);
const { setTabActive } = useAppStateContext();
const [answer_list, setAnswerList] = useState<string[]>([]);
const { MATCHING_FRENZY_COUNT_DOWN_S } = useAppStateContext();
const [countdown_s, setCountDown_s] = useState<number>(MATCHING_FRENZY_COUNT_DOWN_S);
const [show_time_left_s, setShowTimeLeft] = useState<string>('0:00');
const { setMatchingFrenzyInProgress } = useAppStateContext();
const { setMatchingFrenzyCurrentTest, setMatchingFrenzyResult, saveMatchingFrenzyResultToScoreBoard } =
useMyIonQuizContext();
let [start_match, setStartMatch] = useState(false);
let [test_random, setTestRandom] = useState(0);
const formatTimeDisplay = (s: number) => {
return `${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, '0')}`;
};
const nextQuestion = () => {
if (isEndOfQuestionList(current_question, question_list)) {
// consider last question sceranio
let num_correct_result = num_correct;
setMatchingFrenzyCurrentTest(i_p_route);
setMatchingFrenzyResult(num_correct_result);
saveMatchingFrenzyResultToScoreBoard(p_route, {
//
date: new Date().toISOString().slice(0, 10),
result: num_correct_result,
});
//
// reset num_correct for next game
setNumCorrect(0);
router.push(`${MATCHING_FRENZY_LINK}/finished`, 'none', 'replace');
} else {
let next_question_num = current_question + 1;
setCurrentQuestion(next_question_num);
setCurrentQuestionMeta(question_list[next_question_num]);
}
};
const incNumCorrect = () => {
setNumCorrect(num_correct + 1);
};
function processStartMatch() {
setMatchingFrenzyInProgress(true);
setStartMatch(true);
}
useEffect(() => {
setShowTimeLeft(formatTimeDisplay(countdown_s));
if (countdown_s <= 0) {
setMatchingFrenzyCurrentTest(i_p_route);
setMatchingFrenzyResult(num_correct);
saveMatchingFrenzyResultToScoreBoard(p_route, {
//
date: new Date().toISOString().slice(0, 10),
result: num_correct,
});
//
// reset num_correct for next game
setNumCorrect(0);
router.push(`${MATCHING_FRENZY_LINK}/finished`, 'none', 'replace');
}
}, [countdown_s]);
const [init_ans, setInitAns] = useState<string[]>([]);
useEffect(() => {
if (!current_question_meta) return;
let init_options = [...question_list.map((q) => q.word), ...answer_list, ...init_ans];
let all_answer_list = [...new Set(init_options)];
let wrong_answer_list = all_answer_list.filter((a) => a !== current_question_meta.word);
let answer_list_shuffle = shuffleArray(wrong_answer_list);
let sliced_shuffle_array = shuffleArray(answer_list_shuffle).slice(0, 2 + 1);
setAnswerList(shuffleArray([...sliced_shuffle_array, current_question_meta.word]));
}, [current_question_meta]);
useEffect(() => {
(async () => {
const res_json = await listMatchingFrenzyContent();
const cat_json = res_json[i_p_route];
const init_answer = cat_json.init_answer;
setInitAns(cat_json.init_answer);
let temp = res_json[i_p_route].content;
let shuffled_temp = shuffleArray(temp);
setQuestionList(shuffled_temp);
setCurrentQuestionMeta(cat_json.content[current_question]);
})();
setTabActive(QUIZ_MAIN_MENU_LINK);
setTestRandom(Math.random());
}, []);
const [show_confirm_user_exit, setShowConfirmUserExit] = useState<boolean>(false);
if (!current_question_meta) return <LoadingScreen />;
return (
<>
<IonPage>
<IonContent fullscreen>
{start_match ? (
<>
<div>
<div
style={{
marginLeft: '1rem',
marginRight: '1rem',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div style={{ width: '33vw' }}>
<IonButton color="dark" fill="clear" shape="round" onClick={() => setShowConfirmUserExit(true)}>
<IonIcon slot="icon-only" size="large" icon={arrowBackCircleOutline}></IonIcon>
</IonButton>
</div>
<div>
<IonIcon icon={alarmOutline}></IonIcon>
{countdown_s > 0 ? show_time_left_s : 'times up'}
</div>
<div style={{ width: '33vw', display: 'flex', justifyContent: 'flex-end' }}>
Matches:{num_correct}
</div>
</div>
<div style={{ margin: '1rem' }}></div>
{/* */}
{/*
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', flexDirection: 'row', gap: '0.3rem' }}>
<IonIcon icon={alarmOutline}></IonIcon>
{countdown_s > 0 ? show_time_left_s : 'times up'}
</div>
<div>Matches:{num_correct}</div>
</div>
*/}
<MatchingFrenzyCard
num_correct={num_correct}
incNumCorrect={incNumCorrect}
//
nextQuestion={nextQuestion}
question_meta={{
//
word_c: current_question_meta.word_c,
modal_answer: current_question_meta.word,
}}
shuffle_word_list={answer_list}
/>
</div>
</>
) : (
<PressStartToBegin processStartMatch={processStartMatch} />
)}
<CountDown start_match={start_match} init_time_left_s={countdown_s} setCountDown_s={setCountDown_s} />
<ConfirmUserQuitQuiz
setShowConfirmUserExit={setShowConfirmUserExit}
show_confirm_user_exit={show_confirm_user_exit}
url_push_after_user_confirm={MATCHING_FRENZY_LINK}
/>
</IonContent>
</IonPage>
</>
);
}
export default MatchingFrenzyMatchRun;
function isEndOfQuestionList(current_question: number, question_list: [] | IMatchingFrenzyQuestion[]) {
return current_question >= question_list.length - 1;
}

View File

@@ -0,0 +1,148 @@
import { IonButton } from '@ionic/react';
import React, { useEffect, useRef, useState } from 'react';
import { DEBUG } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import { useMatchingFrenzyCorrectCount } from '../../contexts/MyIonMetric/MatchingFrenzyCorrectCount';
interface QuestionCardProps {
num_correct: number;
incNumCorrect: () => void;
//
nextQuestion: () => void;
question_meta: { word_c: string; modal_answer: string };
shuffle_word_list: string[];
}
const MatchingFrenzyCard: React.FC<QuestionCardProps> = ({
nextQuestion,
question_meta,
incNumCorrect,
num_correct,
shuffle_word_list,
}) => {
const [ignore_user_tap, setIgnoreUserTap] = React.useState(false);
let { word_c, modal_answer } = question_meta;
let { myIonMetricIncMatchingFrenzyCorrectCount } = useMatchingFrenzyCorrectCount();
let ref1 = useRef<HTMLIonButtonElement>(null);
let ref2 = useRef<HTMLIonButtonElement>(null);
let ref3 = useRef<HTMLIonButtonElement>(null);
let ref4 = useRef<HTMLIonButtonElement>(null);
let button_refs = [ref1, ref2, ref3, ref4];
let [user_answered, setUserAnswered] = useState<string | undefined>(undefined);
const { MATCHING_FRENZY_ANWERED_WAIT_S } = useAppStateContext();
function handleUserAnswer(answer: string, ref_button: React.RefObject<HTMLIonButtonElement>) {
if (ignore_user_tap) return;
if (answer.toLowerCase() === modal_answer.toLowerCase()) {
myIonMetricIncMatchingFrenzyCorrectCount();
incNumCorrect();
//
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'green';
if (ref_button && ref_button.current) ref_button.current.style.color = 'white';
//
setUserAnswered(answer);
//
} else {
if (ref_button && ref_button.current) ref_button.current.style.backgroundColor = 'red';
if (ref_button && ref_button.current) ref_button.current.style.color = 'white';
//
setUserAnswered(answer);
}
}
useEffect(() => {
DEBUG ? console.log({ 'user_answered is ': user_answered }) : '';
if (user_answered) {
DEBUG ? console.log('setup timer') : '';
setTimeout(() => {
ResetButtonsStyle();
setIgnoreUserTap(false);
nextQuestion();
// 027, fix reset for next question
setUserAnswered(undefined);
}, 500);
} else {
}
}, [user_answered]);
function ResetButtonsStyle() {
button_refs.forEach((ref) => {
if (ref && ref.current) ref.current.style.backgroundColor = 'unset';
if (ref && ref.current) ref.current.style.color = 'unset';
});
}
return (
<>
<div style={{ marginTop: '1rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ fontSize: '1.2rem', marginTop: '1rem' }}>{'Match the correct meaning'}</div>
<div
style={{
fontSize: '2rem',
width: '55vw',
height: '40vw',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid black',
borderRadius: '1rem',
marginTop: '2rem',
}}
>
<div style={{ margin: '2rem' }}>{word_c}</div>
</div>
</div>
<div
style={{
marginTop: '2rem',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
rowGap: '1rem',
}}
>
{shuffle_word_list.map((word: string, index: number) => {
return (
<div key={index}>
<IonButton
color="dark"
ref={button_refs[index]}
style={{ width: '43vw', height: '43vw' }}
onClick={() => {
setIgnoreUserTap(true);
handleUserAnswer(word, button_refs[index]);
}}
className={'answer_button'}
fill="outline"
>
<div
style={{
//
wordBreak: 'break-all',
hyphens: 'auto',
fontSize: word.length > 10 ? '0.75rem' : '1rem',
}}
>
{word}
</div>
</IonButton>
</div>
);
})}
</div>
</>
);
};
export default React.memo(MatchingFrenzyCard);

View File

@@ -0,0 +1,35 @@
import { IonButton } from '@ionic/react';
import { PRESS_START_TO_BEGIN, PRESS_START_TO_BEGIN_MESSAGE } from '../../constants';
interface ContainerProps {
processStartMatch: () => void;
}
function PressStartToBegin({ processStartMatch }: ContainerProps) {
return (
<>
<div
style={{
height: '90vh',
width: '100%',
display: 'inline-flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '0.5rem',
}}
>
<div style={{ fontSize: '3rem' }}>🎬</div>
<div style={{ marginTop: '1rem' }}>{PRESS_START_TO_BEGIN_MESSAGE}</div>
<div>{PRESS_START_TO_BEGIN}</div>
<div style={{ marginTop: '1rem' }}>
<IonButton fill="outline" color="dark" onClick={() => processStartMatch()} size="large">
{'start'}
</IonButton>
</div>
</div>
</>
);
}
export default PressStartToBegin;

View File

@@ -0,0 +1,125 @@
import { IonButton, IonContent, IonPage, useIonRouter } from '@ionic/react';
import { useEffect, useState } from 'react';
import { LoadingScreen } from '../../../components/LoadingScreen';
import { MATCHING_FRENZY_LINK, QUIZ_MAIN_MENU_LINK } from '../../../constants';
import { useAppStateContext } from '../../../contexts/AppState';
import { MatchingFrenzyResult, MatchingFrezyRanking } from '../../../contexts/MatchingFrezyRanking';
import { useMyIonQuizContext } from '../../../contexts/MyIonQuiz';
import { listMatchingFrenzyContent } from '../../../public_data/listMatchingFrenzyContent';
function MatchingFrenzyMatchResult() {
let router = useIonRouter();
let { matching_frenzy_result, matching_frenzy_current_test, loadMatchingFrenzyScoreBoard } = useMyIonQuizContext();
let [ranking, setRanking] = useState<MatchingFrenzyResult[] | []>([]);
const { setTabActive, setDisableUserTap } = useAppStateContext();
const [current_question_meta, setCurrentQuestionMeta] = useState<any>(undefined);
useEffect(() => {
(async () => {
const res_json: any = await listMatchingFrenzyContent();
setCurrentQuestionMeta(res_json[matching_frenzy_current_test]);
let temp: MatchingFrezyRanking = await loadMatchingFrenzyScoreBoard(matching_frenzy_current_test.toString());
let ranking: MatchingFrenzyResult[] = temp['ranking'];
setRanking(ranking);
})();
setTabActive(QUIZ_MAIN_MENU_LINK);
setDisableUserTap(false);
}, []);
if (!current_question_meta) return <LoadingScreen />;
// if (ranking.length == 0) return <>list is empty</>;
return (
<>
<IonPage>
<IonContent fullscreen>
<div
style={{
height: '85vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<div
style={{
width: '50vw',
height: '50vw',
borderRadius: '1rem',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundImage: `url(${current_question_meta.cat_image})`,
}}
></div>
<div style={{ fontSize: '1.3rem', textDecoration: 'underline', marginTop: '1rem' }}>
<div>{current_question_meta.cat_name}</div>
</div>
<div style={{ fontSize: '2rem', marginTop: '1rem' }}>
<div>{'Well Done'}</div>
</div>
<div style={{ marginTop: '0.5rem' }}>
<div>
{'Matches made:'}
{matching_frenzy_result}
</div>
</div>
<div>
<div style={{ width: '66vw', marginTop: '1rem' }}>
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
padding: '1rem',
backgroundColor: 'green',
color: 'white',
}}
>
Scoreboard
</div>
{ranking.map((r, i) => (
<div
key={i}
style={{
padding: '0.5rem',
display: 'flex',
justifyContent: 'space-between',
borderLeft: '1px solid grey',
borderRight: '1px solid grey',
borderBottom: '1px solid grey',
}}
>
<div>{`${r.date}`}</div>
<div>{`${r.result}`}</div>
</div>
))}
</div>
</div>
<div style={{ marginTop: '3rem' }}>
<IonButton color={'dark'} fill="outline" onClick={() => router.push(`${MATCHING_FRENZY_LINK}/`)}>
{'back to Main Menu'}
</IonButton>
</div>
</div>
</div>
</IonContent>
</IonPage>
</>
);
}
export default MatchingFrenzyMatchResult;

View File

@@ -0,0 +1,103 @@
import { IonButton, IonContent, IonHeader, IonIcon, IonPage, IonTitle, IonToolbar, useIonRouter } from '@ionic/react';
import { arrowBackCircleOutline } from 'ionicons/icons';
import { useEffect, useState } from 'react';
import { LoadingScreen } from '../../components/LoadingScreen';
import { DEBUG, MATCHING_FRENZY_LINK, QUIZ_MAIN_MENU_LINK } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import IMatchingFrenzyCategory from '../../interfaces/IMatchingFrenzyCategory';
import { listMatchingFrenzyContent } from '../../public_data/listMatchingFrenzyContent';
function MatchingFrenzySelectCategory() {
const PAGE_TITLE = 'Matching Frenzy';
const router = useIonRouter();
const [loading, setLoading] = useState<boolean>(true);
const [categories, setCategories] = useState<IMatchingFrenzyCategory[] | []>([]);
const { setTabActive } = useAppStateContext();
useEffect(() => {
listMatchingFrenzyContent().then((res_json: any) => {
setCategories(res_json);
DEBUG ? console.log({ res_json }) : '';
setLoading(false);
});
setTabActive(QUIZ_MAIN_MENU_LINK);
}, []);
if (loading) return <LoadingScreen />;
return (
<>
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
<div style={{ width: '15vw' }}>
<IonButton shape={'round'} fill="clear" color="dark" onClick={() => router.push(QUIZ_MAIN_MENU_LINK)}>
<IonIcon size={'large'} slot={'icon-only'} icon={arrowBackCircleOutline}></IonIcon>
</IonButton>
</div>
<IonTitle>
<div style={{ textAlign: 'center' }}>{PAGE_TITLE}</div>
</IonTitle>
<div style={{ width: '15vw' }}></div>
</div>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div>{PAGE_TITLE}</div>
</IonTitle>
</IonToolbar>
</IonHeader>
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div style={{ margin: '1rem 0 1rem' }}>
<div
style={{
width: '50vw',
height: '30vw',
backgroundSize: 'cover',
backgroundImage: `url('/data/Lesson/images/quiz_matching_frenzy.jpg')`,
borderRadius: '1rem',
}}
></div>
</div>
<div>Choose the Chapter you want to revise</div>
<div style={{ width: '80vw' }}>
{categories
.map((item) => item.cat_name)
.map((item_name, idx) => (
<div style={{ margin: '0.9rem 0 0.9rem' }} key={idx}>
<IonButton
color="dark"
fill="outline"
expand="block"
// href={`${MATCHING_FRENZY_LINK}/r/${idx}`}
onClick={() => {
router.push(`${MATCHING_FRENZY_LINK}/r/${idx}`);
}}
>
{item_name}
</IonButton>
</div>
))}
</div>
</div>
</IonContent>
</IonPage>
</>
);
}
export default MatchingFrenzySelectCategory;

View File

@@ -0,0 +1,4 @@
ion-button.answer_button::part(native) {
/* background-color: gold; */
padding: 1px;
}

View File

@@ -0,0 +1,10 @@
import { IonPage } from '@ionic/react';
import { useParams } from 'react-router';
const Page: React.FC = () => {
const { name } = useParams<{ name: string }>();
return <IonPage>{JSON.stringify(name)}</IonPage>;
};
export default Page;

View File

@@ -0,0 +1,55 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import { useEffect } from 'react';
import QuizzesMainMenuContainer from '../../components/QuizzesMainMenuContainer';
import { COLOR_TEXT, QUIZ_MAIN_MENU_LINK } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import './style.css';
import ExitButton from '../../components/ExitButton';
const QuizzesMainMenu: React.FC = () => {
let { setTabActive } = useAppStateContext();
useEffect(() => {
setTabActive(QUIZ_MAIN_MENU_LINK);
}, []);
return (
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar>
<div
style={{
//
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0 0.5rem 0 0.5rem',
}}
>
<div style={{ width: '10vw' }}></div>
<div>
<IonTitle>
<div style={{ color: COLOR_TEXT }}>{'Revision Time !'}</div>
</IonTitle>
</div>
<div style={{ width: '10vw' }}>
<ExitButton />
</div>
</div>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ color: COLOR_TEXT }}>{'Revision Time !'}</div>
</IonTitle>
</IonToolbar>
</IonHeader>
<QuizzesMainMenuContainer name="Setting page" />
</IonContent>
</IonPage>
);
};
export default QuizzesMainMenu;

View File

@@ -0,0 +1,435 @@
import { IonButton, IonContent, IonHeader, IonIcon, IonPage, IonTitle, IonToolbar, useIonRouter } from '@ionic/react';
import { heart } from 'ionicons/icons';
import { useEffect, useState } from 'react';
import AttentiveEarsProgressBar from '../../components/AttentiveEarsProgressBar';
import ConnectivesConquerorProgressBar from '../../components/ConnectivesConquerorProgressBar';
import GeniusProgressBar from '../../components/GeniusProgressBar';
import HardWorkerProgressBar from '../../components/HardWorkerProgressBar';
import { LoadingScreen } from '../../components/LoadingScreen';
import MatchmakingProgressBar from '../../components/MatchmakingProgressBar';
import NoFavoriteVocabModal from '../../components/NoFavoriteConnectivesModal';
import NoFavoriteConnectivesModal from '../../components/NoFavoriteVocabModal';
import {
ATTENTIVE_EARS_STAGES as ATTENTIVE_EAR_STAGES,
COLOR_TEXT,
CONNECTIVE_CONQUEROR_STAGES,
DEBUG,
FAVORITE_LINK,
GENIUS_STAGES,
HARDWORKER_STAGES,
MATCHMAKING_STAGES,
MY_FAVORITE,
RECORD_LINK,
} from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import { useMyIonFavorite } from '../../contexts/MyIonFavorite';
import './style.css';
import ExitButton from '../../components/ExitButton';
import CongratConnectiveConqueror from '../../components/Modal/Congratulation/ConnectiveConqueror';
import CongratGenius from '../../components/Modal/Congratulation/Genius';
import CongratHardworker from '../../components/Modal/Congratulation/Hardworker';
import CongratListeningProgress from '../../components/Modal/Congratulation/ListeningProgress';
import CongratMatchmaking from '../../components/Modal/Congratulation/Matchmaking';
import { useAppUseTime } from '../../contexts/MyIonMetric/AppUseTime';
import { useConnectivesRevisionCorrectCount } from '../../contexts/MyIonMetric/ConnectivesRevisionCorrectCount';
import { useFullmarkCount } from '../../contexts/MyIonMetric/FullmarkCount';
import { useListeningPracticeTimeSpent } from '../../contexts/MyIonMetric/ListeningPracticeTimeSpent';
import { useMatchingFrenzyCorrectCount } from '../../contexts/MyIonMetric/MatchingFrenzyCorrectCount';
const MyAchievementPage: React.FC = () => {
const [loading, setLoading] = useState(true);
//
const router = useIonRouter();
const [full_mark_count, setFullMarkCount] = useState(0);
const [hard_worker_progress, setHardWorkerProgress] = useState(0);
const [listening_practice_progress, setListeningPracticeProgress] = useState(0);
const [matching_frenzy_progress, setMatchingFrenzyProgress] = useState(0);
const [app_use_time, setAppUseTime] = useState<number>(0);
const [connectives_revision_progress, setConnectivesRevisionProgress] = useState(0);
const [num_fav_vocab, setnumFavVocab] = useState(0);
const [num_fav_connectives, setnumFavConnectives] = useState(0);
const [openNoFavVocab, setOpenNoFavVocab] = useState(false);
const [openNoFavConnectives, setOpenNoFavConnectives] = useState(false);
const { myIonStoreLoadFavoriteVocabulary, myIonStoreLoadFavoriteConnectives } = useMyIonFavorite();
const [openedCongraFullmarkTimes, setOpenedCongraFullmarkTimes] = useState<boolean>(false);
const [congra_msg, setCongraMsg] = useState<string>('');
const [openCongratListeningProgress, setOpenCongratListeningProgress] = useState(false);
const { myIonMetricGetAppUseTime } = useAppUseTime();
const { myIonMetricGetListeningPracticeTimeSpent } = useListeningPracticeTimeSpent();
const { myIonMetricGetConnectivesRevisionCorrectCount } = useConnectivesRevisionCorrectCount();
const { myIonMetricGetMatchingFrenzyCorrectCount } = useMatchingFrenzyCorrectCount();
const {
//
myIonMetricGetFullMarkCount,
myIonMetricGetFullMarkIgnore,
} = useFullmarkCount();
const {
//
Helloworld,
myIonMetricGetListeningPracticeProgressIgnore,
myIonMetricSetListeningPracticeProgressIgnore,
} = useListeningPracticeTimeSpent();
const [count_prompted, setCountPrompted] = useState(0);
//
async function promptFullmark(count_to_prompt: number) {
if (count_to_prompt == (await myIonMetricGetFullMarkIgnore()).count) return;
setCountPrompted(count_to_prompt);
setCongraMsg('You scored full marks ' + count_to_prompt + ' times.');
setOpenedCongraFullmarkTimes(true);
}
function checkPromptCongratulationFullMark(full_mark_count: number) {
const prompt_list = [10, 50, 100, 200, 700, 1000];
const b_prompt_list = Array(prompt_list.length).fill(false);
for (let i = 0; i < prompt_list.length; i++) {
if (full_mark_count > prompt_list[i]) {
b_prompt_list[i] = true;
}
}
let lastTrueIndex = -1;
for (let i = b_prompt_list.length - 1; i >= 0; i--) {
if (b_prompt_list[i]) {
lastTrueIndex = i;
break;
}
}
if (lastTrueIndex == -1) {
DEBUG ? console.log('prompt ignored') : '';
} else {
promptFullmark(prompt_list[lastTrueIndex]);
}
}
//
function dismissListeningPracticeCongratulation() {
myIonMetricSetListeningPracticeProgressIgnore(count_prompted);
}
//
async function promptMatchmaking(count_to_prompt: number) {
if (count_to_prompt == (await myIonMetricGetFullMarkIgnore()).count) return;
setCountPrompted(count_to_prompt);
setCongraMsg('You scored match making ' + count_to_prompt + ' times.');
setOpenedCongraFullmarkTimes(true);
}
function checkPromptMatchmaking(full_mark_count: number) {
const prompt_list = [10, 50, 100, 200, 700, 1000];
const b_prompt_list = Array(prompt_list.length).fill(false);
for (let i = 0; i < prompt_list.length; i++) {
if (full_mark_count > prompt_list[i]) {
b_prompt_list[i] = true;
}
}
let lastTrueIndex = -1;
for (let i = b_prompt_list.length - 1; i >= 0; i--) {
if (b_prompt_list[i]) {
lastTrueIndex = i;
break;
}
}
if (lastTrueIndex == -1) {
DEBUG ? console.log('prompt ignored') : '';
} else {
promptMatchmaking(prompt_list[lastTrueIndex]);
}
}
let { tab_active, setTabActive } = useAppStateContext();
useEffect(() => {
if (full_mark_count > 0) {
checkPromptCongratulationFullMark(full_mark_count);
}
}, [tab_active, full_mark_count]);
useEffect(() => {}, [tab_active, matching_frenzy_progress]);
useEffect(() => {
if (connectives_revision_progress > 0) checkPromptMatchmaking(connectives_revision_progress);
}, [tab_active, connectives_revision_progress]);
useEffect(() => {}, [tab_active, app_use_time]);
const { setDisableUserTap } = useAppStateContext();
useEffect(() => {
(async () => {
let { time_spent_s } = await myIonMetricGetAppUseTime();
setHardWorkerProgress(time_spent_s);
const { count: full_mark_count } = await myIonMetricGetFullMarkCount();
setFullMarkCount(full_mark_count);
const { count: matching_frenzy_correct_count } = await myIonMetricGetMatchingFrenzyCorrectCount();
setMatchingFrenzyProgress(matching_frenzy_correct_count);
const { count: connectives_revision_correct_count } = await myIonMetricGetConnectivesRevisionCorrectCount();
setConnectivesRevisionProgress(connectives_revision_correct_count);
setLoading(false);
})();
(async () => {
let temp_fav_list = await myIonStoreLoadFavoriteVocabulary();
setnumFavVocab(temp_fav_list.length);
let temp_fav_c_list = await myIonStoreLoadFavoriteConnectives();
setnumFavConnectives(temp_fav_c_list.length);
})();
setTabActive(RECORD_LINK);
(async () => {
let temp = await myIonMetricGetListeningPracticeTimeSpent();
setListeningPracticeProgress(temp.time_spent_s);
})();
setDisableUserTap(false);
}, []);
if (loading) return <LoadingScreen />;
return (
<>
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar>
<div
style={{
//
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0 0.5rem 0 0.5rem',
}}
>
<IonTitle>{MY_FAVORITE}</IonTitle>
<div>
<ExitButton />
</div>
</div>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ color: COLOR_TEXT }}>{'My Achievement'}</div>
</IonTitle>
</IonToolbar>
</IonHeader>
<div
style={{
marginLeft: '1rem',
marginRight: '1rem',
marginBottom: '1rem',
//
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
color: COLOR_TEXT,
}}
>
<div
style={{
// 003_remove_setting_screen, hide achievement meters from "My Favorite" page
// display: 'flex',
display: 'none',
flexDirection: 'column',
alignItems: 'flex-start',
width: '100%',
}}
>
<div style={{ marginTop: '2rem', width: '100%' }}>
<div style={{ fontSize: '1.2rem' }}>Genius</div>
<div style={{ fontSize: '0.8rem' }}>No of times getting the perfect score</div>
<div style={{ fontSize: '0.8rem' }}>
{GENIUS_STAGES.join(' / ')} {DEBUG ? <span className="debug">({full_mark_count})</span> : ''}
</div>
<div style={{ marginTop: '0.1rem' }}>
<div style={{ fontSize: '0.8rem' }}> </div>
<div style={{ marginTop: '0.2rem' }}>
<GeniusProgressBar value={full_mark_count} range_list={[10, 50, 100, 300, 700, 1000]} />
</div>
</div>
</div>
<div style={{ marginTop: '2rem', width: '100%' }}>
<div style={{ fontSize: '1.2rem', color: COLOR_TEXT }}>Hard worker</div>
<div style={{ fontSize: '0.8rem' }}>Overall Practice duration (In mint.res)</div>
<div style={{ fontSize: '0.8rem' }}>
{HARDWORKER_STAGES.join(' / ')} min{' '}
{DEBUG ? <span className="debug">({(hard_worker_progress / 60).toFixed(3)})</span> : ''}
</div>
<div style={{ marginTop: '0.1rem' }}>
<div style={{ fontSize: '0.8rem' }}> </div>
<div style={{ marginTop: '0.2rem' }}>
<HardWorkerProgressBar
value={Math.floor(hard_worker_progress / 60)}
range_list={[5, 50, 100, 500, 1000, 1500, 3000]}
/>
</div>
</div>
</div>
<div style={{ marginTop: '2rem', width: '100%' }}>
<div style={{ fontSize: '1.2rem', color: COLOR_TEXT }}>Attentive Ears</div>
<div style={{ fontSize: '0.8rem' }}>Duration of listening Track completed</div>
<div style={{ fontSize: '0.8rem' }}>
{ATTENTIVE_EAR_STAGES.join(' / ')} min
{DEBUG ? <span className="debug">({(listening_practice_progress / 60).toFixed(3)})</span> : ''}
</div>
<div style={{ marginTop: '0.1rem' }}>
<div style={{ fontSize: '0.8rem' }}> </div>
<div style={{ marginTop: '0.2rem' }}>
<AttentiveEarsProgressBar
value={Math.floor(listening_practice_progress / 60)}
range_list={ATTENTIVE_EAR_STAGES}
/>
</div>
</div>
</div>
<div style={{ marginTop: '2rem', width: '100%' }}>
<div style={{ fontSize: '1.2rem', color: COLOR_TEXT }}>Matchmaking</div>
<div style={{ fontSize: '0.8rem' }}>No of correct matches mode</div>
<div style={{ fontSize: '0.8rem' }}>
{MATCHMAKING_STAGES.join(' / ')}
{DEBUG ? <span className="debug">({(matching_frenzy_progress / 60).toFixed(3)})</span> : ''}
</div>
<div style={{ marginTop: '0.1rem' }}>
<div style={{ fontSize: '0.8rem' }}> </div>
<div style={{ marginTop: '0.2rem' }}>
<MatchmakingProgressBar
value={matching_frenzy_progress}
range_list={[30, 100, 250, 500, 1500, 3000, 8000]}
/>
</div>
</div>
</div>
<div style={{ marginTop: '2rem', width: '100%' }}>
<div style={{ fontSize: '1.2rem', color: COLOR_TEXT }}>Connectives Conqueror</div>
<div style={{ fontSize: '0.8rem' }}>No of completed connective exercises</div>
<div style={{ fontSize: '0.8rem' }}>
{CONNECTIVE_CONQUEROR_STAGES.join(' / ')}{' '}
{DEBUG ? <span className="debug">({connectives_revision_progress})</span> : ''}
</div>
<div style={{ marginTop: '0.1rem' }}>
<div style={{ fontSize: '0.8rem' }}> </div>
<div style={{ marginTop: '0.2rem' }}>
<ConnectivesConquerorProgressBar
value={connectives_revision_progress}
range_list={CONNECTIVE_CONQUEROR_STAGES}
/>
</div>
</div>
</div>
</div>
<div
style={{
//
display: 'none',
borderTop: '1px solid #ccc',
width: '100%',
marginTop: '1rem',
}}
></div>
<div
style={{
marginTop: '1rem',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
//
}}
>
{/* */}
<IonButton
fill="outline"
color="dark"
onClick={() => (num_fav_vocab > 0 ? router.push(`${FAVORITE_LINK}/v`) : setOpenNoFavVocab(true))}
>
<div
style={{
padding: '1rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '1rem',
}}
>
<IonIcon icon={heart} color="danger" size="large"></IonIcon>
<div>
{'Liked Vocabulary'} <span style={{ fontWeight: 'bold' }}>({num_fav_vocab || 0})</span>
</div>
</div>
</IonButton>
{/* */}
<IonButton
fill="outline"
color="dark"
onClick={() =>
num_fav_connectives > 0 ? router.push(`${FAVORITE_LINK}/c`) : setOpenNoFavConnectives(true)
}
>
<div
style={{
padding: '1rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '1rem',
}}
>
<IonIcon icon={heart} color="danger" size="large"></IonIcon>
<div>
{'Liked Connectives'} <span style={{ fontWeight: 'bold' }}>({num_fav_connectives || 0})</span>
</div>
</div>
</IonButton>
{/* */}
</div>
</div>
</IonContent>
</IonPage>
{/* */}
<NoFavoriteVocabModal open={openNoFavVocab} setOpen={setOpenNoFavVocab} />
<NoFavoriteConnectivesModal open={openNoFavConnectives} setOpen={setOpenNoFavConnectives} />
{/* */}
{/*
<CongratulationAchievementModal
count_prompted={count_prompted}
opened={openedCongraFullmarkTimes}
setOpened={setOpenedCongraFullmarkTimes}
message={congra_msg}
/>
*/}
{/* */}
{/* <CongratHelloworld /> */}
<CongratGenius />
<CongratHardworker />
<CongratListeningProgress />
<CongratMatchmaking />
<CongratConnectiveConqueror />
</>
);
};
export default MyAchievementPage;

View File

@@ -0,0 +1,37 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import { useEffect } from 'react';
import SettingContainer from '../../components/SettingContainer';
import { SETTING_LINK } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
const Setting: React.FC = () => {
const { setTabActive } = useAppStateContext();
useEffect(() => {
setTabActive(SETTING_LINK);
}, []);
return (
<IonPage>
<IonHeader className="ion-no-border">
<IonToolbar>
<IonTitle>
<div>{'Setting'}</div>
</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div>{'Setting'}</div>
</IonTitle>
</IonToolbar>
</IonHeader>
<SettingContainer name="Setting page" />
</IonContent>
</IonPage>
);
};
export default Setting;

View File

@@ -0,0 +1,25 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import ExploreContainer from '../components/ExploreContainer';
import './Tab1.css';
const Tab1: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab 1</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Tab 1</IonTitle>
</IonToolbar>
</IonHeader>
<ExploreContainer name="Tab 1 page" />
</IonContent>
</IonPage>
);
};
export default Tab1;

View File

@@ -0,0 +1,25 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import ExploreContainer from '../components/ExploreContainer';
import './Tab2.css';
const Tab2: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab 2</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Tab 2</IonTitle>
</IonToolbar>
</IonHeader>
<ExploreContainer name="Tab 2 page" />
</IonContent>
</IonPage>
);
};
export default Tab2;

View File

@@ -0,0 +1,25 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import ExploreContainer from '../components/ExploreContainer';
import './Tab3.css';
const Tab3: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab 3</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Tab 3</IonTitle>
</IonToolbar>
</IonHeader>
<ExploreContainer name="Tab 3 page" />
</IonContent>
</IonPage>
);
};
export default Tab3;