init commit,
This commit is contained in:
@@ -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[];
|
||||
}
|
@@ -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;
|
147
002_source/ionic_mobile/src/pages/ConnectiveRevision/QuizRun.tsx
Normal file
147
002_source/ionic_mobile/src/pages/ConnectiveRevision/QuizRun.tsx
Normal 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;
|
@@ -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;
|
@@ -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;
|
@@ -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));
|
||||
}
|
155
002_source/ionic_mobile/src/pages/DebugPage/index.tsx
Normal file
155
002_source/ionic_mobile/src/pages/DebugPage/index.tsx
Normal 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;
|
@@ -0,0 +1,3 @@
|
||||
export function initUserConfig() {
|
||||
return localStorage.setItem('user_config', JSON.stringify({ version: '0.1' }));
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
export function listAllUserConfig(): { [key: string]: any } {
|
||||
let current_config_string = localStorage.getItem('user_config') || '{}';
|
||||
return JSON.parse(current_config_string);
|
||||
}
|
@@ -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];
|
||||
}
|
@@ -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));
|
||||
}
|
@@ -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;
|
@@ -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 <></>;
|
||||
};
|
@@ -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;
|
@@ -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;
|
||||
}
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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 <></>;
|
||||
};
|
369
002_source/ionic_mobile/src/pages/Favorite/WordPage/index.tsx
Normal file
369
002_source/ionic_mobile/src/pages/Favorite/WordPage/index.tsx
Normal 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;
|
@@ -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;
|
||||
}
|
@@ -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}`;
|
||||
}
|
@@ -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}`;
|
||||
}
|
@@ -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;
|
@@ -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;
|
@@ -0,0 +1,3 @@
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
9
002_source/ionic_mobile/src/pages/Lesson/EndOfList.tsx
Normal file
9
002_source/ionic_mobile/src/pages/Lesson/EndOfList.tsx
Normal 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;
|
0
002_source/ionic_mobile/src/pages/Lesson/Lesson.css
Normal file
0
002_source/ionic_mobile/src/pages/Lesson/Lesson.css
Normal file
82
002_source/ionic_mobile/src/pages/Lesson/LessonContainer.tsx
Normal file
82
002_source/ionic_mobile/src/pages/Lesson/LessonContainer.tsx
Normal 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;
|
@@ -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 <></>;
|
||||
};
|
326
002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx
Normal file
326
002_source/ionic_mobile/src/pages/Lesson/WordPage/index.tsx
Normal 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;
|
31
002_source/ionic_mobile/src/pages/Lesson/WordPage/style.css
Normal file
31
002_source/ionic_mobile/src/pages/Lesson/WordPage/style.css
Normal 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;
|
||||
}
|
@@ -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}`;
|
||||
}
|
@@ -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;
|
||||
}
|
134
002_source/ionic_mobile/src/pages/Lesson/index.tsx
Normal file
134
002_source/ionic_mobile/src/pages/Lesson/index.tsx
Normal 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;
|
@@ -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 <></>;
|
||||
};
|
21
002_source/ionic_mobile/src/pages/LessonWord/style.css
Normal file
21
002_source/ionic_mobile/src/pages/LessonWord/style.css
Normal 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;
|
||||
}
|
@@ -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;
|
@@ -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;
|
@@ -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 <></>;
|
||||
};
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
110
002_source/ionic_mobile/src/pages/ListeningPractice/index.tsx
Normal file
110
002_source/ionic_mobile/src/pages/ListeningPractice/index.tsx
Normal 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;
|
@@ -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;
|
@@ -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;
|
215
002_source/ionic_mobile/src/pages/MatchingFrenzy/MatchRun.tsx
Normal file
215
002_source/ionic_mobile/src/pages/MatchingFrenzy/MatchRun.tsx
Normal 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;
|
||||
}
|
@@ -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);
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -0,0 +1,4 @@
|
||||
ion-button.answer_button::part(native) {
|
||||
/* background-color: gold; */
|
||||
padding: 1px;
|
||||
}
|
10
002_source/ionic_mobile/src/pages/Page.tsx
Normal file
10
002_source/ionic_mobile/src/pages/Page.tsx
Normal 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;
|
55
002_source/ionic_mobile/src/pages/QuizzesMainMenu/index.tsx
Normal file
55
002_source/ionic_mobile/src/pages/QuizzesMainMenu/index.tsx
Normal 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;
|
435
002_source/ionic_mobile/src/pages/Record/index.tsx
Normal file
435
002_source/ionic_mobile/src/pages/Record/index.tsx
Normal 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;
|
0
002_source/ionic_mobile/src/pages/Record/style.css
Normal file
0
002_source/ionic_mobile/src/pages/Record/style.css
Normal file
37
002_source/ionic_mobile/src/pages/Setting/indx.tsx
Normal file
37
002_source/ionic_mobile/src/pages/Setting/indx.tsx
Normal 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;
|
0
002_source/ionic_mobile/src/pages/Tab1.css
Normal file
0
002_source/ionic_mobile/src/pages/Tab1.css
Normal file
25
002_source/ionic_mobile/src/pages/Tab1.tsx
Normal file
25
002_source/ionic_mobile/src/pages/Tab1.tsx
Normal 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;
|
0
002_source/ionic_mobile/src/pages/Tab2.css
Normal file
0
002_source/ionic_mobile/src/pages/Tab2.css
Normal file
25
002_source/ionic_mobile/src/pages/Tab2.tsx
Normal file
25
002_source/ionic_mobile/src/pages/Tab2.tsx
Normal 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;
|
0
002_source/ionic_mobile/src/pages/Tab3.css
Normal file
0
002_source/ionic_mobile/src/pages/Tab3.css
Normal file
25
002_source/ionic_mobile/src/pages/Tab3.tsx
Normal file
25
002_source/ionic_mobile/src/pages/Tab3.tsx
Normal 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;
|
Reference in New Issue
Block a user