init commit,

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

View File

@@ -0,0 +1,7 @@
import { render } from '@testing-library/react';
import App from './App';
test('renders without crashing', () => {
const { baseElement } = render(<App />);
expect(baseElement).toBeDefined();
});

View File

@@ -0,0 +1,199 @@
import { DEBUG, DEBUG_LINK, LESSON_LINK, QUIZ_MAIN_MENU_LINK, RECORD_LINK, SETTING_LINK } from './constants';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
/* Basic CSS for apps built with Ionic */
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
/* Optional CSS utils that can be commented out */
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/typography.css';
import '@ionic/react/css/display.css';
/**
* Ionic Dark Mode
* -----------------------------------------------------
* For more info, please see:
* https://ionicframework.com/docs/theming/dark-mode
*/
/* import '@ionic/react/css/palettes/dark.always.css'; */
/* import '@ionic/react/css/palettes/dark.class.css'; */
// import '@ionic/react/css/palettes/dark.system.css';
/* Theme variables */
import './theme/variables.css';
import './debug.css';
import './style.css';
import './google_fonts.css';
import { App as CapacitorApp } from '@capacitor/app';
import {
IonApp,
IonIcon,
IonLabel,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs,
setupIonicReact,
useIonRouter,
} from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { bookOutline, bug, heartOutline, newspaperOutline, settingsOutline } from 'ionicons/icons';
import { useTranslation } from 'react-i18next';
import ConfirmUserExit from './components/ConfirmUserExit';
import { DisableUserTap } from './components/DisableUserTap';
import ContextMeta from './contexts';
import { useAppStateContext } from './contexts/AppState';
import { useMyIonQuizContext } from './contexts/MyIonQuiz';
import { RouteConfig } from './RouteConfig';
let active_color = 'tomato';
let inactive_color = 'gray';
CapacitorApp.addListener('appStateChange', ({ isActive }) => {
console.log('App state changed. Is active?', isActive);
});
CapacitorApp.addListener('backButton', ({ canGoBack }) => {
if (!canGoBack) {
// CapacitorApp.minimizeApp();
// alert('blablabla');
}
});
setupIonicReact();
const TabButtons: React.FC = () => {
let router = useIonRouter();
const { t } = useTranslation();
let {
url_push_after_user_confirm,
setURLPushAfterUserConfirm,
setShowConfirmUserExit,
listening_practice_in_progress,
setListeningPracticeInProgress,
matching_frenzy_in_progress,
connective_revision_in_progress,
tab_active,
setTabActive,
} = useAppStateContext();
const { resetListeningPracticeCorrectionList } = useMyIonQuizContext();
function goSwitchPage(url: string) {
if (listening_practice_in_progress || matching_frenzy_in_progress || connective_revision_in_progress) {
if (url != QUIZ_MAIN_MENU_LINK) {
setURLPushAfterUserConfirm(url);
setShowConfirmUserExit(true);
}
} else {
setTabActive(url);
router.push(url, 'none', 'replace');
}
}
return (
<>
<IonTabBar slot="bottom">
<IonTabButton
tab="lesson"
onClick={() => goSwitchPage(LESSON_LINK)}
style={{ color: tab_active == LESSON_LINK ? active_color : inactive_color }}
>
<IonIcon aria-hidden="true" icon={bookOutline} size="large" />
</IonTabButton>
{/* */}
<IonTabButton
tab="quiz"
onClick={() => goSwitchPage(QUIZ_MAIN_MENU_LINK)}
style={{ color: tab_active == QUIZ_MAIN_MENU_LINK ? active_color : inactive_color }}
>
<IonIcon aria-hidden="true" icon={newspaperOutline} size="large" />
</IonTabButton>
<IonTabButton
tab="quiz"
onClick={() => goSwitchPage(QUIZ_MAIN_MENU_LINK)}
style={{ color: tab_active == QUIZ_MAIN_MENU_LINK ? active_color : inactive_color }}
></IonTabButton>
{/* */}
<IonTabButton
tab="record"
onClick={() => goSwitchPage(RECORD_LINK)}
style={{ color: tab_active == RECORD_LINK ? active_color : inactive_color }}
>
<IonIcon aria-hidden="true" icon={heartOutline} size="large" />
</IonTabButton>
{/* */}
{/* 003_remove_setting_screen, hide setting on bottom tabs */}
<IonTabButton
tab="setting"
onClick={() => goSwitchPage(SETTING_LINK)}
style={{ color: tab_active == SETTING_LINK ? active_color : inactive_color }}
>
<IonIcon aria-hidden="true" icon={settingsOutline} size="large" />
</IonTabButton>
{DEBUG ? (
<IonTabButton
tab="debug"
onClick={() => goSwitchPage(DEBUG_LINK)}
// href={DEBUG_LINK}
>
<IonIcon aria-hidden="true" icon={bug} color="danger" />
<IonLabel color="danger">Debug</IonLabel>
</IonTabButton>
) : (
<></>
)}
</IonTabBar>
</>
);
};
const App: React.FC = () => {
return (
<IonApp>
<ContextMeta>
<DisableUserTap>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<RouteConfig />
</IonRouterOutlet>
<TabButtons />
</IonTabs>
<div
style={{
zIndex: 9999,
backgroundColor: 'tomato',
position: 'fixed',
bottom: '1rem',
height: '4rem',
width: '4rem',
left: 'calc( (100vw - 4rem ) / 2 )',
borderRadius: '50%',
//
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div style={{ fontWeight: 'bold', fontSize: '2rem', color: 'white' }}>AI</div>
</div>
{/* */}
<ConfirmUserExit />
{/* */}
</IonReactRouter>
</DisableUserTap>
</ContextMeta>
</IonApp>
);
};
export default App;

View File

@@ -0,0 +1,158 @@
import { Redirect, Route } from 'react-router-dom';
import {
CONNECTIVE_REVISION_LINK,
DEBUG_LINK,
FAVORITE_LINK,
LESSON_LINK,
LESSON_WORD_PAGE_LINK,
LISTENING_PRACTICE_LINK,
MATCHING_FRENZY_LINK,
QUIZ_MAIN_MENU_LINK,
RECORD_LINK,
} from './constants';
import ConnectiveRevisionQuizRun from './pages/ConnectiveRevision/QuizRun';
import ConnectiveRevisionQuizResult from './pages/ConnectiveRevision/Result';
// import CorrectionRoute from './pages/MatchingFrenzy/Route/Correction';
// import PracticeFinish from './pages/MatchingFrenzy/Finish';
import ConnectiveRevisionSelectCategory from './pages/ConnectiveRevision/SelectCategory';
import DebugPage from './pages/DebugPage';
import FavConnectivesPage from './pages/Favorite/Connectives';
import ConnectivesWordPage from './pages/Favorite/ConnectivesWordPage';
import FavVocabularyPage from './pages/Favorite/Vocabulary';
import FavoriteVocabularyPage from './pages/Favorite/WordPage';
import ConnectivesPage from './pages/Lesson/ConnectivesPage';
import Lesson from './pages/Lesson/index';
import LessonWordPage from './pages/Lesson/WordPage';
//
import ListeningPractice from './pages/ListeningPractice';
import PracticeFinish from './pages/ListeningPractice/Finish';
import PracticeResult from './pages/ListeningPractice/Result';
import QuestionRoute from './pages/ListeningPractice/Route';
import CorrectionRoute from './pages/ListeningPractice/Route/CorrectionRoute';
import MatchingFrenzyMatchFinished from './pages/MatchingFrenzy/Finished';
import MatchingFrenzyMatchRun from './pages/MatchingFrenzy/MatchRun';
import MatchingFrenzyMatchResult from './pages/MatchingFrenzy/Result';
//
import MatchingFrenzySelectCategory from './pages/MatchingFrenzy/SelectCategory';
import Page from './pages/Page';
import QuizzesMainMenu from './pages/QuizzesMainMenu';
//
import MyAchievementPage from './pages/Record/index';
import Setting from './pages/Setting/indx';
import Tab1 from './pages/Tab1';
import Tab2 from './pages/Tab2';
import Tab3 from './pages/Tab3';
function RouteConfig() {
return (
<>
{/* */}
<Route exact path={QUIZ_MAIN_MENU_LINK}>
<QuizzesMainMenu />
</Route>
{/* */}
<Route exact path={RECORD_LINK}>
<MyAchievementPage />
</Route>
{/* */}
<Route exact path={`${LESSON_LINK}/a/:act_category`}>
<Lesson />
</Route>
<Route exact path={LESSON_LINK}>
<Lesson />
</Route>
{/* */}
{/* http://localhost:5173/listening_practice/c/0 */}
<Route exact path={`${LISTENING_PRACTICE_LINK}/c/:p_route`}>
<CorrectionRoute />
</Route>
{/* http://localhost:5173/listening_practice/r/0 */}
<Route exact path={`${LISTENING_PRACTICE_LINK}/r/:p_route`}>
<QuestionRoute />
</Route>
<Route exact path={`${LISTENING_PRACTICE_LINK}/finished`}>
<PracticeFinish />
</Route>
<Route exact path={`${LISTENING_PRACTICE_LINK}/result`}>
<PracticeResult />
</Route>
<Route exact path={LISTENING_PRACTICE_LINK}>
<ListeningPractice />
</Route>
{/* */}
<Route exact path={`${CONNECTIVE_REVISION_LINK}/r/:p_route`}>
<ConnectiveRevisionQuizRun />
</Route>
<Route exact path={`${CONNECTIVE_REVISION_LINK}/finished`}>
<ConnectiveRevisionQuizResult />
</Route>
<Route exact path={CONNECTIVE_REVISION_LINK}>
<ConnectiveRevisionSelectCategory />
</Route>
{/* */}
<Route exact path={`${MATCHING_FRENZY_LINK}/r/:p_route`}>
<MatchingFrenzyMatchRun />
</Route>
<Route exact path={`${MATCHING_FRENZY_LINK}/finished`}>
<MatchingFrenzyMatchFinished />
</Route>
<Route exact path={`${MATCHING_FRENZY_LINK}/result`}>
<MatchingFrenzyMatchResult />
</Route>
<Route exact path={MATCHING_FRENZY_LINK}>
<MatchingFrenzySelectCategory />
</Route>
<Route exact path={`${FAVORITE_LINK}/v/:lesson_idx/:cat_idx/:word_idx`}>
<FavoriteVocabularyPage />
</Route>
<Route exact path={`${FAVORITE_LINK}/v`}>
<FavVocabularyPage />
</Route>
<Route exact path={`${FAVORITE_LINK}/c/:lesson_idx/:cat_idx/:word_idx`}>
{/* <FavoriteVocabularyPage /> */}
{/* <FavConnectivesContentPage /> */}
<ConnectivesWordPage />
</Route>
<Route exact path={`${FAVORITE_LINK}/c`}>
<FavConnectivesPage />
</Route>
<Route exact path={`${LESSON_WORD_PAGE_LINK}/v/:lesson_idx/:cat_idx/:word_idx`}>
<LessonWordPage />
</Route>
<Route exact path={`${LESSON_WORD_PAGE_LINK}/c/:lesson_idx/:cat_idx/:word_idx`}>
<ConnectivesPage />
</Route>
{/* TODO: remove below */}
<Route exact path="/tab1">
<Tab1 />
</Route>
<Route exact path="/tab2">
<Tab2 />
</Route>
<Route path="/tab3">
<Tab3 />
</Route>
<Route path="/setting">
<Setting />
</Route>
<Route path="/page/:name" exact={true}>
<Page />
</Route>
<Route exact path={DEBUG_LINK}>
<DebugPage />
</Route>
<Route exact path="/">
<Redirect to={LESSON_LINK} />
</Route>
</>
);
}
export { RouteConfig };

View File

@@ -0,0 +1,68 @@
import { FunctionComponent } from 'react';
import './style.css';
interface AudioSeekBarProps {
value: number;
range_list: number[];
}
const AttentiveEarsProgressBar: FunctionComponent<AudioSeekBarProps> = ({ value, range_list }) => {
let test_value = value;
let scale = [];
for (let i = 0; i < range_list.length; i++) {
let scale_full_range = range_list[i];
let scale_range = i > 0 ? scale_full_range - range_list[i - 1] : scale_full_range;
let fill = 0;
if (test_value > scale_full_range) {
fill = scale_full_range;
} else {
if (i > 0) {
fill = Math.max(0, test_value - range_list[i - 1]);
} else {
fill = test_value;
}
}
let fill_pct = Math.min(fill / scale_range, 1);
scale.push([fill_pct]);
}
return (
<>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{scale.map((item: any, index: number) => {
return (
<div
style={{
width: `calc( 100% / ${range_list.length} )`,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
}}
>
<div style={{ fontSize: '0.5rem' }}>{range_list[index]}</div>
<div
className={`audioSeekBar playBar_t__seek`}
style={{
// border: '1px solid #000',
borderTop: '1px solid #000',
borderBottom: '1px solid #000',
borderLeft: index == 0 ? '1px solid #000' : '',
borderRight: index == scale.length - 1 ? '1px solid #000' : '',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<div style={{ width: `${item * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
);
})}
</div>
</>
);
};
export default AttentiveEarsProgressBar;

View File

@@ -0,0 +1,29 @@
.audioSeekBar {
cursor: pointer;
background-color: white;
overflow: hidden;
}
.audioSeekBar__tick {
background-color: rgb(56, 182, 255);
height: 100%;
}
.playBar_t {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: #1c192f;
display: flex;
align-items: flex-start;
padding: 0px 50px;
}
.playBar_t__seek {
width: 100%;
height: 10px;
/* border-radius: 12px; */
margin-right: 0px;
}

View File

@@ -0,0 +1,79 @@
import { IonButton, IonContent, IonModal, useIonRouter } from '@ionic/react';
import { useRef } from 'react';
import { COLOR_TEXT } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import { useMyIonQuizContext } from '../../contexts/MyIonQuiz';
function ConfirmUserExit() {
let {
setTabActive,
show_confirm_user_exit,
setShowConfirmUserExit,
url_push_after_user_confirm,
setListeningPracticeInProgress,
setConnectiveRevisionInProgress,
setMatchingFrenzyInProgress,
} = useAppStateContext();
const modal = useRef<HTMLIonModalElement>(null);
const router = useIonRouter();
const { resetListeningPracticeCorrectionList } = useMyIonQuizContext();
return (
<>
<IonModal
isOpen={show_confirm_user_exit}
ref={modal}
// trigger='open-modal'
initialBreakpoint={0.25}
breakpoints={[0, 0.25, 0.5, 0.75]}
onDidDismiss={() => setShowConfirmUserExit(false)}
>
<IonContent className="ion-padding">
<div style={{ margin: '2rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ color: COLOR_TEXT }}>Are you sure want to quit ?</div>
<div
style={{
margin: '2rem',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: '3rem',
}}
>
<IonButton
color="dark"
shape={'round'}
style={{ minWidth: '80px' }}
onClick={() => setShowConfirmUserExit(false)}
>
No
</IonButton>
<IonButton
fill="outline"
shape={'round'}
style={{ minWidth: '80px', color: COLOR_TEXT }}
color="dark"
onClick={() => {
setShowConfirmUserExit(false);
setListeningPracticeInProgress(false);
setMatchingFrenzyInProgress(false);
setConnectiveRevisionInProgress(false);
//
resetListeningPracticeCorrectionList();
//
setTabActive(url_push_after_user_confirm);
//
router.push(url_push_after_user_confirm, 'none', 'replace');
}}
>
Yes
</IonButton>
</div>
</div>
</IonContent>
</IonModal>
</>
);
}
export default ConfirmUserExit;

View File

@@ -0,0 +1,86 @@
import { IonButton, IonContent, IonModal, useIonRouter } from '@ionic/react';
import { Dispatch, FunctionComponent, SetStateAction, useRef } from 'react';
import { COLOR_TEXT } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
import { useMyIonQuizContext } from '../../contexts/MyIonQuiz';
interface AudioSeekBarProps {
show_confirm_user_exit: boolean;
setShowConfirmUserExit: Dispatch<SetStateAction<boolean>>;
url_push_after_user_confirm: string;
}
const ConfirmUserQuitQuiz: FunctionComponent<AudioSeekBarProps> = ({
show_confirm_user_exit,
setShowConfirmUserExit,
//
url_push_after_user_confirm,
}) => {
let { setListeningPracticeInProgress, setConnectiveRevisionInProgress, setMatchingFrenzyInProgress } =
useAppStateContext();
let { setTabActive } = useAppStateContext();
const modal = useRef<HTMLIonModalElement>(null);
const router = useIonRouter();
const { resetListeningPracticeCorrectionList } = useMyIonQuizContext();
return (
<>
<IonModal
isOpen={show_confirm_user_exit}
ref={modal}
// trigger='open-modal'
initialBreakpoint={0.25}
breakpoints={[0, 0.25, 0.5, 0.75]}
onDidDismiss={() => setShowConfirmUserExit(false)}
>
<IonContent className="ion-padding">
<div style={{ margin: '2rem', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ color: COLOR_TEXT }}>Are you sure want to quit ?</div>
<div
style={{
margin: '2rem',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: '3rem',
}}
>
<IonButton
color="dark"
shape={'round'}
style={{ minWidth: '80px' }}
onClick={() => setShowConfirmUserExit(false)}
>
No
</IonButton>
<IonButton
fill="outline"
shape={'round'}
style={{ minWidth: '80px', color: COLOR_TEXT }}
color="dark"
onClick={() => {
setShowConfirmUserExit(false);
setListeningPracticeInProgress(false);
setMatchingFrenzyInProgress(false);
setConnectiveRevisionInProgress(false);
//
resetListeningPracticeCorrectionList();
//
setTabActive(url_push_after_user_confirm);
//
router.push(url_push_after_user_confirm, 'none', 'replace');
}}
>
Yes
</IonButton>
</div>
</div>
</IonContent>
</IonModal>
</>
);
};
export default ConfirmUserQuitQuiz;

View File

@@ -0,0 +1,68 @@
import { FunctionComponent } from 'react';
import './style.css';
interface AudioSeekBarProps {
value: number;
range_list: number[];
}
const ConnectivesConquerorProgressBar: FunctionComponent<AudioSeekBarProps> = ({ value, range_list }) => {
let test_value = value;
let scale = [];
for (let i = 0; i < range_list.length; i++) {
let scale_full_range = range_list[i];
let scale_range = i > 0 ? scale_full_range - range_list[i - 1] : scale_full_range;
let fill = 0;
if (test_value > scale_full_range) {
fill = scale_full_range;
} else {
if (i > 0) {
fill = Math.max(0, test_value - range_list[i - 1]);
} else {
fill = test_value;
}
}
let fill_pct = Math.min(fill / scale_range, 1);
scale.push([fill_pct]);
}
return (
<>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{scale.map((item: any, index: number) => {
return (
<div
style={{
width: `calc( 100% / ${range_list.length} )`,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
}}
>
<div style={{ fontSize: '0.5rem' }}>{range_list[index]}</div>
<div
className={`audioSeekBar playBar_t__seek`}
style={{
// border: '1px solid #000',
borderTop: '1px solid #000',
borderBottom: '1px solid #000',
borderLeft: index == 0 ? '1px solid #000' : '',
borderRight: index == scale.length - 1 ? '1px solid #000' : '',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<div style={{ width: `${item * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
);
})}
</div>
</>
);
};
export default ConnectivesConquerorProgressBar;

View File

@@ -0,0 +1,29 @@
.audioSeekBar {
cursor: pointer;
background-color: white;
overflow: hidden;
}
.audioSeekBar__tick {
background-color: rgb(56, 182, 255);
height: 100%;
}
.playBar_t {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: #1c192f;
display: flex;
align-items: flex-start;
padding: 0px 50px;
}
.playBar_t__seek {
width: 100%;
height: 10px;
/* border-radius: 12px; */
margin-right: 0px;
}

View File

@@ -0,0 +1,28 @@
import { IonToast } from '@ionic/react';
import './style.css';
import { CORRECT_ANSWER_MESSAGE } from '../../constants';
import { useAppStateContext } from '../../contexts/AppState';
interface QuestionCardProps {
isOpen: boolean;
dismiss: () => void;
}
const CorrectAnswerToast: React.FC<QuestionCardProps> = ({ isOpen, dismiss }) => {
const { CORRECT_ANS_TOAST_APPEAR_TIMEOUT_S } = useAppStateContext();
return (
<>
<IonToast
className="correct-answer-toast"
isOpen={isOpen}
message={CORRECT_ANSWER_MESSAGE}
onDidDismiss={() => dismiss()}
duration={CORRECT_ANS_TOAST_APPEAR_TIMEOUT_S * 1000}
color="success"
></IonToast>
</>
);
};
export default CorrectAnswerToast;

View File

@@ -0,0 +1,9 @@
ion-toast.correct-answer-toast::part(message) {
text-align: center;
font-size: 1.5rem;
color: rgba(0, 0, 0, 0.9);
}
ion-toast.correct-answer-toast::part(container) {
bottom: 100px;
}

View File

@@ -0,0 +1,12 @@
import { IonBackdrop } from '@ionic/react';
import { useAppStateContext } from '../../contexts/AppState';
export function DisableUserTap({ children }: { children: React.ReactNode }) {
const { disable_user_tap } = useAppStateContext();
return (
<>
{disable_user_tap ? <IonBackdrop visible={true}></IonBackdrop> : null}
{children}
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,21 @@
import { App } from '@capacitor/app';
import { IonButton, IonIcon } from '@ionic/react';
import { FunctionComponent } from 'react';
import './style.css';
import { exitOutline } from 'ionicons/icons';
const ExitButton: FunctionComponent = () => {
const handleExitOnClick = () => {
App.exitApp();
};
return (
<>
<IonButton color="clear" fill="clear" shape="round" onClick={handleExitOnClick}>
<IonIcon slot={'icon-only'} color={'dark'} size={'large'} icon={exitOutline} />
</IonButton>
</>
);
};
export default ExitButton;

View File

@@ -0,0 +1,29 @@
.audioSeekBar {
cursor: pointer;
background-color: white;
overflow: hidden;
}
.audioSeekBar__tick {
background-color: rgb(56, 182, 255);
height: 100%;
}
.playBar_t {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: #1c192f;
display: flex;
align-items: flex-start;
padding: 0px 50px;
}
.playBar_t__seek {
width: 100%;
height: 10px;
/* border-radius: 12px; */
margin-right: 0px;
}

View File

@@ -0,0 +1,24 @@
.container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.container strong {
font-size: 20px;
line-height: 26px;
}
.container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
.container a {
text-decoration: none;
}

View File

@@ -0,0 +1,43 @@
import './ExploreContainer.css';
import React from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
interface ContainerProps {
name: string;
}
const markdown = `
Just a link: www.nasa.gov.
- [ ] helloworld
__bold__
# h1
## h2
### h3
#### h4
##### h5
###### h6
| test | test | test |
| --- | --- | --- |
| 1 | 1 | 1 |
`;
const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
return (
<div className="container">
<strong>{name}</strong>
<p>
Explore{' '}
<a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">
UI Components
</a>
</p>
<div>
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
</div>
</div>
);
};
export default ExploreContainer;

View File

@@ -0,0 +1,109 @@
import { FunctionComponent } from 'react';
import './style.css';
interface AudioSeekBarProps {
value: number;
range_list: number[];
}
const GeniusProgressBar: FunctionComponent<AudioSeekBarProps> = ({ value, range_list }) => {
let test_value = value;
let scale = [];
for (let i = 0; i < range_list.length; i++) {
let scale_full_range = range_list[i];
let scale_range = i > 0 ? scale_full_range - range_list[i - 1] : scale_full_range;
let fill = 0;
if (test_value > scale_full_range) {
fill = scale_full_range;
} else {
if (i > 0) {
fill = Math.max(0, test_value - range_list[i - 1]);
} else {
fill = Math.max(0, test_value);
}
}
let fill_pct = Math.min(fill / scale_range, 1);
scale.push([fill_pct]);
}
return (
<>
<div style={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
<div style={{ width: '100%', display: 'flex', flexDirection: 'row' }}>
{scale.map((item: any, index: number) => {
return (
<>
<div
style={{
width: `calc( 100% / ${range_list.length} )`,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
}}
>
<div style={{ fontSize: '0.5rem' }}>{range_list[index]}</div>
<div
className={`audioSeekBar playBar_t__seek`}
style={{
// border: '1px solid #000',
borderTop: '1px solid #000',
borderBottom: '1px solid #000',
borderLeft: index == 0 ? '1px solid #000' : '',
borderRight: index == scale.length - 1 ? '1px solid #000' : '',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<div style={{ width: `${item * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
</>
);
})}
</div>
</div>
</>
);
return (
<>
<div style={{ backgroundColor: 'gold' }}>
<div>helloworld</div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{scale.map((item, index) => {
return (
<div
style={{
width: `calc( 100% / ${range_list.length} )`,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
}}
>
<div style={{ fontSize: '0.5rem' }}>{range_list[index]}</div>
<div
className={`audioSeekBar playBar_t__seek`}
style={{ borderTop: '1px solid #000', borderBottom: '1px solid #000' }}
>
<div
style={{
width: `${item * 100}%`,
borderLeft: index == 0 ? '1px solid #000' : '',
borderRight: index == 6 ? '1px solid #000' : '',
}}
className="audioSeekBar__tick"
/>
</div>
</div>
);
})}
</div>
</div>
</>
);
};
export default GeniusProgressBar;

View File

@@ -0,0 +1,29 @@
.audioSeekBar {
cursor: pointer;
background-color: white;
overflow: hidden;
}
.audioSeekBar__tick {
background-color: rgb(56, 182, 255);
height: 100%;
}
.playBar_t {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: #1c192f;
display: flex;
align-items: flex-start;
padding: 0px 50px;
}
.playBar_t__seek {
width: 100%;
height: 10px;
/* border-radius: 12px; */
margin-right: 0px;
}

View File

@@ -0,0 +1,72 @@
import { FunctionComponent } from 'react';
import './style.css';
interface AudioSeekBarProps {
value: number;
range_list: number[];
}
const HardWorkerProgressBar: FunctionComponent<AudioSeekBarProps> = ({ value, range_list }) => {
let test_value = value;
let scale = [];
for (let i = 0; i < range_list.length; i++) {
let scale_full_range = range_list[i];
let scale_range = i > 0 ? scale_full_range - range_list[i - 1] : scale_full_range;
let fill = 0;
if (test_value > scale_full_range) {
fill = scale_full_range;
} else {
if (i > 0) {
fill = Math.max(0, test_value - range_list[i - 1]);
} else {
fill = test_value;
}
}
let fill_pct = Math.min(fill / scale_range, 1);
scale.push([fill_pct]);
}
return (
<>
<div style={{ display: 'flex', flexDirection: 'row', width: '100%' }}>
<div style={{ width: '100%', display: 'flex', flexDirection: 'row' }}>
{scale.map((item: any, index: number) => {
return (
<>
<div
style={{
width: `calc( 100% / ${range_list.length} )`,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
}}
>
<div style={{ fontSize: '0.5rem' }}>{range_list[index]}</div>
<div
className={`audioSeekBar playBar_t__seek`}
style={{
// border: '1px solid #000',
borderTop: '1px solid #000',
borderBottom: '1px solid #000',
borderLeft: index == 0 ? '1px solid #000' : '',
borderRight: index == scale.length - 1 ? '1px solid #000' : '',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<div style={{ width: `${item * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
</>
);
})}
</div>
</div>
</>
);
};
export default HardWorkerProgressBar;

View File

@@ -0,0 +1,29 @@
.audioSeekBar {
cursor: pointer;
background-color: white;
overflow: hidden;
}
.audioSeekBar__tick {
background-color: rgb(56, 182, 255);
height: 100%;
}
.playBar_t {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: #1c192f;
display: flex;
align-items: flex-start;
padding: 0px 50px;
}
.playBar_t__seek {
width: 100%;
height: 10px;
/* border-radius: 12px; */
margin-right: 0px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@@ -0,0 +1,9 @@
import ImageJpg from './image.jpg';
function HelloworldImage() {
return (
<div style={{ width: '100%', height: '100%', backgroundImage: `url(${ImageJpg})`, backgroundSize: 'cover' }}></div>
);
}
export { HelloworldImage };

View File

@@ -0,0 +1,26 @@
import { IonContent, IonPage, IonSpinner } from '@ionic/react';
import { useTranslation } from 'react-i18next';
export function LoadingScreen() {
const { t, i18n } = useTranslation();
return (
<IonPage>
<IonContent fullscreen>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '1rem',
//
marginTop: '33vh',
}}
>
<IonSpinner></IonSpinner>
<div>{t('Loading')}</div>
</div>
</IonContent>
</IonPage>
);
}

View File

@@ -0,0 +1,34 @@
import { IonIcon } from '@ionic/react';
import './style.css';
import { star, starOutline } from 'ionicons/icons';
interface ContainerProps {
num_rating: number;
max_rating?: number;
}
const MarkRating: React.FC<ContainerProps> = ({ num_rating, max_rating = 3 }) => {
return (
<>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '0.5rem' }}>
{Array.from({ length: max_rating }, (_, index) => {
if (index < num_rating) {
return (
<div style={{ width: '10vw', height: '10vw' }}>
<IonIcon size="large" color="warning" icon={star}></IonIcon>
</div>
);
} else {
return (
<div style={{ width: '10vw', height: '10vw' }}>
<IonIcon size="large" color="warning" style={{ opacity: '0.3' }} icon={starOutline}></IonIcon>
</div>
);
}
})}
</div>
</>
);
};
export default MarkRating;

View File

@@ -0,0 +1,68 @@
import { FunctionComponent } from 'react';
import './style.css';
interface AudioSeekBarProps {
value: number;
range_list: number[];
}
const MatchmakingProgressBar: FunctionComponent<AudioSeekBarProps> = ({ value, range_list }) => {
let test_value = value;
let scale = [];
for (let i = 0; i < range_list.length; i++) {
let scale_full_range = range_list[i];
let scale_range = i > 0 ? scale_full_range - range_list[i - 1] : scale_full_range;
let fill = 0;
if (test_value > scale_full_range) {
fill = scale_full_range;
} else {
if (i > 0) {
fill = Math.max(0, test_value - range_list[i - 1]);
} else {
fill = test_value;
}
}
let fill_pct = Math.min(fill / scale_range, 1);
scale.push([fill_pct]);
}
return (
<>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{scale.map((item: any, index: number) => {
return (
<div
style={{
width: `calc( 100% / ${range_list.length} )`,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
}}
>
<div style={{ fontSize: '0.5rem' }}>{range_list[index]}</div>
<div
className={`audioSeekBar playBar_t__seek`}
style={{
// border: '1px solid #000',
borderTop: '1px solid #000',
borderBottom: '1px solid #000',
borderLeft: index == 0 ? '1px solid #000' : '',
borderRight: index == scale.length - 1 ? '1px solid #000' : '',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<div style={{ width: `${item * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
);
})}
</div>
</>
);
};
export default MatchmakingProgressBar;

View File

@@ -0,0 +1,29 @@
.audioSeekBar {
cursor: pointer;
background-color: white;
overflow: hidden;
}
.audioSeekBar__tick {
background-color: rgb(56, 182, 255);
height: 100%;
}
.playBar_t {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: #1c192f;
display: flex;
align-items: flex-start;
padding: 0px 50px;
}
.playBar_t__seek {
width: 100%;
height: 10px;
/* border-radius: 12px; */
margin-right: 0px;
}

View File

@@ -0,0 +1,124 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useEffect, useRef, useState } from 'react';
import { COLOR_TEXT, CONNECTIVE_CONQUEROR_STAGES, DEBUG } from '../../../../constants';
import './style.css';
import { useConnectivesRevisionCorrectCount } from '../../../../contexts/MyIonMetric/ConnectivesRevisionCorrectCount';
function CongratConnectiveConqueror({}: {}) {
const modal = useRef<HTMLIonModalElement>(null);
const [opened, setOpened] = useState(false);
// const message = '';
const [message, setMessage] = useState('');
const [openCongratListeningProgress, setOpenCongratListeningProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [count_to_prompt, setCountToPrompt] = useState(0);
const {
myIonMetricGetConnectivesRevisionCorrectCount,
myIonMetricGetConnectivesRevisionCorrectCountIgnore,
myIonMetricSetConnectivesRevisionCorrectCountIgnore,
} = useConnectivesRevisionCorrectCount();
const dismiss_modal = () => {
myIonMetricSetConnectivesRevisionCorrectCountIgnore(count_to_prompt);
setOpened(false);
};
async function promptProgress(count_to_prompt: number) {
if (count_to_prompt == (await myIonMetricGetConnectivesRevisionCorrectCountIgnore()).count) return;
// setCountPrompted(count_to_prompt);
setMessage('You scored full marks ' + count_to_prompt + ' times.');
// setOpenedCongraFullmarkTimes(true);
setOpened(true);
}
useEffect(() => {
const prompt_list = CONNECTIVE_CONQUEROR_STAGES;
const b_prompt_list = Array(prompt_list.length).fill(false);
for (let i = 0; i < prompt_list.length; i++) {
if (progress > 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 {
setCountToPrompt(prompt_list[lastTrueIndex]);
promptProgress(prompt_list[lastTrueIndex]);
}
}, [progress]);
useEffect(() => {
(async () => {
let temp = await myIonMetricGetConnectivesRevisionCorrectCount();
setProgress(temp.count);
})();
}, []);
return (
<>
<IonModal
id="congratulation-listening-progress-modal"
ref={modal}
trigger="open-custom-dialog"
onIonModalWillDismiss={() => dismiss_modal()}
// 008_remove_achievement_announcement, hide congratulation modal
isOpen={false}
>
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
{DEBUG ? <span className={'debug'}>congratulation connectives conqueror</span> : <></>}
<div>
<h1>Congratulation!</h1>
</div>
<div>{'Genius'}</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div>{message}</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{ position: 'absolute', top: '5px', right: '5px' }}
onClick={() => dismiss_modal()}
>
<IonIcon slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonModal>
</>
);
}
export default CongratConnectiveConqueror;

View File

@@ -0,0 +1,21 @@
.bold {
font-weight: bold;
}
ion-modal#congratulation-listening-progress-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#congratulation-listening-progress-modal::part(backdrop) {
/* background: rgba(209, 213, 219); */
opacity: 1;
}
ion-modal#congratulation-listening-progress-modal ion-toolbar {
/* --background: rgb(14 116 144); */
/* --color: white; */
--color: black;
}

View File

@@ -0,0 +1,122 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useEffect, useRef, useState } from 'react';
import { COLOR_TEXT, DEBUG, GENIUS_STAGES } from '../../../../constants';
import './style.css';
import { useAppStateContext } from '../../../../contexts/AppState';
import { useFullmarkCount } from '../../../../contexts/MyIonMetric/FullmarkCount';
function CongratGenius({}: {}) {
const modal = useRef<HTMLIonModalElement>(null);
const [opened, setOpened] = useState(false);
// const message = '';
const [message, setMessage] = useState('');
const [progress, setProgress] = useState(0);
const [count_to_prompt, setCountToPrompt] = useState(0);
const { myIonMetricGetFullMarkCount, myIonMetricGetFullMarkIgnore, myIonMetricSetFullMarkIgnore } =
useFullmarkCount();
const dismiss_modal = () => {
myIonMetricSetFullMarkIgnore(count_to_prompt);
setOpened(false);
};
async function promptGeniusProgress(count_to_prompt: number) {
if (count_to_prompt == (await myIonMetricGetFullMarkIgnore()).count) return;
// setCountPrompted(count_to_prompt);
setMessage('You scored full marks ' + count_to_prompt + ' times.');
// setOpenedCongraFullmarkTimes(true);
setOpened(true);
}
useEffect(() => {
const prompt_list = GENIUS_STAGES;
const b_prompt_list = Array(prompt_list.length).fill(false);
for (let i = 0; i < prompt_list.length; i++) {
if (progress >= 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 {
console.log({ progress });
setCountToPrompt(prompt_list[lastTrueIndex]);
promptGeniusProgress(prompt_list[lastTrueIndex]);
}
}, [progress]);
let { tab_active } = useAppStateContext();
useEffect(() => {
(async () => {
let temp = await myIonMetricGetFullMarkCount();
setProgress(temp.count);
})();
}, []);
return (
<>
<IonModal
id="congratulation-listening-progress-modal"
ref={modal}
trigger="open-custom-dialog"
onIonModalWillDismiss={() => dismiss_modal()}
// 008_remove_achievement_announcement, hide congratulation modal
isOpen={false}
>
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
{DEBUG ? <span className={'debug'}>congratulation genius</span> : <></>}
<div>
<h1>Congratulation!</h1>
</div>
<div>{'Genius'}</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div>{message}</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{ position: 'absolute', top: '5px', right: '5px' }}
onClick={() => dismiss_modal()}
>
<IonIcon slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonModal>
</>
);
}
export default CongratGenius;

View File

@@ -0,0 +1,21 @@
.bold {
font-weight: bold;
}
ion-modal#congratulation-listening-progress-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#congratulation-listening-progress-modal::part(backdrop) {
/* background: rgba(209, 213, 219); */
opacity: 1;
}
ion-modal#congratulation-listening-progress-modal ion-toolbar {
/* --background: rgb(14 116 144); */
/* --color: white; */
--color: black;
}

View File

@@ -0,0 +1,125 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useEffect, useRef, useState } from 'react';
import { COLOR_TEXT, DEBUG, HARDWORKER_STAGES } from '../../../../constants';
import './style.css';
import { useAppUseTime } from '../../../../contexts/MyIonMetric/AppUseTime';
function CongratHardworker({}: {}) {
const modal = useRef<HTMLIonModalElement>(null);
const [opened, setOpened] = useState(false);
// const message = '';
const [message, setMessage] = useState('');
const [progress, setProgress] = useState(0);
const [count_to_prompt, setCountToPrompt] = useState(0);
const {
//
myIonMetricGetAppUseTime,
myIonMetricSetAppUseTimeIgnore,
myIonMetricGetAppUseTimeIgnore,
} = useAppUseTime();
const dismiss_modal = () => {
myIonMetricSetAppUseTimeIgnore(count_to_prompt);
setOpened(false);
};
async function promptHardworkerProgress(count_to_prompt: number) {
if (count_to_prompt == (await myIonMetricGetAppUseTimeIgnore()).time_spent_s) return;
// setCountPrompted(count_to_prompt);
setMessage('You scored full marks ' + count_to_prompt + ' times.');
// setOpenedCongraFullmarkTimes(true);
setOpened(true);
}
useEffect(() => {
const prompt_list = HARDWORKER_STAGES;
const b_prompt_list = Array(prompt_list.length).fill(false);
for (let i = 0; i < prompt_list.length; i++) {
if (progress >= 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 {
console.log({ progress });
setCountToPrompt(prompt_list[lastTrueIndex]);
promptHardworkerProgress(prompt_list[lastTrueIndex]);
}
}, [progress]);
useEffect(() => {
(async () => {
let temp = await myIonMetricGetAppUseTime();
setProgress(Math.floor(temp.time_spent_s / 60));
})();
}, []);
return (
<>
<IonModal
id="congratulation-listening-progress-modal"
ref={modal}
trigger="open-custom-dialog"
onIonModalWillDismiss={() => dismiss_modal()}
// 008_remove_achievement_announcement, hide congratulation modal
isOpen={false}
>
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
{DEBUG ? <span className={'debug'}>congratulation hardworker</span> : <></>}
<div>
<h1>Congratulation!</h1>
</div>
<div>{'Genius'}</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div>{message}</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{ position: 'absolute', top: '5px', right: '5px' }}
onClick={() => dismiss_modal()}
>
<IonIcon slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonModal>
</>
);
}
export default CongratHardworker;

View File

@@ -0,0 +1,21 @@
.bold {
font-weight: bold;
}
ion-modal#congratulation-listening-progress-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#congratulation-listening-progress-modal::part(backdrop) {
/* background: rgba(209, 213, 219); */
opacity: 1;
}
ion-modal#congratulation-listening-progress-modal ion-toolbar {
/* --background: rgb(14 116 144); */
/* --color: white; */
--color: black;
}

View File

@@ -0,0 +1,78 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useEffect, useRef, useState } from 'react';
import { COLOR_TEXT } from '../../../../constants';
import './style.css';
function CongratHelloworld({}: {}) {
const modal = useRef<HTMLIonModalElement>(null);
const [opened, setOpened] = useState(false);
// const message = '';
const [message, setMessage] = useState('');
const [openCongratListeningProgress, setOpenCongratListeningProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [count_to_prompt, setCountToPrompt] = useState(0);
const dismiss_modal = () => {
setOpened(false);
};
useEffect(() => {}, [progress]);
useEffect(() => {
(async () => {})();
}, []);
return (
<>
<IonModal
id="congratulation-listening-progress-modal"
ref={modal}
trigger="open-custom-dialog"
onIonModalWillDismiss={() => dismiss_modal()}
isOpen={opened}
>
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
<div>
<h1>Congratulation!</h1>
</div>
<div>{'Genius'}</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div>{message}</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{ position: 'absolute', top: '5px', right: '5px' }}
onClick={() => dismiss_modal()}
>
<IonIcon slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonModal>
</>
);
}
export default CongratHelloworld;

View File

@@ -0,0 +1,21 @@
.bold {
font-weight: bold;
}
ion-modal#congratulation-listening-progress-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#congratulation-listening-progress-modal::part(backdrop) {
/* background: rgba(209, 213, 219); */
opacity: 1;
}
ion-modal#congratulation-listening-progress-modal ion-toolbar {
/* --background: rgb(14 116 144); */
/* --color: white; */
--color: black;
}

View File

@@ -0,0 +1,124 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useEffect, useRef, useState } from 'react';
import { ATTENTIVE_EARS_STAGES, COLOR_TEXT, DEBUG } from '../../../../constants';
import { useListeningPracticeTimeSpent } from '../../../../contexts/MyIonMetric/ListeningPracticeTimeSpent';
import './style.css';
function CongratListeningProgress({}: {}) {
const modal = useRef<HTMLIonModalElement>(null);
const [opened, setOpened] = useState(false);
// const message = '';
const [message, setMessage] = useState('');
const [openCongratListeningProgress, setOpenCongratListeningProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [count_to_prompt, setCountToPrompt] = useState(0);
const {
myIonMetricGetListeningPracticeTimeSpent,
myIonMetricGetListeningPracticeProgressIgnore,
myIonMetricSetListeningPracticeProgressIgnore,
} = useListeningPracticeTimeSpent();
const dismiss_modal = () => {
myIonMetricSetListeningPracticeProgressIgnore(count_to_prompt);
setOpened(false);
};
async function promptListeningPracticeProgress(count_to_prompt: number) {
if (count_to_prompt == (await myIonMetricGetListeningPracticeProgressIgnore()).time_spent_s) return;
// setCountPrompted(count_to_prompt);
setMessage('You scored full marks ' + count_to_prompt + ' times.');
// setOpenedCongraFullmarkTimes(true);
setOpened(true);
}
useEffect(() => {
const prompt_list = ATTENTIVE_EARS_STAGES;
const b_prompt_list = Array(prompt_list.length).fill(false);
for (let i = 0; i < prompt_list.length; i++) {
if (progress > 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 {
setCountToPrompt(prompt_list[lastTrueIndex]);
promptListeningPracticeProgress(prompt_list[lastTrueIndex]);
}
}, [progress]);
useEffect(() => {
(async () => {
let temp = await myIonMetricGetListeningPracticeTimeSpent();
setProgress(temp.time_spent_s / 60);
})();
}, []);
return (
<>
<IonModal
id="congratulation-listening-progress-modal"
ref={modal}
trigger="open-custom-dialog"
onIonModalWillDismiss={() => dismiss_modal()}
// 008_remove_achievement_announcement, hide congratulation modal
isOpen={false}
>
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
{DEBUG ? <span className={'debug'}>congratulation listening progress</span> : <></>}
<div>
<h1>Congratulation!</h1>
</div>
<div>{'Genius'}</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div>{message}</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{ position: 'absolute', top: '5px', right: '5px' }}
onClick={() => dismiss_modal()}
>
<IonIcon slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonModal>
</>
);
}
export default CongratListeningProgress;

View File

@@ -0,0 +1,21 @@
.bold {
font-weight: bold;
}
ion-modal#congratulation-listening-progress-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#congratulation-listening-progress-modal::part(backdrop) {
/* background: rgba(209, 213, 219); */
opacity: 1;
}
ion-modal#congratulation-listening-progress-modal ion-toolbar {
/* --background: rgb(14 116 144); */
/* --color: white; */
--color: black;
}

View File

@@ -0,0 +1,124 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useEffect, useRef, useState } from 'react';
import { COLOR_TEXT, DEBUG, MATCHMAKING_STAGES } from '../../../../constants';
import './style.css';
import { useMatchingFrenzyCorrectCount } from '../../../../contexts/MyIonMetric/MatchingFrenzyCorrectCount';
function CongratMatchmaking({}: {}) {
const modal = useRef<HTMLIonModalElement>(null);
const [opened, setOpened] = useState(false);
// const message = '';
const [message, setMessage] = useState('');
const [openCongratListeningProgress, setOpenCongratListeningProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [count_to_prompt, setCountToPrompt] = useState(0);
const {
myIonMetricGetMatchingFrenzyCorrectCount,
myIonMetricGetMatchingFrenzyCorrectCountIgnore,
myIonMetricSetMatchingFrenzyCorrectCountIgnore,
} = useMatchingFrenzyCorrectCount();
const dismiss_modal = () => {
myIonMetricSetMatchingFrenzyCorrectCountIgnore(count_to_prompt);
setOpened(false);
};
async function promptProgress(count_to_prompt: number) {
if (count_to_prompt == (await myIonMetricGetMatchingFrenzyCorrectCountIgnore()).count) return;
// setCountPrompted(count_to_prompt);
setMessage('You scored full marks ' + count_to_prompt + ' times.');
// setOpenedCongraFullmarkTimes(true);
setOpened(true);
}
useEffect(() => {
const prompt_list = MATCHMAKING_STAGES;
const b_prompt_list = Array(prompt_list.length).fill(false);
for (let i = 0; i < prompt_list.length; i++) {
if (progress > 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 {
setCountToPrompt(prompt_list[lastTrueIndex]);
promptProgress(prompt_list[lastTrueIndex]);
}
}, [progress]);
useEffect(() => {
(async () => {
let temp = await myIonMetricGetMatchingFrenzyCorrectCount();
setProgress(temp.count);
})();
}, []);
return (
<>
<IonModal
id="congratulation-listening-progress-modal"
ref={modal}
trigger="open-custom-dialog"
onIonModalWillDismiss={() => dismiss_modal()}
// 008_remove_achievement_announcement, hide congratulation modal
isOpen={false}
>
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
{DEBUG ? <span className={'debug'}>congratulation matchmaking</span> : <></>}
<div>
<h1>Congratulation!</h1>
</div>
<div>{'Genius'}</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div>{message}</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{ position: 'absolute', top: '5px', right: '5px' }}
onClick={() => dismiss_modal()}
>
<IonIcon slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonModal>
</>
);
}
export default CongratMatchmaking;

View File

@@ -0,0 +1,21 @@
.bold {
font-weight: bold;
}
ion-modal#congratulation-listening-progress-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#congratulation-listening-progress-modal::part(backdrop) {
/* background: rgba(209, 213, 219); */
opacity: 1;
}
ion-modal#congratulation-listening-progress-modal ion-toolbar {
/* --background: rgb(14 116 144); */
/* --color: white; */
--color: black;
}

View File

@@ -0,0 +1,78 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useRef } from 'react';
import { COLOR_TEXT } from '../../../constants';
import { useFullmarkCount } from '../../../contexts/MyIonMetric/FullmarkCount';
function CongratulationAchievementModal({
opened,
setOpened,
count_prompted,
message = 'hello message',
}: {
message?: string;
opened: boolean;
count_prompted: number;
setOpened: (opened: boolean) => void;
}) {
const modal = useRef<HTMLIonModalElement>(null);
const { myIonMetricSetFullMarkIgnore } = useFullmarkCount();
const dismiss_modal = () => {
myIonMetricSetFullMarkIgnore(count_prompted);
setOpened(false);
};
return (
<>
<IonModal
id="example-modal"
ref={modal}
trigger="open-custom-dialog"
onIonModalWillDismiss={() => dismiss_modal()}
isOpen={opened}
>
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
<div>
<h1>Congratulation!</h1>
</div>
<div>{'Genius'}</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div>{message}</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{ position: 'absolute', top: '5px', right: '5px' }}
onClick={() => dismiss_modal()}
>
<IonIcon slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonModal>
</>
);
}
export default CongratulationAchievementModal;

View File

@@ -0,0 +1,58 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useRef, useState } from 'react';
import { COLOR_TEXT } from '../../../constants';
function HelloworldModal() {
const modal = useRef<HTMLIonModalElement>(null);
const [opened, setOpened] = useState(true);
return (
<>
<IonButton onClick={() => setOpened(true)}>Open</IonButton>
<IonModal isOpen={opened} id="example-modal" ref={modal} trigger="open-custom-dialog">
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
<div>
<h1>Congratulation!</h1>
</div>
<div>{'Genius'}</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div>You scored full</div>
<div>marks {'1000'} times!</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{ position: 'absolute', top: '5px', right: '5px' }}
onClick={() => setOpened(false)}
>
<IonIcon slot="icon-only" icon={closeOutline}></IonIcon>
</IonButton>
</IonModal>
</>
);
}
export default HelloworldModal;

View File

@@ -0,0 +1,87 @@
import { FunctionComponent } from 'react';
import './style.css';
interface AudioSeekBarProps {
value: number;
full_range: number;
}
const MyProgressBar: FunctionComponent<AudioSeekBarProps> = ({ value, full_range }) => {
let test_value = value;
let first_scale_full_range = 10;
let first_scale = test_value > first_scale_full_range ? first_scale_full_range : test_value;
//
let second_scale_full_range = 50;
let second_scale_range = second_scale_full_range - first_scale_full_range;
let second_scale =
test_value >= second_scale_full_range ? second_scale_full_range : Math.max(0, test_value - first_scale_full_range);
let third_scale_full_range = 100;
let third_scale_range = third_scale_full_range - second_scale_full_range;
let third_scale =
test_value >= third_scale_full_range ? third_scale_full_range : Math.max(0, test_value - second_scale_full_range);
let forth_scale_full_range = 300;
let forth_scale_range = forth_scale_full_range - third_scale_full_range;
let forth_scale =
test_value >= forth_scale_full_range ? forth_scale_full_range : Math.max(0, test_value - third_scale_full_range);
let fifth_scale_full_range = 700;
let fifth_scale_range = fifth_scale_full_range - forth_scale_full_range;
let fifth_scale =
test_value >= fifth_scale_full_range ? fifth_scale_full_range : Math.max(0, test_value - forth_scale_full_range);
let sixth_scale_full_range = 1000;
let sixth_scale_range = sixth_scale_full_range - fifth_scale_full_range;
let sixth_scale =
test_value >= sixth_scale_full_range ? sixth_scale_full_range : Math.max(0, test_value - fifth_scale_full_range);
return (
<>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<div style={{ width: 'calc( 100% / 6 )', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{ fontSize: '0.5rem' }}>{first_scale_full_range}</div>
<div className={`audioSeekBar playBar_t__seek`}>
<div style={{ width: `${(first_scale / first_scale_full_range) * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
<div style={{ width: 'calc( 100% / 6 )', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{ fontSize: '0.5rem' }}>{second_scale_full_range}</div>
<div className={`audioSeekBar playBar_t__seek`}>
<div style={{ width: `${(second_scale / second_scale_range) * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
<div style={{ width: 'calc( 100% / 6 )', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{ fontSize: '0.5rem' }}>{third_scale_full_range}</div>
<div className={`audioSeekBar playBar_t__seek`}>
<div style={{ width: `${(third_scale / third_scale_range) * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
<div style={{ width: 'calc( 100% / 6 )', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{ fontSize: '0.5rem' }}>{forth_scale_full_range}</div>
<div className={`audioSeekBar playBar_t__seek`}>
<div style={{ width: `${(forth_scale / forth_scale_range) * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
<div style={{ width: 'calc( 100% / 6 )', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{ fontSize: '0.5rem' }}>{fifth_scale_full_range}</div>
<div className={`audioSeekBar playBar_t__seek`}>
<div style={{ width: `${(fifth_scale / fifth_scale_range) * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
<div style={{ width: 'calc( 100% / 6 )', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
<div style={{ fontSize: '0.5rem' }}>{sixth_scale_full_range}</div>
<div className={`audioSeekBar playBar_t__seek`}>
<div style={{ width: `${(sixth_scale / sixth_scale_range) * 100}%` }} className="audioSeekBar__tick" />
</div>
</div>
</div>
</>
);
};
export default MyProgressBar;

View File

@@ -0,0 +1,29 @@
.audioSeekBar {
cursor: pointer;
background-color: white;
overflow: hidden;
}
.audioSeekBar__tick {
background-color: rgb(56, 182, 255);
height: 100%;
}
.playBar_t {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: #1c192f;
display: flex;
align-items: flex-start;
padding: 0px 50px;
}
.playBar_t__seek {
width: 100%;
height: 10px;
/* border-radius: 12px; */
margin-right: 0px;
}

View File

@@ -0,0 +1,19 @@
import { FunctionComponent } from 'react';
import './style.css';
interface AudioSeekBarProps {
value: number;
full_range: number;
}
const MyProgressBar: FunctionComponent<AudioSeekBarProps> = ({ value, full_range }) => {
return (
<>
<div className={`audioSeekBar playBar_t__seek`}>
<div style={{ width: `${(value / full_range) * 100}%` }} className="audioSeekBar__tick" />
</div>
</>
);
};
export default MyProgressBar;

View File

@@ -0,0 +1,29 @@
.audioSeekBar {
cursor: pointer;
background-color: white;
overflow: hidden;
}
.audioSeekBar__tick {
background-color: #999900;
height: 100%;
}
.playBar_t {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background-color: #1c192f;
display: flex;
align-items: flex-start;
padding: 25px 50px;
}
.playBar_t__seek {
width: 100%;
height: 10px;
border-radius: 12px;
margin-right: 10px;
}

View File

@@ -0,0 +1,140 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="649.3779"
height="727.77798"
viewBox="0 0 649.3779 727.77798"
xmlns:xlink="http://www.w3.org/1999/xlink"
role="img"
artist="Katerina Limpitsouni"
source="https://undraw.co/"
><path
d="M648.20033,259.05824h-3.99878V149.51291A63.40187,63.40187,0,0,0,580.79976,86.111H348.713a63.40184,63.40184,0,0,0-63.402,63.4017V750.48713A63.40181,63.40181,0,0,0,348.71284,813.889H580.79945a63.40185,63.40185,0,0,0,63.402-63.40167V337.0345h3.99884Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
d="M583.3578,102.606h-30.295a22.49485,22.49485,0,0,1-20.82715,30.99053H399.2762a22.49484,22.49484,0,0,1-20.82715-30.99061H350.15346a47.34781,47.34781,0,0,0-47.34784,47.34774V750.04628a47.34781,47.34781,0,0,0,47.34777,47.34784H583.3578a47.34781,47.34781,0,0,0,47.34784-47.34778h0V149.95371A47.34777,47.34777,0,0,0,583.3578,102.606Z"
transform="translate(-285.31105 -86.11101)"
fill="#fff"
/><path
id="f3818c68-126c-4685-b4e0-2450731ccc2a-761"
data-name="a2804879-ded6-4045-b20f-1f1dde9b938b"
d="M611.46248,573.92982h-279.619a5.184,5.184,0,0,1-5.178-5.178v-69.361a5.184,5.184,0,0,1,5.178-5.178h279.619a5.184,5.184,0,0,1,5.178,5.178v69.362A5.184,5.184,0,0,1,611.46248,573.92982Zm-279.619-77.646a3.11,3.11,0,0,0-3.107,3.107v69.362a3.11,3.11,0,0,0,3.107,3.107h279.619a3.11,3.11,0,0,0,3.107-3.107v-69.362a3.11,3.11,0,0,0-3.107-3.107Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><circle
id="abdb74b7-e321-430b-89c2-b563f66442fc"
data-name="b9ad11c9-d8a0-4df6-8741-900b9ec46a35"
cx="82.77841"
cy="447.95985"
r="21.74799"
fill="#e6e6e6"
/><path
id="addd02b1-b85b-481a-baea-1b0ba5ed9f4a-762"
data-name="bd261eec-7ae0-49b0-bf26-57ff03972605"
d="M418.31746,519.57286a3.625,3.625,0,0,0,0,7.249h170.878a3.625,3.625,0,0,0,.13989-7.24866l-.02087-.00033q-.05943-.001-.119,0Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
id="a42dc2a4-5fb2-4ea8-b2b3-ce81bc256782-763"
data-name="e80b4447-8c34-435b-ba6c-5300a190ab24"
d="M418.31746,541.32087a3.625,3.625,0,0,0,0,7.249h170.878a3.625,3.625,0,0,0,.119-7.249q-.05943-.00092-.119,0Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
id="b84687c4-e3b4-4975-8361-bf73c33c9ee5-764"
data-name="e55fcb7d-3a3b-45d8-b167-72fb2263dd92"
d="M611.46248,689.91981h-279.619a5.184,5.184,0,0,1-5.178-5.178v-69.361a5.184,5.184,0,0,1,5.178-5.178h279.619a5.184,5.184,0,0,1,5.178,5.178v69.362A5.184,5.184,0,0,1,611.46248,689.91981Zm-279.619-77.646a3.11,3.11,0,0,0-3.107,3.107v69.362a3.11,3.11,0,0,0,3.107,3.107h279.619a3.11,3.11,0,0,0,3.107-3.107v-69.362a3.11,3.11,0,0,0-3.107-3.107Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><circle
id="fe3713e7-4e14-41f8-af1d-48b338e5371c"
data-name="a50d232f-7710-43e4-8fa9-6ef0443fc454"
cx="82.77841"
cy="563.94987"
r="21.74799"
fill="#e6e6e6"
/><path
id="e34cf46a-1d6c-4c41-b5e1-331fa5bf8d4c-765"
data-name="e49b4965-a9e9-4371-9134-194e44e65c31"
d="M418.31746,635.56288a3.625,3.625,0,0,0,0,7.249h170.878a3.625,3.625,0,0,0,.119-7.249q-.05943-.001-.119,0Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
id="e105039f-b7a6-49c8-8f81-87505f1b0ae5-766"
data-name="abc4586a-ac92-4255-aae6-84f53baad571"
d="M418.31746,657.31086a3.625,3.625,0,0,0,0,7.249h170.878a3.625,3.625,0,0,0,.119-7.249q-.05943-.00092-.119,0Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
id="ad9187ec-89e0-4b9d-a4fb-dc654c09bafe-767"
data-name="a9e593af-a319-4e97-9065-f0c2c04624d5"
d="M465.76845,429.99485a98.343,98.343,0,0,1-98.384-98.30194v-.08206c0-.206,0-.423.012-.629.3-53.879,44.432-97.756,98.372-97.756a98.384,98.384,0,0,1,.0224,196.768h-.0224Zm0-194.7a96.519,96.519,0,0,0-96.3,95.749c-.011.22-.011.4-.011.564a96.325,96.325,0,1,0,96.337-96.313h-.026Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><circle cx="315.11393" cy="422.84174" r="40" fill="#fff" /><path
d="M586.53265,526.94884c-.06861,0-.13721-.00049-.20606-.00195h-21.5a10.39761,10.39761,0,0,1-.0083-20.79248h21.51807c.10547-.00195.22021-.00195.334,0a10.39771,10.39771,0,0,1-.13769,20.79443Z"
transform="translate(-285.31105 -86.11101)"
fill="#6c63ff"
/><rect x="325.3453" y="725.34374" width="324.03261" height="2.24072" fill="#3f3d56" /><path
d="M630.6207,621.842a6.56111,6.56111,0,0,1-.62973-1.259l-5.16339-13.82657a6.50779,6.50779,0,0,1,3.81532-8.36324l115.24228-43.03187a6.50621,6.50621,0,0,1,8.363,3.81491l5.16283,13.82751a6.49951,6.49951,0,0,1-3.81558,8.36283l-115.2412,43.03174a6.5067,6.5067,0,0,1-7.73351-2.55634Z"
transform="translate(-285.31105 -86.11101)"
fill="#6c63ff"
/><path
d="M743.51834,551.78826l-46.86323,18.68723a6,6,0,0,0-4.093,7.1996l5.43852,21.77515a6,6,0,0,0,8.37642,3.97481l46.99241-22.1188a6.0109,6.0109,0,0,0,3.52446-7.717l-5.64883-18.27558A6.01072,6.01072,0,0,0,743.51834,551.78826Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M829.28044,687.45a6.50671,6.50671,0,0,1-6.89226-4.34039l-40.114-116.289a6.49951,6.49951,0,0,1,4.02482-8.26416l13.95295-4.81352a6.50408,6.50408,0,0,1,8.26373,4.02536l40.11465,116.2899a6.50779,6.50779,0,0,1-4.02531,8.26422l-13.95257,4.8125A6.56115,6.56115,0,0,1,829.28044,687.45Z"
transform="translate(-285.31105 -86.11101)"
fill="#6c63ff"
/><path
d="M811.5058,555.77814l15.39577,48.04524a6,6,0,0,1-2.95088,7.73816l-20.26024,9.657a6,6,0,0,1-8.39566-3.934l-12.83035-50.328a6.0109,6.0109,0,0,1,3.71178-7.62871l17.694-7.26835A6.01071,6.01071,0,0,1,811.5058,555.77814Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M764.89068,812.11413H750.13141a6.50753,6.50753,0,0,1-6.5-6.5V682.59948a6.50753,6.50753,0,0,1,6.5-6.5h14.75927a6.50753,6.50753,0,0,1,6.5,6.5V805.61413A6.50753,6.50753,0,0,1,764.89068,812.11413Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M794.08307,812.11413H779.3233a6.50753,6.50753,0,0,1-6.5-6.5V682.59948a6.50753,6.50753,0,0,1,6.5-6.5h14.75977a6.50753,6.50753,0,0,1,6.5,6.5V805.61413A6.50753,6.50753,0,0,1,794.08307,812.11413Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M806.2467,697.06158H739.1842a7.00787,7.00787,0,0,1-7-7V581.11725a40.53125,40.53125,0,0,1,81.0625,0V690.06158A7.00786,7.00786,0,0,1,806.2467,697.06158Z"
transform="translate(-285.31105 -86.11101)"
fill="#ccc"
/><path
d="M816.34118,723.711h-34V531.55621l.6316.17236a45.38181,45.38181,0,0,1,33.3684,43.6875Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M760.32629,723.711h-34V575.41607a45.38226,45.38226,0,0,1,33.36841-43.6875l.63159-.17236Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><circle cx="486.89704" cy="378.42061" r="53.51916" fill="#6c63ff" /><path
d="M832.71545,452.21s3-83-36-56c0,0-22.5-75.5-77.5,37.5l-16,16s72-10,139,21Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M773.02436,491.03636c-3.30591-.09179-7.42029-.20654-10.59-2.522a8.13272,8.13272,0,0,1-3.20008-6.07275,5.47091,5.47091,0,0,1,1.86035-4.49317c1.65552-1.39892,4.073-1.727,6.67823-.96142l-2.69922-19.72559,1.98144-.27148,3.17322,23.18994-1.65466-.75928c-1.91834-.87988-4.55164-1.32764-6.188.05518a3.51513,3.51513,0,0,0-1.15271,2.8955,6.14685,6.14685,0,0,0,2.38123,4.52783c2.46667,1.80176,5.74621,2.03418,9.46582,2.13819Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><rect x="459.19183" y="372.85162" width="10.77161" height="2" fill="#2f2e41" /><rect
x="493.19183"
y="372.85162"
width="10.77161"
height="2"
fill="#2f2e41"
/><path
d="M652.88209,626.94393a6.00014,6.00014,0,0,1-7.96794-2.91753l-34.511-74.38406a6,6,0,0,1,10.88547-5.0504l34.511,74.38406A6.00012,6.00012,0,0,1,652.88209,626.94393Z"
transform="translate(-285.31105 -86.11101)"
fill="#3f3d56"
/><path
d="M617.425,546.90538a4,4,0,0,1-4-4V491.95275h-50a4,4,0,0,1,0-8h54a4,4,0,0,1,4,4v54.95263A4,4,0,0,1,617.425,546.90538Z"
transform="translate(-285.31105 -86.11101)"
fill="#6c63ff"
/><path
d="M618.371,552.55988a48,48,0,1,1,23.34031-63.74348A48.05437,48.05437,0,0,1,618.371,552.55988Zm-35.35277-76.19831a36,36,0,1,0,47.80761,17.50523A36.04072,36.04072,0,0,0,583.01827,476.36157Z"
transform="translate(-285.31105 -86.11101)"
fill="#3f3d56"
/></svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,94 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useRef } from 'react';
import MissingSvg from './image.svg';
import './style.css';
import { COLOR_TEXT } from '../../constants';
export interface NoFavoriteVocabModalProps {
open: boolean;
setOpen: (open: boolean) => void;
}
function NoFavoriteVocabModal({ open, setOpen }: NoFavoriteVocabModalProps) {
const modal = useRef<HTMLIonModalElement>(null);
function dismiss() {
modal.current?.dismiss();
}
return (
<IonModal
id="no-favorite-connectives-modal"
ref={modal}
trigger="open-custom-dialog"
isOpen={open}
onIonModalDidDismiss={() => setOpen(false)}
>
<div
style={{
padding: '1rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
height: '100%',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div
style={{
height: '33vw',
width: '33vw',
backgroundImage: `url(${MissingSvg})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
}}
></div>
<div style={{ marginTop: '1rem' }}>You have currently no favorite</div>
<div> Vocabulary</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{
width: '1rem',
height: '1rem',
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
}}
onClick={() => setOpen(false)}
>
<IonIcon slot={'icon-only'} icon={closeOutline}></IonIcon>
</IonButton>
</div>
</IonModal>
);
}
export default NoFavoriteVocabModal;

View File

@@ -0,0 +1,21 @@
.bold {
font-weight: bold;
}
ion-modal#no-favorite-connectives-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#no-favorite-connectives-modal::part(backdrop) {
/* background: rgba(209, 213, 219); */
opacity: 1;
}
ion-modal#no-favorite-connectives-modal ion-toolbar {
/* --background: rgb(14 116 144); */
/* --color: white; */
--color: black;
}

View File

@@ -0,0 +1,140 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="649.3779"
height="727.77798"
viewBox="0 0 649.3779 727.77798"
xmlns:xlink="http://www.w3.org/1999/xlink"
role="img"
artist="Katerina Limpitsouni"
source="https://undraw.co/"
><path
d="M648.20033,259.05824h-3.99878V149.51291A63.40187,63.40187,0,0,0,580.79976,86.111H348.713a63.40184,63.40184,0,0,0-63.402,63.4017V750.48713A63.40181,63.40181,0,0,0,348.71284,813.889H580.79945a63.40185,63.40185,0,0,0,63.402-63.40167V337.0345h3.99884Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
d="M583.3578,102.606h-30.295a22.49485,22.49485,0,0,1-20.82715,30.99053H399.2762a22.49484,22.49484,0,0,1-20.82715-30.99061H350.15346a47.34781,47.34781,0,0,0-47.34784,47.34774V750.04628a47.34781,47.34781,0,0,0,47.34777,47.34784H583.3578a47.34781,47.34781,0,0,0,47.34784-47.34778h0V149.95371A47.34777,47.34777,0,0,0,583.3578,102.606Z"
transform="translate(-285.31105 -86.11101)"
fill="#fff"
/><path
id="f3818c68-126c-4685-b4e0-2450731ccc2a-761"
data-name="a2804879-ded6-4045-b20f-1f1dde9b938b"
d="M611.46248,573.92982h-279.619a5.184,5.184,0,0,1-5.178-5.178v-69.361a5.184,5.184,0,0,1,5.178-5.178h279.619a5.184,5.184,0,0,1,5.178,5.178v69.362A5.184,5.184,0,0,1,611.46248,573.92982Zm-279.619-77.646a3.11,3.11,0,0,0-3.107,3.107v69.362a3.11,3.11,0,0,0,3.107,3.107h279.619a3.11,3.11,0,0,0,3.107-3.107v-69.362a3.11,3.11,0,0,0-3.107-3.107Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><circle
id="abdb74b7-e321-430b-89c2-b563f66442fc"
data-name="b9ad11c9-d8a0-4df6-8741-900b9ec46a35"
cx="82.77841"
cy="447.95985"
r="21.74799"
fill="#e6e6e6"
/><path
id="addd02b1-b85b-481a-baea-1b0ba5ed9f4a-762"
data-name="bd261eec-7ae0-49b0-bf26-57ff03972605"
d="M418.31746,519.57286a3.625,3.625,0,0,0,0,7.249h170.878a3.625,3.625,0,0,0,.13989-7.24866l-.02087-.00033q-.05943-.001-.119,0Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
id="a42dc2a4-5fb2-4ea8-b2b3-ce81bc256782-763"
data-name="e80b4447-8c34-435b-ba6c-5300a190ab24"
d="M418.31746,541.32087a3.625,3.625,0,0,0,0,7.249h170.878a3.625,3.625,0,0,0,.119-7.249q-.05943-.00092-.119,0Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
id="b84687c4-e3b4-4975-8361-bf73c33c9ee5-764"
data-name="e55fcb7d-3a3b-45d8-b167-72fb2263dd92"
d="M611.46248,689.91981h-279.619a5.184,5.184,0,0,1-5.178-5.178v-69.361a5.184,5.184,0,0,1,5.178-5.178h279.619a5.184,5.184,0,0,1,5.178,5.178v69.362A5.184,5.184,0,0,1,611.46248,689.91981Zm-279.619-77.646a3.11,3.11,0,0,0-3.107,3.107v69.362a3.11,3.11,0,0,0,3.107,3.107h279.619a3.11,3.11,0,0,0,3.107-3.107v-69.362a3.11,3.11,0,0,0-3.107-3.107Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><circle
id="fe3713e7-4e14-41f8-af1d-48b338e5371c"
data-name="a50d232f-7710-43e4-8fa9-6ef0443fc454"
cx="82.77841"
cy="563.94987"
r="21.74799"
fill="#e6e6e6"
/><path
id="e34cf46a-1d6c-4c41-b5e1-331fa5bf8d4c-765"
data-name="e49b4965-a9e9-4371-9134-194e44e65c31"
d="M418.31746,635.56288a3.625,3.625,0,0,0,0,7.249h170.878a3.625,3.625,0,0,0,.119-7.249q-.05943-.001-.119,0Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
id="e105039f-b7a6-49c8-8f81-87505f1b0ae5-766"
data-name="abc4586a-ac92-4255-aae6-84f53baad571"
d="M418.31746,657.31086a3.625,3.625,0,0,0,0,7.249h170.878a3.625,3.625,0,0,0,.119-7.249q-.05943-.00092-.119,0Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><path
id="ad9187ec-89e0-4b9d-a4fb-dc654c09bafe-767"
data-name="a9e593af-a319-4e97-9065-f0c2c04624d5"
d="M465.76845,429.99485a98.343,98.343,0,0,1-98.384-98.30194v-.08206c0-.206,0-.423.012-.629.3-53.879,44.432-97.756,98.372-97.756a98.384,98.384,0,0,1,.0224,196.768h-.0224Zm0-194.7a96.519,96.519,0,0,0-96.3,95.749c-.011.22-.011.4-.011.564a96.325,96.325,0,1,0,96.337-96.313h-.026Z"
transform="translate(-285.31105 -86.11101)"
fill="#e6e6e6"
/><circle cx="315.11393" cy="422.84174" r="40" fill="#fff" /><path
d="M586.53265,526.94884c-.06861,0-.13721-.00049-.20606-.00195h-21.5a10.39761,10.39761,0,0,1-.0083-20.79248h21.51807c.10547-.00195.22021-.00195.334,0a10.39771,10.39771,0,0,1-.13769,20.79443Z"
transform="translate(-285.31105 -86.11101)"
fill="#6c63ff"
/><rect x="325.3453" y="725.34374" width="324.03261" height="2.24072" fill="#3f3d56" /><path
d="M630.6207,621.842a6.56111,6.56111,0,0,1-.62973-1.259l-5.16339-13.82657a6.50779,6.50779,0,0,1,3.81532-8.36324l115.24228-43.03187a6.50621,6.50621,0,0,1,8.363,3.81491l5.16283,13.82751a6.49951,6.49951,0,0,1-3.81558,8.36283l-115.2412,43.03174a6.5067,6.5067,0,0,1-7.73351-2.55634Z"
transform="translate(-285.31105 -86.11101)"
fill="#6c63ff"
/><path
d="M743.51834,551.78826l-46.86323,18.68723a6,6,0,0,0-4.093,7.1996l5.43852,21.77515a6,6,0,0,0,8.37642,3.97481l46.99241-22.1188a6.0109,6.0109,0,0,0,3.52446-7.717l-5.64883-18.27558A6.01072,6.01072,0,0,0,743.51834,551.78826Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M829.28044,687.45a6.50671,6.50671,0,0,1-6.89226-4.34039l-40.114-116.289a6.49951,6.49951,0,0,1,4.02482-8.26416l13.95295-4.81352a6.50408,6.50408,0,0,1,8.26373,4.02536l40.11465,116.2899a6.50779,6.50779,0,0,1-4.02531,8.26422l-13.95257,4.8125A6.56115,6.56115,0,0,1,829.28044,687.45Z"
transform="translate(-285.31105 -86.11101)"
fill="#6c63ff"
/><path
d="M811.5058,555.77814l15.39577,48.04524a6,6,0,0,1-2.95088,7.73816l-20.26024,9.657a6,6,0,0,1-8.39566-3.934l-12.83035-50.328a6.0109,6.0109,0,0,1,3.71178-7.62871l17.694-7.26835A6.01071,6.01071,0,0,1,811.5058,555.77814Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M764.89068,812.11413H750.13141a6.50753,6.50753,0,0,1-6.5-6.5V682.59948a6.50753,6.50753,0,0,1,6.5-6.5h14.75927a6.50753,6.50753,0,0,1,6.5,6.5V805.61413A6.50753,6.50753,0,0,1,764.89068,812.11413Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M794.08307,812.11413H779.3233a6.50753,6.50753,0,0,1-6.5-6.5V682.59948a6.50753,6.50753,0,0,1,6.5-6.5h14.75977a6.50753,6.50753,0,0,1,6.5,6.5V805.61413A6.50753,6.50753,0,0,1,794.08307,812.11413Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M806.2467,697.06158H739.1842a7.00787,7.00787,0,0,1-7-7V581.11725a40.53125,40.53125,0,0,1,81.0625,0V690.06158A7.00786,7.00786,0,0,1,806.2467,697.06158Z"
transform="translate(-285.31105 -86.11101)"
fill="#ccc"
/><path
d="M816.34118,723.711h-34V531.55621l.6316.17236a45.38181,45.38181,0,0,1,33.3684,43.6875Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M760.32629,723.711h-34V575.41607a45.38226,45.38226,0,0,1,33.36841-43.6875l.63159-.17236Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><circle cx="486.89704" cy="378.42061" r="53.51916" fill="#6c63ff" /><path
d="M832.71545,452.21s3-83-36-56c0,0-22.5-75.5-77.5,37.5l-16,16s72-10,139,21Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><path
d="M773.02436,491.03636c-3.30591-.09179-7.42029-.20654-10.59-2.522a8.13272,8.13272,0,0,1-3.20008-6.07275,5.47091,5.47091,0,0,1,1.86035-4.49317c1.65552-1.39892,4.073-1.727,6.67823-.96142l-2.69922-19.72559,1.98144-.27148,3.17322,23.18994-1.65466-.75928c-1.91834-.87988-4.55164-1.32764-6.188.05518a3.51513,3.51513,0,0,0-1.15271,2.8955,6.14685,6.14685,0,0,0,2.38123,4.52783c2.46667,1.80176,5.74621,2.03418,9.46582,2.13819Z"
transform="translate(-285.31105 -86.11101)"
fill="#2f2e41"
/><rect x="459.19183" y="372.85162" width="10.77161" height="2" fill="#2f2e41" /><rect
x="493.19183"
y="372.85162"
width="10.77161"
height="2"
fill="#2f2e41"
/><path
d="M652.88209,626.94393a6.00014,6.00014,0,0,1-7.96794-2.91753l-34.511-74.38406a6,6,0,0,1,10.88547-5.0504l34.511,74.38406A6.00012,6.00012,0,0,1,652.88209,626.94393Z"
transform="translate(-285.31105 -86.11101)"
fill="#3f3d56"
/><path
d="M617.425,546.90538a4,4,0,0,1-4-4V491.95275h-50a4,4,0,0,1,0-8h54a4,4,0,0,1,4,4v54.95263A4,4,0,0,1,617.425,546.90538Z"
transform="translate(-285.31105 -86.11101)"
fill="#6c63ff"
/><path
d="M618.371,552.55988a48,48,0,1,1,23.34031-63.74348A48.05437,48.05437,0,0,1,618.371,552.55988Zm-35.35277-76.19831a36,36,0,1,0,47.80761,17.50523A36.04072,36.04072,0,0,0,583.01827,476.36157Z"
transform="translate(-285.31105 -86.11101)"
fill="#3f3d56"
/></svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,95 @@
import { IonButton, IonIcon, IonModal } from '@ionic/react';
import { closeOutline } from 'ionicons/icons';
import { useRef } from 'react';
import MissingSvg from './image.svg';
import './style.css';
import { COLOR_TEXT } from '../../constants';
export interface NoFavoriteVocabModalProps {
open: boolean;
setOpen: (open: boolean) => void;
}
function NoFavoriteConnectivesModal({ open, setOpen }: NoFavoriteVocabModalProps) {
const modal = useRef<HTMLIonModalElement>(null);
function dismiss() {
modal.current?.dismiss();
}
return (
<IonModal
id="no-favorite-vocalbulary-modal"
ref={modal}
trigger="open-custom-dialog"
onWillDismiss={() => setOpen(false)}
onIonModalDidDismiss={() => setOpen(false)}
isOpen={open}
>
<div
style={{
padding: '1rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
height: '100%',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
color: COLOR_TEXT,
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
}}
>
<div
style={{
height: '33vw',
width: '33vw',
backgroundImage: `url(${MissingSvg})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
}}
></div>
<div style={{ marginTop: '1rem' }}>You have currently no favorite</div>
<div>Connectives</div>
</div>
</div>
<IonButton
color="light"
shape="round"
style={{
width: '1rem',
height: '1rem',
position: 'absolute',
top: '0.5rem',
right: '0.5rem',
}}
onClick={() => setOpen(false)}
>
<IonIcon slot={'icon-only'} icon={closeOutline}></IonIcon>
</IonButton>
</div>
</IonModal>
);
}
export default NoFavoriteConnectivesModal;

View File

@@ -0,0 +1,21 @@
.bold {
font-weight: bold;
}
ion-modal#no-favorite-vocalbulary-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#no-favorite-vocalbulary-modal::part(backdrop) {
/* background: rgba(209, 213, 219); */
opacity: 1;
}
ion-modal#no-favorite-vocalbulary-modal ion-toolbar {
/* --background: rgb(14 116 144); */
/* --color: white; */
--color: black;
}

View File

@@ -0,0 +1,29 @@
import { checkmarkOutline } from 'ionicons/icons';
import './style.css';
import { IonButton, IonIcon } from '@ionic/react';
interface ContainerProps {
num_rating: number;
num_full_rating: number;
}
const QuestionProgress: React.FC<ContainerProps> = ({ num_rating = 0, num_full_rating = 2 }) => {
return (
<>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '0.15rem' }}>
{Array.from({ length: num_rating }, (_, index) => (
<IonButton shape="round" fill="outline" size="small" color="success">
<IonIcon slot="icon-only" icon={checkmarkOutline}></IonIcon>
</IonButton>
))}
{Array.from({ length: num_full_rating - num_rating }, (_, index) => (
<IonButton shape="round" fill="outline" size="small" color="medium">
<IonIcon slot="icon-only"></IonIcon>
</IonButton>
))}
</div>
</>
);
};
export default QuestionProgress;

View File

@@ -0,0 +1,3 @@
.quizzes_main_menu_container {
text-align: center;
}

View File

@@ -0,0 +1,95 @@
import { IonButton, useIonRouter } from '@ionic/react';
import './QuizzesMainMenuContainer.css';
import { COLOR_TEXT, CONNECTIVE_REVISION_LINK, LISTENING_PRACTICE_LINK, MATCHING_FRENZY_LINK } from '../constants';
interface ContainerProps {
name: string;
}
const QuizzesMainMenuContainer: React.FC<ContainerProps> = ({ name }) => {
const router = useIonRouter();
return (
<div
className="quizzes_main_menu_container"
style={{
display: 'column',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div style={{ margin: '1rem 1rem' }}>
<IonButton
expand="block"
// href={LISTENING_PRACTICE_LINK}
onClick={() => {
router.push(LISTENING_PRACTICE_LINK, undefined, 'replace');
}}
fill="clear"
>
<div style={{ padding: '1rem' }}>
<div
style={{
width: '60vw',
height: '30vw',
backgroundImage: `url('/data/Lesson/images/quiz_listening_practice.jpg')`,
backgroundSize: 'cover',
borderRadius: '1rem',
}}
></div>
<div style={{ marginTop: '2rem', color: COLOR_TEXT }}>Listening Practice</div>
</div>
</IonButton>
</div>
<div style={{ margin: '1rem 1rem' }}>
<IonButton
expand="block"
// href={MATCHING_FRENZY_LINK}
onClick={() => {
router.push(MATCHING_FRENZY_LINK, undefined, 'replace');
}}
fill="clear"
>
<div style={{ padding: '1rem' }}>
<div
style={{
width: '60vw',
height: '30vw',
backgroundImage: `url('/data/Lesson/images/quiz_matching_frenzy.jpg')`,
backgroundSize: 'cover',
borderRadius: '1rem',
}}
></div>
<div style={{ marginTop: '0.5rem', color: COLOR_TEXT }}>Matching Frenzy</div>
</div>
</IonButton>
</div>
<div style={{ margin: '1rem 1rem' }}>
<IonButton
expand="block"
// href={CONNECTIVE_REVISION_LINK}
onClick={() => {
router.push(CONNECTIVE_REVISION_LINK, undefined, 'replace');
}}
fill="clear"
>
<div style={{ padding: '1rem' }}>
<div
style={{
width: '60vw',
height: '30vw',
backgroundImage: `url('/data/Lesson/images/quiz_connectives_revision.jpg')`,
backgroundSize: 'cover',
borderRadius: '1rem',
}}
></div>
<div style={{ marginTop: '0.5rem', color: COLOR_TEXT }}>Connectives Revision</div>
</div>
</IonButton>
</div>
</div>
);
};
export default QuizzesMainMenuContainer;

View File

@@ -0,0 +1,24 @@
import { IonToast } from '@ionic/react';
import React from 'react';
import './style.css';
import { THE_WORD_REMOVED, THE_WORD_REMOVED_DISMISS_TIMEOUT } from '../../constants';
const RemoveFavoritePrompt: React.FC<{ open: boolean; setIsOpen: (isOpen: boolean) => void }> = ({
open,
setIsOpen,
}) => {
return (
<>
<IonToast
className="remove-favorite-prompt-toast"
isOpen={open}
message={THE_WORD_REMOVED}
onDidDismiss={() => setIsOpen(false)}
color="success"
duration={THE_WORD_REMOVED_DISMISS_TIMEOUT}
/>
</>
);
};
export default RemoveFavoritePrompt;

View File

@@ -0,0 +1,11 @@
ion-toast.remove-favorite-prompt-toast::part(message) {
text-align: center;
font-size: 1.5rem;
color: rgba(0, 0, 0, 0.9);
}
/*
ion-toast.remove-favorite-prompt-toast::part(container) {
bottom: 100px;
}
*/

View File

@@ -0,0 +1,24 @@
.container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.container strong {
font-size: 20px;
line-height: 26px;
}
.container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
.container a {
text-decoration: none;
}

View File

@@ -0,0 +1,135 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="845.87074"
height="569.98927"
viewBox="0 0 845.87074 569.98927"
xmlns:xlink="http://www.w3.org/1999/xlink"
role="img"
artist="Katerina Limpitsouni"
source="https://undraw.co/"
><polygon
points="760.114 287.832 752.997 294.857 762.452 319.527 775.643 292.177 797.249 168.007 764.02 167.081 760.114 287.832"
fill="#9f616a"
/><polygon
points="705.966 547.714 687.966 547.714 673.966 388.714 708.966 388.714 705.966 547.714"
fill="#9f616a"
/><polygon
points="773.966 547.714 755.966 547.714 741.966 388.714 776.966 388.714 773.966 547.714"
fill="#9f616a"
/><path
d="M937.09032,733.461l-26.27966-.13983a10.60246,10.60246,0,0,1-5.18583-13.20591h0a10.60249,10.60249,0,0,1,8.18437-6.73544l4.96424-.82737,4.32555-5.50525a8.19029,8.19029,0,0,1,10.758-2.00034c4.14944,2.63357,9.90449,3.511,17,2.92223l5.52291,13.4861c1.09439,7.29591-3.90074,12.10883-11.188,13.25945h0A13.43132,13.43132,0,0,1,937.09032,733.461Z"
transform="translate(-177.06463 -165.00536)"
fill="#2f2e41"
/><path
d="M869.09032,733.461l-26.27966-.13983a10.60246,10.60246,0,0,1-5.18583-13.20591h0a10.60249,10.60249,0,0,1,8.18437-6.73544l4.96424-.82737,4.80952-6.12121a7.45937,7.45937,0,0,1,10.01669-1.55131c4.16748,2.7633,10.01393,3.69017,17.25728,3.08916l5.52291,13.4861c1.09439,7.29591-3.90074,12.10883-11.188,13.25945h0A13.43132,13.43132,0,0,1,869.09032,733.461Z"
transform="translate(-177.06463 -165.00536)"
fill="#2f2e41"
/><circle cx="710.9656" cy="55.71363" r="27" fill="#9f616a" /><path
d="M927.03023,272.719l-44,3c.15576-14.2391-.46558-27.13579-4-35l31-10C910.3279,243.25667,916.90486,257.50461,927.03023,272.719Z"
transform="translate(-177.06463 -165.00536)"
fill="#9f616a"
/><path
d="M954.03023,467.719l-113-9c15.0769-50.26016,10.35876-116.07391-5-165.41577a28.4717,28.4717,0,0,1,18.28887-26.58848L883.03023,255.719l36,4,30.356,15.178a27.71585,27.71585,0,0,1,15.31608,25.30937C953.29231,345.83844,951.91774,404.51285,954.03023,467.719Z"
transform="translate(-177.06463 -165.00536)"
fill="#6c63ff"
/><path
d="M869.03023,344.719l-51-9L822.42,301.69848a42.20258,42.20258,0,0,1,30.84455-35.34011l9.76571-2.63938Z"
transform="translate(-177.06463 -165.00536)"
fill="#6c63ff"
/><polygon
points="661.966 292.714 667.966 300.714 658.966 325.714 645.966 294.714 642.966 168.714 675.966 172.714 661.966 292.714"
fill="#9f616a"
/><path
d="M925.03023,346.719l51-9-4.38975-34.02051a42.20256,42.20256,0,0,0-30.84455-35.34011l-9.7657-2.63938Z"
transform="translate(-177.06463 -165.00536)"
fill="#6c63ff"
/><path
d="M957.03023,558.719l-41.83858,2-12.16142-59-14.76772,55-44.23228,1-8.8417-71.06584a41.09716,41.09716,0,0,1,8.84169-30.93414v0l110,5Z"
transform="translate(-177.06463 -165.00536)"
fill="#2f2e41"
/><path
d="M890.53023,250.219l20.19008-3.82345,12.0114-20.51949a27.28013,27.28013,0,0,0-3.19387-31.95038l0,0a27.28016,27.28016,0,0,0-24.2073-8.837l-26.16686,3.73812a17.27884,17.27884,0,0,0-14.64408,19.66833l17.62513,2.99868Z"
transform="translate(-177.06463 -165.00536)"
fill="#2f2e41"
/><ellipse cx="701.4656" cy="63.21363" rx="5" ry="6" fill="#9f616a" /><path
d="M303.53023,177.219a11.01245,11.01245,0,0,0-11,11v201a11.01245,11.01245,0,0,0,11,11h5a11.01245,11.01245,0,0,0,11-11v-201a11.01245,11.01245,0,0,0-11-11Z"
transform="translate(-177.06463 -165.00536)"
fill="#e6e6e6"
/><path
d="M303.53023,181.719a6.50753,6.50753,0,0,0-6.5,6.5v201a6.50752,6.50752,0,0,0,6.5,6.5h5a6.50753,6.50753,0,0,0,6.5-6.5v-201a6.50753,6.50753,0,0,0-6.5-6.5Z"
transform="translate(-177.06463 -165.00536)"
fill="#fff"
/><path
d="M385.53023,177.219a11.01245,11.01245,0,0,0-11,11v201a11.01245,11.01245,0,0,0,11,11h5a11.01245,11.01245,0,0,0,11-11v-201a11.01245,11.01245,0,0,0-11-11Z"
transform="translate(-177.06463 -165.00536)"
fill="#e6e6e6"
/><path
d="M385.53023,181.719a6.50753,6.50753,0,0,0-6.5,6.5v201a6.50752,6.50752,0,0,0,6.5,6.5h5a6.50753,6.50753,0,0,0,6.5-6.5v-201a6.50753,6.50753,0,0,0-6.5-6.5Z"
transform="translate(-177.06463 -165.00536)"
fill="#fff"
/><path
d="M467.53023,177.219a11.01245,11.01245,0,0,0-11,11v201a11.01245,11.01245,0,0,0,11,11h5a11.01245,11.01245,0,0,0,11-11v-201a11.01245,11.01245,0,0,0-11-11Z"
transform="translate(-177.06463 -165.00536)"
fill="#e6e6e6"
/><path
d="M467.53023,181.719a6.50753,6.50753,0,0,0-6.5,6.5v201a6.50752,6.50752,0,0,0,6.5,6.5h5a6.50753,6.50753,0,0,0,6.5-6.5v-201a6.50753,6.50753,0,0,0-6.5-6.5Z"
transform="translate(-177.06463 -165.00536)"
fill="#fff"
/><path
d="M549.53023,177.219a11.01245,11.01245,0,0,0-11,11v201a11.01245,11.01245,0,0,0,11,11h5a11.01245,11.01245,0,0,0,11-11v-201a11.01245,11.01245,0,0,0-11-11Z"
transform="translate(-177.06463 -165.00536)"
fill="#e6e6e6"
/><path
d="M549.53023,181.719a6.50753,6.50753,0,0,0-6.5,6.5v201a6.50752,6.50752,0,0,0,6.5,6.5h5a6.50753,6.50753,0,0,0,6.5-6.5v-201a6.50753,6.50753,0,0,0-6.5-6.5Z"
transform="translate(-177.06463 -165.00536)"
fill="#fff"
/><path
d="M631.53023,177.219a11.01245,11.01245,0,0,0-11,11v201a11.01245,11.01245,0,0,0,11,11h5a11.01245,11.01245,0,0,0,11-11v-201a11.01245,11.01245,0,0,0-11-11Z"
transform="translate(-177.06463 -165.00536)"
fill="#e6e6e6"
/><path
d="M631.53023,181.719a6.50753,6.50753,0,0,0-6.5,6.5v201a6.50752,6.50752,0,0,0,6.5,6.5h5a6.50753,6.50753,0,0,0,6.5-6.5v-201a6.50753,6.50753,0,0,0-6.5-6.5Z"
transform="translate(-177.06463 -165.00536)"
fill="#fff"
/><path
d="M322.2878,242.2493H290.77265a10.25379,10.25379,0,0,1-10.24242-10.24243v-1.57576a10.25379,10.25379,0,0,1,10.24242-10.24242H322.2878a10.25379,10.25379,0,0,1,10.24243,10.24242v1.57576A10.25379,10.25379,0,0,1,322.2878,242.2493Z"
transform="translate(-177.06463 -165.00536)"
fill="#6c63ff"
/><path
d="M406.2878,275.2493H374.77265a10.25379,10.25379,0,0,1-10.24242-10.24243v-1.57576a10.25379,10.25379,0,0,1,10.24242-10.24242H406.2878a10.25379,10.25379,0,0,1,10.24243,10.24242v1.57576A10.25379,10.25379,0,0,1,406.2878,275.2493Z"
transform="translate(-177.06463 -165.00536)"
fill="#6c63ff"
/><path
d="M486.2878,340.2493H454.77265a10.25379,10.25379,0,0,1-10.24242-10.24243v-1.57576a10.25379,10.25379,0,0,1,10.24242-10.24242H486.2878a10.25379,10.25379,0,0,1,10.24243,10.24242v1.57576A10.25379,10.25379,0,0,1,486.2878,340.2493Z"
transform="translate(-177.06463 -165.00536)"
fill="#6c63ff"
/><path
d="M568.2878,222.2493H536.77265a10.25379,10.25379,0,0,1-10.24242-10.24243v-1.57576a10.25379,10.25379,0,0,1,10.24242-10.24242H568.2878a10.25379,10.25379,0,0,1,10.24243,10.24242v1.57576A10.25379,10.25379,0,0,1,568.2878,222.2493Z"
transform="translate(-177.06463 -165.00536)"
fill="#6c63ff"
/><path
d="M650.2878,300.2493H618.77265a10.25379,10.25379,0,0,1-10.24242-10.24243v-1.57576a10.25379,10.25379,0,0,1,10.24242-10.24242H650.2878a10.25379,10.25379,0,0,1,10.24243,10.24242v1.57576A10.25379,10.25379,0,0,1,650.2878,300.2493Z"
transform="translate(-177.06463 -165.00536)"
fill="#6c63ff"
/><polygon
points="845.475 569.989 578.074 569.989 578.074 567.804 845.871 567.804 845.475 569.989"
fill="#3f3d56"
/><circle cx="129.4656" cy="275.21363" r="13" fill="#e6e6e6" /><circle
cx="211.4656"
cy="275.21363"
r="13"
fill="#e6e6e6"
/><circle cx="293.4656" cy="275.21363" r="13" fill="#e6e6e6" /><circle
cx="375.4656"
cy="275.21363"
r="13"
fill="#e6e6e6"
/><circle cx="457.4656" cy="275.21363" r="13" fill="#e6e6e6" /><path
d="M205.86092,165.00536a28.79631,28.79631,0,1,0,28.79632,28.79629A28.79629,28.79629,0,0,0,205.86092,165.00536Zm0,53.33839a24.54208,24.54208,0,1,1,24.5421-24.5421A24.54208,24.54208,0,0,1,205.86092,218.34375Z"
transform="translate(-177.06463 -165.00536)"
fill="#e6e6e6"
/><path
d="M204.26992,203.64975a1.51846,1.51846,0,0,1-1.07424-.44513L194.99111,195a1.5192,1.5192,0,0,1,2.14848-2.14847l6.95228,6.95228,12.32319-16.80442a1.51945,1.51945,0,0,1,2.45057,1.79712l-13.37042,18.23239a1.52072,1.52072,0,0,1-1.10955.61665C204.34708,203.64856,204.3082,203.64975,204.26992,203.64975Z"
transform="translate(-177.06463 -165.00536)"
fill="#6c63ff"
/></svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,50 @@
import { IonButton, IonIcon, useIonRouter } from '@ionic/react';
import { arrowBack } from 'ionicons/icons';
import { LESSON_LINK, VERSIONS } from '../../constants';
import SettingSvg from './image.svg';
interface ContainerProps {
name: string;
}
const SettingContainer: React.FC<ContainerProps> = ({ name }) => {
const router = useIonRouter();
return (
<div
style={{
width: '100%',
height: '85%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
backgroundImage: `url(${SettingSvg})`,
height: '33vh',
width: '33vh',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
}}
></div>
<div>
<p>T.B.A.</p>
</div>
<div>{VERSIONS}</div>
<IonButton
onClick={() => {
router.push(LESSON_LINK, undefined, 'replace');
}}
style={{ marginTop: '1rem' }}
>
<IonIcon slot="start" icon={arrowBack}></IonIcon>
Back
</IonButton>
</div>
);
};
export default SettingContainer;

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,90 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="863.91732"
height="364.20537"
viewBox="0 0 863.91732 364.20537"
xmlns:xlink="http://www.w3.org/1999/xlink"
role="img"
artist="Katerina Limpitsouni"
source="https://undraw.co/"
><polygon
points="311.959 119.745 0 119.745 0 222.156 11.817 222.156 11.817 248.941 38.601 222.156 311.959 222.156 311.959 119.745"
fill="#f0f0f0"
/><rect x="8.66553" y="129.71814" width="294.62811" height="81.92868" fill="#fff" /><rect
x="34.72148"
y="154.42552"
width="141.85589"
height="4.30497"
fill="#f0f0f0"
/><rect x="34.72148" y="169.31777" width="247.24292" height="4.30497" fill="#f0f0f0" /><rect
x="34.72148"
y="184.21003"
width="247.00081"
height="4.30497"
fill="#f0f0f0"
/><path
d="M692.07274,618.96676s1.487-31.15875,31.97119-27.537"
transform="translate(-168.04134 -267.89732)"
fill="#f0f0f0"
/><circle cx="515.41796" cy="306.16087" r="15.2571" fill="#f0f0f0" /><rect
x="512.9354"
y="331.85294"
width="4.30672"
height="30.14703"
fill="#f0f0f0"
/><circle cx="666.92952" cy="180.07338" r="123.29665" fill="#3f3d56" /><path
d="M757.348,457.86815a32.62688,32.62688,0,0,1,50.081,0,36.26372,36.26372,0,1,0-51.27085-1.18987Q756.73918,457.28694,757.348,457.86815Z"
transform="translate(-168.04134 -267.89732)"
fill="#fff"
/><path
d="M855.26,457.86815a32.627,32.627,0,0,1,50.08092,0,36.26371,36.26371,0,1,0-51.2708-1.18987Q854.65117,457.28694,855.26,457.86815Z"
transform="translate(-168.04134 -267.89732)"
fill="#fff"
/><circle cx="601.97649" cy="151.39215" r="12.47434" fill="#3f3d56" /><circle
cx="699.88499"
cy="151.39215"
r="12.47434"
fill="#3f3d56"
/><circle cx="578.08341" cy="210.89752" r="14.50548" fill="#6c63ff" /><circle
cx="744.8965"
cy="210.89752"
r="14.5055"
fill="#6c63ff"
/><polygon points="661.49 181.886 650.611 229.029 668.742 210.898 661.49 181.886" fill="#6c63ff" /><polygon
points="717.39 363.205 705.038 352.839 705.326 363.205 701.49 363.205 701.183 352.244 684.507 363.205 677.526 363.205 701.059 347.737 700.147 315.258 699.466 290.728 703.293 290.623 703.984 315.258 704.894 347.708 723.354 363.205 717.39 363.205"
fill="#3f3d56"
/><polygon
points="659.363 363.205 647.012 352.839 647.3 363.205 643.474 363.205 643.167 352.244 626.49 363.205 619.509 363.205 643.033 347.737 642.122 315.258 641.441 290.728 645.276 290.623 645.967 315.258 646.868 347.708 665.328 363.205 659.363 363.205"
fill="#3f3d56"
/><path
d="M836.784,315.60813c-3.3831,0-6.36764,2.628-8.36294,6.66445-1.75872-6.06969-5.45374-10.29078-9.7689-10.29078a6.56326,6.56326,0,0,0-.87094.1463c-1.65871-6.4805-5.51368-11.02542-10.00816-11.02542-6.00841,0-10.8791,8.118-10.8791,18.13187s4.87073,18.13187,10.8791,18.13187a6.56119,6.56119,0,0,0,.87093-.14629c1.65871,6.4805,5.51369,11.02541,10.00817,11.02541,3.3831,0,6.36764-2.62795,8.36294-6.66444,1.75876,6.06971,5.45374,10.29077,9.7689,10.29077,6.00841,0,10.8791-8.118,10.8791-18.13187S842.79244,315.60813,836.784,315.60813Z"
transform="translate(-168.04134 -267.89732)"
fill="#3f3d56"
/><path
d="M718.72328,451.807l-67.92039-11.01653c-3.42269-.55515-6.90789-1.11141-10.34147-.6282s-6.87069,2.1737-8.62107,5.16688a8.651,8.651,0,0,0,9.14985,12.853c-3.70741-.12023-7.60411-.19978-10.894,1.51369s-5.61946,5.87559-4.01553,9.22024a8.27667,8.27667,0,0,0,1.91922,2.4289,17.60582,17.60582,0,0,0,18.52289,3.14128c-2.50047,3.58582-7.46212,4.11838-11.7541,4.94866s-9.25362,3.258-9.41312,7.62664c-.17922,4.90869,5.66264,7.51763,10.47189,8.51687A137.41687,137.41687,0,0,0,712.648,489.3171a30.98,30.98,0,0,0,7.737-3.95049,17.43266,17.43266,0,0,0-7.05356-30.96345"
transform="translate(-168.04134 -267.89732)"
fill="#3f3d56"
/><path
d="M1011.89005,507.47917a137.41884,137.41884,0,0,0-51.17256-57.63676,30.97519,30.97519,0,0,0-7.80737-3.80966,17.43272,17.43272,0,0,0-20.50879,24.24615l-5.31525-2.74921Q943.09323,497.98314,959.1,528.43656c1.61312,3.06929,3.26318,6.18918,5.71292,8.64309s5.86648,4.18514,9.31075,3.78525a8.6006,8.6006,0,0,0,6.77916-12.2999,16.64264,16.64264,0,0,0,5.752,5.05979c3.34648,1.59972,8.07321.9603,9.7823-2.33177a8.27455,8.27455,0,0,0,.78809-2.99368,17.60592,17.60592,0,0,0-8.62117-16.69248c4.36853-.1565,7.77622,3.48909,11.01912,6.42052,3.24327,2.93143,8.1652,5.43808,11.75289,2.94008C1015.40712,518.16062,1013.98161,511.92354,1011.89005,507.47917Z"
transform="translate(-168.04134 -267.89732)"
fill="#3f3d56"
/><polygon
points="55.757 0 506 0 506 147.807 488.945 147.807 488.945 186.463 450.289 147.807 55.757 147.807 55.757 0"
fill="#cacaca"
/><rect x="68.26381" y="14.39335" width="425.22943" height="118.24561" fill="#fff" /><rect
x="102.45877"
y="48.91591"
width="204.73707"
height="6.21326"
fill="#6c63ff"
/><rect x="102.45877" y="70.40954" width="356.83952" height="6.21326" fill="#6c63ff" /><rect
x="102.45877"
y="91.90316"
width="356.49009"
height="6.21326"
fill="#6c63ff"
/><path
d="M1030.95866,632.10268h-381a1,1,0,0,1,0-2h381a1,1,0,0,1,0,2Z"
transform="translate(-168.04134 -267.89732)"
fill="#cacaca"
/></svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,18 @@
import ImageSvg from './image.svg';
function SpeakerImage() {
return (
<div
style={{
width: '100%',
height: '100%',
backgroundImage: `url(${ImageSvg})`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
></div>
);
}
export { SpeakerImage };

View File

@@ -0,0 +1,44 @@
import { IonToast } from '@ionic/react';
import { WRONG_ANSWER_MESSAGE } from '../../constants';
import './style.css';
import { useAppStateContext } from '../../contexts/AppState';
interface QuestionCardProps {
isOpen: boolean;
dismiss: () => void;
correct_answer?: string;
}
const WrongAnswerToast: React.FC<QuestionCardProps> = ({ isOpen, dismiss, correct_answer = '' }) => {
const { WRONG_ANS_TOAST_APPEAR_TIMEOUT_S } = useAppStateContext();
if (correct_answer != '') {
return (
<>
<IonToast
className="wrong-answer-toast"
isOpen={isOpen}
message={`${WRONG_ANSWER_MESSAGE}. The correct answer is "${correct_answer}"`}
onDidDismiss={() => dismiss()}
duration={WRONG_ANS_TOAST_APPEAR_TIMEOUT_S * 1000}
color="danger"
></IonToast>
</>
);
} else {
return (
<>
<IonToast
className="wrong-answer-toast"
isOpen={isOpen}
message={`${WRONG_ANSWER_MESSAGE}`}
onDidDismiss={() => dismiss()}
duration={WRONG_ANS_TOAST_APPEAR_TIMEOUT_S * 1000}
color="danger"
></IonToast>
</>
);
}
};
export default WrongAnswerToast;

View File

@@ -0,0 +1,10 @@
ion-toast.wrong-answer-toast::part(message) {
/* text-align: center; */
font-size: 1.5rem;
color: rgba(0, 0, 0, 0.9);
line-height: 2rem;
}
ion-toast.wrong-answer-toast::part(container) {
bottom: 100px;
}

View File

@@ -0,0 +1,48 @@
import { IonToast } from '@ionic/react';
import { WRONG_ANSWER_MESSAGE } from '../../constants';
import './style.css';
import { useAppStateContext } from '../../contexts/AppState';
interface QuestionCardProps {
isOpen: boolean;
dismiss: () => void;
correct_answer: string;
}
const WrongAnswerToastWithoutCorrectAnswer: React.FC<QuestionCardProps> = ({
isOpen,
dismiss,
correct_answer = '',
}) => {
const { WRONG_ANS_TOAST_APPEAR_TIMEOUT_S } = useAppStateContext();
if (correct_answer != '') {
return (
<>
<IonToast
className="wrong-answer-toast"
isOpen={isOpen}
message={`${WRONG_ANSWER_MESSAGE}. The correct answer is "${correct_answer}"`}
onDidDismiss={() => dismiss()}
duration={WRONG_ANS_TOAST_APPEAR_TIMEOUT_S * 1000}
color="danger"
></IonToast>
</>
);
} else {
return (
<>
<IonToast
className="wrong-answer-toast"
isOpen={isOpen}
message={`${WRONG_ANSWER_MESSAGE}`}
onDidDismiss={() => dismiss()}
duration={WRONG_ANS_TOAST_APPEAR_TIMEOUT_S * 1000}
color="danger"
></IonToast>
</>
);
}
};
export default WrongAnswerToastWithoutCorrectAnswer;

View File

@@ -0,0 +1,10 @@
ion-toast.wrong-answer-toast::part(message) {
/* text-align: center; */
font-size: 1.5rem;
color: rgba(0, 0, 0, 0.9);
line-height: 2rem;
}
ion-toast.wrong-answer-toast::part(container) {
bottom: 100px;
}

View File

@@ -0,0 +1,138 @@
// 0.0.1 - implement screen
// 0.0.2 - implement logic
// 0.0.3 - first demo
// 0.0.4 - bug fix
// 0.0.5 - misc update
// 0.0.6 - fix scroe board problem
// 0.0.7 - add back button for listening practice, matching frenzy, connective revision
// 0.0.8 - add back button for listening practice card, matching frenzy card, connective revision card
// 0.0.9 - fix ticket 26,27,28,29
// 0.0.10 - remove debug symbol and re-route ending page
const VERSIONS = 'v0.0.10';
const HELLOWORLD_MP3 = '/helloworld.mp3';
// api
const API_URL = 'http://localhost:8080';
// route
const LISTENING_PRACTICE_LINK = '/listening_practice';
const MATCHING_FRENZY_LINK = '/matching_frenzy';
const FAVORITE_LINK = '/fav';
const LESSON_WORD_PAGE_LINK = '/lesson_word_page';
const CONNECTIVE_REVISION_LINK = '/connective_revision';
const LESSON_LINK = '/lesson';
const QUIZ_MAIN_MENU_LINK = '/quizzes_main_menu';
const RECORD_LINK = '/record';
const SETTING_LINK = '/setting';
const DEBUG_LINK = '/debug';
const MY_ACHIEVEMENT_LINK = '/my_achievement';
// achievement
const FULLMARK_COUNT_KEY = 'full_mark_count';
const APP_USE_TIME = 'app_use_time_s';
const LISTENING_PRACTICE_TIME_SPENT = 'listening_practice_time_spent_s';
const MATCHING_FRENZY_CORRECT_COUNT = 'matching_frenzy_correct_count';
const CONNECTIVES_REVISION_CORRECT_COUNT = 'connectives_revision_correct_count';
const CONNECTIVES_REVISION_IGNORE_COUNT = 'connectives_revision_ignore_count';
const FULLMARK_IGNORE_COUNT = 'full_mark_ignore_count';
const MATCH_FRENZY_SCOREBOARD_KEY = 'matching_frenzy_scoreboard';
const COLOR_TEXT = 'rgba(0,0,0,0.9)';
const GOOD_JOB_BG_COLOR = 'rgba (0,255,0,1)';
const WRONG_ANS_BG_COLOR = 'rgba (255,0,0,1)';
const HELLOWORLD = 'HELLOWORLD';
//
const GENIUS_STAGES = [10, 50, 100, 300, 700, 1000];
const HARDWORKER_STAGES = [5, 50, 100, 500, 1000, 1500, 3000];
const ATTENTIVE_EARS_STAGES = [1, 10, 50, 100, 300, 700, 1000];
const MATCHMAKING_STAGES = [30, 100, 250, 500, 1500, 3000, 8000];
const CONNECTIVE_CONQUEROR_STAGES = [1, 5, 15, 35, 60];
//
const THE_WORD_REMOVED = 'The word removed';
const THE_WORD_REMOVED_DISMISS_TIMEOUT = 1000;
const CORRECT_ANSWER_MESSAGE = 'Good job!';
const CORRECT_ANSWER_DISMISS_TIMEOUT = 3000;
//
const WRONG_ANSWER_MESSAGE = 'Wrong answer !';
const WRONG_ANSWER_DISMISS_TIMEOUT = 3000;
//
const PRESS_START_TO_BEGIN = 'Press start to begin';
const PRESS_START_TO_BEGIN_MESSAGE = 'Relax and Prepare yourself';
//
const CORRECTION_PHASE = 'correction phase';
//
const hide_setting = true;
const DEBUG = false;
const TEST = process.env.NODE_ENV === 'test';
//
const MY_FAVORITE = 'My Favorite';
//
export {
//
API_URL,
APP_USE_TIME,
ATTENTIVE_EARS_STAGES,
//
COLOR_TEXT,
CONNECTIVES_REVISION_CORRECT_COUNT,
CONNECTIVES_REVISION_IGNORE_COUNT,
CONNECTIVE_CONQUEROR_STAGES,
CONNECTIVE_REVISION_LINK,
//
CORRECTION_PHASE,
CORRECT_ANSWER_DISMISS_TIMEOUT,
//
CORRECT_ANSWER_MESSAGE,
DEBUG,
DEBUG_LINK,
FAVORITE_LINK,
//
FULLMARK_COUNT_KEY,
FULLMARK_IGNORE_COUNT,
//
GENIUS_STAGES,
//
GOOD_JOB_BG_COLOR,
HARDWORKER_STAGES,
//
HELLOWORLD,
HELLOWORLD_MP3,
LESSON_LINK,
LESSON_WORD_PAGE_LINK,
LISTENING_PRACTICE_LINK,
LISTENING_PRACTICE_TIME_SPENT,
MATCHING_FRENZY_CORRECT_COUNT,
MATCHING_FRENZY_LINK,
MATCHMAKING_STAGES,
MATCH_FRENZY_SCOREBOARD_KEY,
MY_ACHIEVEMENT_LINK,
//
MY_FAVORITE,
//
PRESS_START_TO_BEGIN,
PRESS_START_TO_BEGIN_MESSAGE,
QUIZ_MAIN_MENU_LINK,
RECORD_LINK,
SETTING_LINK,
TEST,
//
THE_WORD_REMOVED,
THE_WORD_REMOVED_DISMISS_TIMEOUT,
VERSIONS,
WRONG_ANSWER_DISMISS_TIMEOUT,
WRONG_ANSWER_MESSAGE,
WRONG_ANS_BG_COLOR,
//
hide_setting,
};
export const DEFAULT_FORWARD_TIMEOUT = 3000;

View File

@@ -0,0 +1,42 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { useIdle } from 'react-use';
import { useAppUseTime } from './MyIonMetric/AppUseTime';
const AppOnRecorderContext = createContext<MyContextProps | undefined>(undefined);
export const AppOnRecorderProvider: React.FC = () => {
const [my_context, setMyContext] = useState<string>('initial value');
const { myIonMetricIncAppUseTime } = useAppUseTime();
const isIdle = useIdle(3e3);
let getCurrentTimeInSec = () => Math.floor(Date.now() / 1000);
let [last_on_s, setLastOn_s] = useState(getCurrentTimeInSec());
useEffect(() => {
(async () => {
if (isIdle) {
// active -> inactive
let current_s = getCurrentTimeInSec();
myIonMetricIncAppUseTime(current_s - last_on_s);
} else {
// inactive->active
setLastOn_s(getCurrentTimeInSec());
}
})();
}, [isIdle]);
return <AppOnRecorderContext.Provider value={{ my_context, setMyContext }}>{/* */}</AppOnRecorderContext.Provider>;
};
export const useMyContext = (): MyContextProps => {
const context = useContext(AppOnRecorderContext);
if (!context) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
interface MyContextProps {
my_context: string;
setMyContext: React.Dispatch<React.SetStateAction<string>>;
}

View File

@@ -0,0 +1,145 @@
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import RemoveFavoritePrompt from '../components/RemoveFavoritePrompt';
import { LESSON_LINK } from '../constants';
const AppStateContext = createContext<AppStateContextProps | undefined>(undefined);
export const AppStateProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [my_context, setMyContext] = useState<string>('initial value');
const [tab_active, setTabActive] = useState<string>(LESSON_LINK);
const [show_confirm_user_exit, useShowConfirmUserExit] = useState<boolean>(false);
const [url_push_after_user_confirm, setURLPushAfterUserConfirm] = useState<string>(LESSON_LINK);
const [matching_frenzy_in_progress, setMatchingFrenzyInProgress] = useState<boolean>(false);
const [connective_revision_in_progress, setConnectiveRevisionInProgress] = useState<boolean>(false);
const [listening_practice_in_progress, setListeningPracticeInProgress] = useState<boolean>(false);
const [disable_user_tap, setDisableUserTap] = useState<boolean>(false);
const [show_remove_fav_prompt, setShowRemoceFavPrompt] = useState(false);
// 010_user_configurable_anwered_timeout
const [LISTENING_PRACTICE_ANWERED_WAIT_S, setListeningPracticeAnswerWait_s] = useState<number>(1);
const [MATCHING_FRENZY_ANWERED_WAIT_S, setMatchingFrenzyAnswerWait_s] = useState<number>(1);
//
const [WRONG_ANS_TOAST_APPEAR_TIMEOUT_S, setWRONG_ANS_TOAST_APPEAR_TIMEOUT_S] = useState<number>(3);
const [CORRECT_ANS_TOAST_APPEAR_TIMEOUT_S, setCORRECT_ANS_TOAST_APPEAR_TIMEOUT_S] = useState<number>(3);
const [CONNECTIVES_REVISION_ANWERED_WAIT_S, setCONNECTIVES_REVISION_ANWERED_WAIT_S] = useState<number>(3);
// user_config.json
// 012_put_matching_frenzy_count_down_time_to_config_file
const [user_config_json, setUserConfigJson] = useState<any>({});
const [MATCHING_FRENZY_COUNT_DOWN_S, setMATCHING_FRENZY_COUNT_DOWN_S] = useState<number>(120);
useEffect(() => {
fetch('/data/user_config.json')
.then((res) => res.json())
.then((res_json) => {
setUserConfigJson(res_json);
setMATCHING_FRENZY_COUNT_DOWN_S(res_json['matching_frenzy_count_down_s']);
setListeningPracticeAnswerWait_s(res_json['listening_practice_anwered_wait_s']);
setMatchingFrenzyAnswerWait_s(res_json['matching_frenzy_anwered_wait_s']);
setCONNECTIVES_REVISION_ANWERED_WAIT_S(res_json['connectives_revision_anwered_wait_s']);
// console.log({ res_json });
setWRONG_ANS_TOAST_APPEAR_TIMEOUT_S(res_json['WRONG_ANS_TOAST_APPEAR_TIMEOUT_S']);
setCORRECT_ANS_TOAST_APPEAR_TIMEOUT_S(res_json['CORRECT_ANS_TOAST_APPEAR_TIMEOUT_S']);
});
}, []);
return (
<AppStateContext.Provider
value={{
my_context,
setMyContext,
//
tab_active,
setTabActive,
//
listening_practice_in_progress,
setListeningPracticeInProgress,
//
matching_frenzy_in_progress,
setMatchingFrenzyInProgress,
//
connective_revision_in_progress,
setConnectiveRevisionInProgress,
//
show_confirm_user_exit,
setShowConfirmUserExit: useShowConfirmUserExit,
//
url_push_after_user_confirm,
setURLPushAfterUserConfirm,
//
disable_user_tap,
setDisableUserTap,
//
show_remove_fav_prompt,
setShowRemoceFavPrompt,
//
// 012_put_matching_frenzy_count_down_time_to_config_file
MATCHING_FRENZY_COUNT_DOWN_S,
//
LISTENING_PRACTICE_ANWERED_WAIT_S,
MATCHING_FRENZY_ANWERED_WAIT_S,
CONNECTIVES_REVISION_ANWERED_WAIT_S,
//
WRONG_ANS_TOAST_APPEAR_TIMEOUT_S,
CORRECT_ANS_TOAST_APPEAR_TIMEOUT_S,
}}
>
{children}
<RemoveFavoritePrompt open={show_remove_fav_prompt} setIsOpen={setShowRemoceFavPrompt} />
</AppStateContext.Provider>
);
};
export const useAppStateContext = (): AppStateContextProps => {
const context = useContext(AppStateContext);
if (!context) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
interface AppStateContextProps {
my_context: string;
setMyContext: React.Dispatch<React.SetStateAction<string>>;
//
listening_practice_in_progress: boolean;
setListeningPracticeInProgress: React.Dispatch<React.SetStateAction<boolean>>;
//
//
matching_frenzy_in_progress: boolean;
setMatchingFrenzyInProgress: React.Dispatch<React.SetStateAction<boolean>>;
//
connective_revision_in_progress: boolean;
setConnectiveRevisionInProgress: React.Dispatch<React.SetStateAction<boolean>>;
//
tab_active: string;
setTabActive: React.Dispatch<React.SetStateAction<string>>;
//
show_confirm_user_exit: boolean;
setShowConfirmUserExit: React.Dispatch<React.SetStateAction<boolean>>;
//
url_push_after_user_confirm: string;
setURLPushAfterUserConfirm: React.Dispatch<React.SetStateAction<string>>;
//
disable_user_tap: boolean;
setDisableUserTap: React.Dispatch<React.SetStateAction<boolean>>;
//
show_remove_fav_prompt: boolean;
setShowRemoceFavPrompt: React.Dispatch<React.SetStateAction<boolean>>;
//
MATCHING_FRENZY_COUNT_DOWN_S: number;
//
LISTENING_PRACTICE_ANWERED_WAIT_S: number;
MATCHING_FRENZY_ANWERED_WAIT_S: number;
CONNECTIVES_REVISION_ANWERED_WAIT_S: number;
//
WRONG_ANS_TOAST_APPEAR_TIMEOUT_S: number;
CORRECT_ANS_TOAST_APPEAR_TIMEOUT_S: number;
}

View File

@@ -0,0 +1,8 @@
export interface ConnectiveRevisionAllResult {
[quiz_name: string]: number;
}
export interface ConnectiveRevisionResult {
date: string;
progress: number;
}

View File

@@ -0,0 +1,91 @@
import { Haptics, ImpactStyle } from '@capacitor/haptics';
import React, { createContext, ReactNode, useContext, useState } from 'react';
const MyContext = createContext<MyContextProps | undefined>(undefined);
const hapticsImpactMedium = async () => {
await Haptics.impact({ style: ImpactStyle.Medium });
};
const hapticsImpactLight = async () => {
await Haptics.impact({ style: ImpactStyle.Light });
};
const hapticsVibrate = async () => {
await Haptics.vibrate();
};
const hapticsSelectionStart = async () => {
await Haptics.selectionStart();
};
const hapticsSelectionChanged = async () => {
await Haptics.selectionChanged();
};
const hapticsSelectionEnd = async () => {
await Haptics.selectionEnd();
};
export const MyProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [my_context, setMyContext] = useState<string>('initial value');
const [test_first, setTestFirst] = useState<IFirst | undefined>(undefined);
const [test_second, setTestSecond] = useState<ISecond | undefined>(undefined);
const [test_third, setTestThird] = useState<IThird | undefined>(undefined);
return (
<MyContext.Provider
value={{
my_context,
setMyContext,
//
test_first,
setTestFirst,
test_second,
setTestSecond,
test_third,
setTestThird,
}}
>
{children}
</MyContext.Provider>
);
};
export const useMyContext = (): MyContextProps => {
const context = useContext(MyContext);
if (!context) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
interface MyContextProps {
my_context: string;
setMyContext: React.Dispatch<React.SetStateAction<string>>;
//
test_first: IFirst | undefined;
setTestFirst: React.Dispatch<React.SetStateAction<IFirst | undefined>>;
test_second: ISecond | undefined;
setTestSecond: React.Dispatch<React.SetStateAction<ISecond | undefined>>;
test_third: IThird | undefined;
setTestThird: React.Dispatch<React.SetStateAction<IThird | undefined>>;
}
interface IFirst {
test_s: string;
test_i: number;
}
interface ISecond {
test_s: string;
test_i: number;
content: IFirst[] | [];
}
interface IThird {
test_s: string;
test_i: number;
content: ISecond[] | [];
}

View File

@@ -0,0 +1,66 @@
import React, { createContext, ReactNode, useContext, useState } from 'react';
const MyContext = createContext<MyContextProps | undefined>(undefined);
export const MyProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [my_context, setMyContext] = useState<string>('initial value');
const [test_first, setTestFirst] = useState<IFirst | undefined>(undefined);
const [test_second, setTestSecond] = useState<ISecond | undefined>(undefined);
const [test_third, setTestThird] = useState<IThird | undefined>(undefined);
return (
<MyContext.Provider
value={{
my_context,
setMyContext,
//
test_first,
setTestFirst,
test_second,
setTestSecond,
test_third,
setTestThird,
}}
>
{children}
</MyContext.Provider>
);
};
export const useMyContext = (): MyContextProps => {
const context = useContext(MyContext);
if (!context) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
interface MyContextProps {
my_context: string;
setMyContext: React.Dispatch<React.SetStateAction<string>>;
//
test_first: IFirst | undefined;
setTestFirst: React.Dispatch<React.SetStateAction<IFirst | undefined>>;
test_second: ISecond | undefined;
setTestSecond: React.Dispatch<React.SetStateAction<ISecond | undefined>>;
test_third: IThird | undefined;
setTestThird: React.Dispatch<React.SetStateAction<IThird | undefined>>;
}
interface IFirst {
test_s: string;
test_i: number;
}
interface ISecond {
test_s: string;
test_i: number;
content: IFirst[] | [];
}
interface IThird {
test_s: string;
test_i: number;
content: ISecond[] | [];
}

View File

@@ -0,0 +1,7 @@
export interface MatchingFrezyRanking {
ranking: MatchingFrenzyResult[] | [];
}
export interface MatchingFrenzyResult {
date: string;
result: number;
}

View File

@@ -0,0 +1,138 @@
import React, { createContext, ReactNode, useContext } from 'react';
import { useMyIonStore } from './MyIonStore';
const MyIonFavoriteContext = createContext<MyContextProps | undefined>(undefined);
export const MyIonFavoriteProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { myIonStoreRead, myIonStoreWrite, user_store } = useMyIonStore();
async function myIonStoreAddFavorite(address_to_add: string) {
console.log('myIonStoreAddFavorite', address_to_add);
let current_fav = await JSON.parse(await myIonStoreRead('lesson_favorite_word'));
if (JSON.stringify(current_fav) == '{}') current_fav = [];
current_fav = [...current_fav, address_to_add];
await myIonStoreWrite('lesson_favorite_word', JSON.stringify(current_fav));
}
async function myIonStoreRemoveFavorite(address_to_remove: string) {
let current_fav = await JSON.parse(await myIonStoreRead('lesson_favorite_word'));
current_fav = current_fav.filter((address: string) => address !== address_to_remove);
await myIonStoreWrite('lesson_favorite_word', JSON.stringify(current_fav));
}
async function myIonStoreLoadFavorite() {
let current_fav = await JSON.parse(await myIonStoreRead('lesson_favorite_word'));
return current_fav ? current_fav : [];
}
async function myIonStoreFindInFavorite(string_to_search: string) {
const current_fav = await myIonStoreLoadFavorite();
return current_fav.includes(string_to_search);
}
//
const KEY_FAV_VOCBULARY = 'lesson_favorite_vocabulary';
async function myIonStoreAddFavoriteVocabulary(address_to_add: string) {
console.log('myIonStoreAddFavorite', address_to_add);
let current_fav = await JSON.parse(await myIonStoreRead(KEY_FAV_VOCBULARY));
if (JSON.stringify(current_fav) == '{}') current_fav = [];
current_fav = [...current_fav, address_to_add];
await myIonStoreWrite(KEY_FAV_VOCBULARY, JSON.stringify(current_fav));
}
async function myIonStoreRemoveFavoriteVocabulary(address_to_remove: string) {
let current_fav = await JSON.parse(await myIonStoreRead(KEY_FAV_VOCBULARY));
current_fav = current_fav.filter((address: string) => address !== address_to_remove);
await myIonStoreWrite(KEY_FAV_VOCBULARY, JSON.stringify(current_fav));
}
async function myIonStoreLoadFavoriteVocabulary(): Promise<string[]> {
let result = await myIonStoreRead(KEY_FAV_VOCBULARY);
if (result == '{}') return [];
let current_fav = JSON.parse(result);
return current_fav;
}
async function myIonStoreFindInFavoriteVocabulary(string_to_search: string): Promise<boolean> {
const current_fav = await myIonStoreLoadFavoriteVocabulary();
console.log({ current_fav });
return current_fav.includes(string_to_search);
}
//
const KEY_FAV_CONNECTIVES = 'lesson_favorite_connectives';
async function myIonStoreAddFavoriteConnectives(address_to_add: string) {
console.log('myIonStoreAddFavorite', address_to_add);
let current_fav = await JSON.parse(await myIonStoreRead(KEY_FAV_CONNECTIVES));
if (JSON.stringify(current_fav) == '{}') current_fav = [];
current_fav = [...current_fav, address_to_add];
await myIonStoreWrite(KEY_FAV_CONNECTIVES, JSON.stringify(current_fav));
}
async function myIonStoreRemoveFavoriteConnectives(address_to_remove: string) {
let current_fav = await JSON.parse(await myIonStoreRead(KEY_FAV_CONNECTIVES));
current_fav = current_fav.filter((address: string) => address !== address_to_remove);
await myIonStoreWrite(KEY_FAV_CONNECTIVES, JSON.stringify(current_fav));
}
async function myIonStoreLoadFavoriteConnectives(): Promise<string[]> {
let temp = await myIonStoreRead(KEY_FAV_CONNECTIVES);
let current_fav = await JSON.parse(temp);
return current_fav ? current_fav : [];
}
async function myIonStoreFindInFavoriteConnectives(string_to_search: string) {
const current_fav = await myIonStoreLoadFavoriteConnectives();
return current_fav.includes(string_to_search);
}
return (
<MyIonFavoriteContext.Provider
value={{
myIonStoreAddFavorite,
myIonStoreRemoveFavorite,
myIonStoreLoadFavorite,
myIonStoreFindInFavorite,
//
myIonStoreAddFavoriteVocabulary,
myIonStoreRemoveFavoriteVocabulary,
myIonStoreLoadFavoriteVocabulary,
myIonStoreFindInFavoriteVocabulary,
//
myIonStoreAddFavoriteConnectives,
myIonStoreRemoveFavoriteConnectives,
myIonStoreLoadFavoriteConnectives,
myIonStoreFindInFavoriteConnectives,
}}
>
{children}
</MyIonFavoriteContext.Provider>
);
};
export const useMyIonFavorite = (): MyContextProps => {
const context = useContext(MyIonFavoriteContext);
if (!context) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
interface MyContextProps {
myIonStoreAddFavorite: (address_to_add: string) => Promise<void>;
myIonStoreRemoveFavorite: (address_to_add: string) => Promise<void>;
myIonStoreLoadFavorite: () => Promise<[]>;
myIonStoreFindInFavorite: (string_to_search: string) => Promise<boolean>;
//
myIonStoreAddFavoriteVocabulary: (address_to_add: string) => Promise<void>;
myIonStoreRemoveFavoriteVocabulary: (address_to_add: string) => Promise<void>;
myIonStoreLoadFavoriteVocabulary: () => Promise<string[]>;
myIonStoreFindInFavoriteVocabulary: (string_to_search: string) => Promise<boolean>;
//
myIonStoreAddFavoriteConnectives: (address_to_add: string) => Promise<void>;
myIonStoreRemoveFavoriteConnectives: (address_to_add: string) => Promise<void>;
myIonStoreLoadFavoriteConnectives: () => Promise<string[]>;
myIonStoreFindInFavoriteConnectives: (string_to_search: string) => Promise<boolean>;
}

View File

@@ -0,0 +1,78 @@
import React, { createContext, ReactNode, useContext } from 'react';
import { useMyIonMetric } from '.';
import { APP_USE_TIME } from '../../constants';
const AppUseTimeContext = createContext<AppUseTimeContextProps | undefined>(undefined);
export const AppUseTimeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { myIonStoreRead, myIonStoreWrite } = useMyIonMetric();
function Helloworld() {
console.log('helloworld');
}
async function myIonMetricGetAppUseTime() {
let result = JSON.parse(await myIonStoreRead(APP_USE_TIME, { time_spent_s: 0 }));
return result;
}
async function myIonMetricResetAppUseTime() {
await myIonStoreWrite(APP_USE_TIME, JSON.stringify({ time_spent_s: 0 }));
}
async function myIonMetricIncAppUseTime(time_spent_s: number) {
let result = await myIonMetricGetAppUseTime();
await myIonStoreWrite(APP_USE_TIME, JSON.stringify({ time_spent_s: result.time_spent_s + time_spent_s }));
}
async function myIonMetricSetAppUseTime(time_spent_s: number) {
await myIonStoreWrite(APP_USE_TIME, JSON.stringify({ time_spent_s: time_spent_s }));
}
async function myIonMetricGetAppUseTimeIgnore() {
let result = JSON.parse(await myIonStoreRead(APP_USE_TIME + '_ignore', { time_spent_s: 0 }));
return result;
}
async function myIonMetricSetAppUseTimeIgnore(time_spent_s: number) {
await myIonStoreWrite(APP_USE_TIME + '_ignore', JSON.stringify({ time_spent_s: time_spent_s }));
}
return (
<AppUseTimeContext.Provider
value={{
//
Helloworld,
myIonMetricGetAppUseTime,
myIonMetricResetAppUseTime,
myIonMetricIncAppUseTime,
//
myIonMetricSetAppUseTime,
myIonMetricGetAppUseTimeIgnore,
myIonMetricSetAppUseTimeIgnore,
}}
>
{/* */}
{children}
</AppUseTimeContext.Provider>
);
};
export const useAppUseTime = (): AppUseTimeContextProps => {
const context = useContext(AppUseTimeContext);
if (!context) {
throw new Error('useAppUseTime must be used within a AppUseTimeProvider');
}
return context;
};
interface AppUseTimeContextProps {
Helloworld: () => void;
myIonMetricGetAppUseTime: () => Promise<{ time_spent_s: number }>;
myIonMetricIncAppUseTime: (time_spent_s: number) => Promise<void>;
myIonMetricResetAppUseTime: () => Promise<void>;
//
myIonMetricSetAppUseTime: (time_spent_s: number) => Promise<void>;
myIonMetricGetAppUseTimeIgnore: () => Promise<{ time_spent_s: number }>;
myIonMetricSetAppUseTimeIgnore: (time_spent_s: number) => Promise<void>;
}

View File

@@ -0,0 +1,81 @@
import React, { createContext, ReactNode, useContext } from 'react';
import { useMyIonMetric } from '.';
import { CONNECTIVES_REVISION_CORRECT_COUNT } from '../../constants';
const ConnectivesRevisionCorrectCount = createContext<ConnectivesRevisionCorrectCountProps | undefined>(undefined);
export const ConnectivesRevisionCorrectCountProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const NS_KEY_IGNORE = `${CONNECTIVES_REVISION_CORRECT_COUNT}_ignore`;
const { myIonStoreRead, myIonStoreWrite } = useMyIonMetric();
function Helloworld() {
console.log('helloworld ConnectivesRevisionCorrectCountProvider');
}
async function myIonMetricGetConnectivesRevisionCorrectCount() {
let result = JSON.parse(await myIonStoreRead(CONNECTIVES_REVISION_CORRECT_COUNT, { count: 0 }));
return result;
}
async function myIonMetricResetConnectivesRevisionCorrectCount() {
await myIonStoreWrite(CONNECTIVES_REVISION_CORRECT_COUNT, JSON.stringify({ count: 0 }));
}
async function myIonMetricIncConnectivesRevisionCorrectCount() {
let result = await myIonMetricGetConnectivesRevisionCorrectCount();
await myIonStoreWrite(CONNECTIVES_REVISION_CORRECT_COUNT, JSON.stringify({ count: result.count + 1 }));
}
async function myIonMetricSetConnectivesRevisionCorrectCount(count: number) {
await myIonStoreWrite(CONNECTIVES_REVISION_CORRECT_COUNT, JSON.stringify({ count: count }));
}
async function myIonMetricGetConnectivesRevisionCorrectCountIgnore() {
let result = JSON.parse(await myIonStoreRead(NS_KEY_IGNORE, { count: 0 }));
return result;
}
async function myIonMetricSetConnectivesRevisionCorrectCountIgnore(count: number) {
await myIonStoreWrite(NS_KEY_IGNORE, JSON.stringify({ count: count }));
}
return (
<ConnectivesRevisionCorrectCount.Provider
value={{
Helloworld,
// ConnectivesRevisionCorrect
myIonMetricGetConnectivesRevisionCorrectCount,
myIonMetricResetConnectivesRevisionCorrectCount,
myIonMetricIncConnectivesRevisionCorrectCount,
//
myIonMetricSetConnectivesRevisionCorrectCount,
myIonMetricGetConnectivesRevisionCorrectCountIgnore,
myIonMetricSetConnectivesRevisionCorrectCountIgnore,
}}
>
{children}
</ConnectivesRevisionCorrectCount.Provider>
);
};
export const useConnectivesRevisionCorrectCount = (): ConnectivesRevisionCorrectCountProps => {
const context = useContext(ConnectivesRevisionCorrectCount);
if (!context) {
throw new Error('useConnectivesRevisionCorrectCount must be used within a MyProvider');
}
return context;
};
interface ConnectivesRevisionCorrectCountProps {
Helloworld: () => void;
// ConnectivesRevision
myIonMetricGetConnectivesRevisionCorrectCount: () => Promise<{ count: number }>;
myIonMetricResetConnectivesRevisionCorrectCount: () => Promise<void>;
myIonMetricIncConnectivesRevisionCorrectCount: () => Promise<void>;
//
myIonMetricSetConnectivesRevisionCorrectCount: (count: number) => Promise<void>;
myIonMetricGetConnectivesRevisionCorrectCountIgnore: () => Promise<{ count: number }>;
myIonMetricSetConnectivesRevisionCorrectCountIgnore: (count: number) => Promise<void>;
}

View File

@@ -0,0 +1,77 @@
import React, { createContext, ReactNode, useContext } from 'react';
import { useMyIonMetric } from '.';
import { FULLMARK_COUNT_KEY, FULLMARK_IGNORE_COUNT } from '../../constants';
const FullmarkCountContext = createContext<FullmarkCountContextProps | undefined>(undefined);
export const FullmarkCountProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { myIonStoreRead, myIonStoreWrite } = useMyIonMetric();
function Helloworld() {
console.log('helloworld');
}
async function myIonMetricGetFullMarkCount() {
let result = JSON.parse(await myIonStoreRead(FULLMARK_COUNT_KEY, { count: 0 }));
return result;
}
async function myIonMetricResetFullMarkCount() {
await myIonStoreWrite(FULLMARK_COUNT_KEY, JSON.stringify({ count: 0 }));
}
async function myIonMetricIncFullMarkCount() {
let result = await myIonMetricGetFullMarkCount();
await myIonStoreWrite(FULLMARK_COUNT_KEY, JSON.stringify({ count: result.count + 1 }));
}
async function myIonMetricSetFullMarkCount(num: number) {
await myIonStoreWrite(FULLMARK_COUNT_KEY, JSON.stringify({ count: num }));
}
async function myIonMetricSetFullMarkIgnore(num: number) {
await myIonStoreWrite(FULLMARK_IGNORE_COUNT, JSON.stringify({ count: num }));
}
async function myIonMetricGetFullMarkIgnore() {
let result = JSON.parse(await myIonStoreRead(FULLMARK_IGNORE_COUNT, { count: 0 }));
return result;
}
return (
<FullmarkCountContext.Provider
value={{
Helloworld,
myIonMetricGetFullMarkCount,
myIonMetricResetFullMarkCount,
myIonMetricIncFullMarkCount,
//
myIonMetricSetFullMarkCount,
myIonMetricSetFullMarkIgnore,
myIonMetricGetFullMarkIgnore,
}}
>
{/* */}
{children}
</FullmarkCountContext.Provider>
);
};
export const useFullmarkCount = (): FullmarkCountContextProps => {
const context = useContext(FullmarkCountContext);
if (!context) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
interface FullmarkCountContextProps {
Helloworld: () => void;
//
// FullMark
myIonMetricGetFullMarkCount: () => Promise<{ count: number }>;
myIonMetricResetFullMarkCount: () => Promise<void>;
myIonMetricIncFullMarkCount: () => Promise<void>;
//
myIonMetricSetFullMarkCount: (num: number) => Promise<void>;
myIonMetricSetFullMarkIgnore: (num: number) => Promise<void>;
myIonMetricGetFullMarkIgnore: () => Promise<{ count: number }>;
}

View File

@@ -0,0 +1,5 @@
function Helloworld() {
console.log('helloworld');
}
export { Helloworld };

View File

@@ -0,0 +1,88 @@
import React, { createContext, ReactNode, useContext } from 'react';
import { useMyIonMetric } from '.';
import { LISTENING_PRACTICE_TIME_SPENT } from '../../constants';
const ListenPracticeTimeSpent = createContext<ListenPracticeTimeSpentProps | undefined>(undefined);
export const ListeningPracticeTimeSpentProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
// TODO create namespace for LISTENING_PRACTICE_TIME_SPENT ignore
const { myIonStoreRead, myIonStoreWrite } = useMyIonMetric();
function Helloworld() {
console.log('helloworld ListeningPracticeTimeSpentProvider');
}
async function myIonMetricGetListeningPracticeTimeSpent() {
let result = JSON.parse(await myIonStoreRead(LISTENING_PRACTICE_TIME_SPENT, { time_spent_s: 0 }));
return result;
}
async function myIonMetricResetListeningPracticeTimeSpent() {
await myIonStoreWrite(LISTENING_PRACTICE_TIME_SPENT, JSON.stringify({ time_spent_s: 0 }));
}
async function myIonMetricIncListeningPracticeTimeSpent(time_spent_s: number) {
let result = await myIonMetricGetListeningPracticeTimeSpent();
await myIonStoreWrite(
LISTENING_PRACTICE_TIME_SPENT,
JSON.stringify({ time_spent_s: result.time_spent_s + time_spent_s }),
);
}
async function myIonMetricSetListeningPracticeProgress(num: number) {
await myIonStoreWrite(
//
LISTENING_PRACTICE_TIME_SPENT,
JSON.stringify({ time_spent_s: num }),
);
}
async function myIonMetricGetListeningPracticeProgressIgnore() {
let result = JSON.parse(await myIonStoreRead(LISTENING_PRACTICE_TIME_SPENT + '_ignore', { time_spent_s: 0 }));
return result;
}
async function myIonMetricSetListeningPracticeProgressIgnore(time_spent_s: number) {
await myIonStoreWrite(LISTENING_PRACTICE_TIME_SPENT + '_ignore', JSON.stringify({ time_spent_s: time_spent_s }));
}
return (
<ListenPracticeTimeSpent.Provider
value={{
Helloworld,
myIonMetricGetListeningPracticeTimeSpent,
myIonMetricIncListeningPracticeTimeSpent,
myIonMetricResetListeningPracticeTimeSpent,
//
myIonMetricSetListeningPracticeProgress,
myIonMetricGetListeningPracticeProgressIgnore,
myIonMetricSetListeningPracticeProgressIgnore,
}}
>
{/* */}
{children}
</ListenPracticeTimeSpent.Provider>
);
};
export const useListeningPracticeTimeSpent = (): ListenPracticeTimeSpentProps => {
const context = useContext(ListenPracticeTimeSpent);
if (!context) {
throw new Error('useListenPracticeTimeSpent must be used within a MyProvider');
}
return context;
};
interface ListenPracticeTimeSpentProps {
Helloworld: () => void;
//
// ListeningPractice
myIonMetricGetListeningPracticeTimeSpent: () => Promise<{ time_spent_s: number }>;
myIonMetricIncListeningPracticeTimeSpent: (time_spent_s: number) => Promise<void>;
myIonMetricResetListeningPracticeTimeSpent: () => Promise<void>;
//
myIonMetricSetListeningPracticeProgress: (time_spent_s: number) => Promise<void>;
myIonMetricGetListeningPracticeProgressIgnore: () => Promise<{ time_spent_s: number }>;
myIonMetricSetListeningPracticeProgressIgnore: (time_spent_s: number) => Promise<void>;
}

View File

@@ -0,0 +1,79 @@
import React, { createContext, ReactNode, useContext } from 'react';
import { useMyIonMetric } from '.';
import { MATCHING_FRENZY_CORRECT_COUNT } from '../../constants';
const MatchingFrenzyCorrectCountContext = createContext<MatchingFrenzyCorrectCountContextProps | undefined>(undefined);
export const MatchingFrenzyCorrectCountProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { myIonStoreRead, myIonStoreWrite } = useMyIonMetric();
function Helloworld() {
console.log('helloworld MatchingFrenzyCorrectCountProvider');
}
async function myIonMetricGetMatchingFrenzyCorrectCount() {
let result = JSON.parse(await myIonStoreRead(MATCHING_FRENZY_CORRECT_COUNT, { count: 0 }));
return result;
}
async function myIonMetricResetMatchingFrenzyCorrectCount() {
await myIonStoreWrite(MATCHING_FRENZY_CORRECT_COUNT, JSON.stringify({ count: 0 }));
}
async function myIonMetricIncMatchingFrenzyCorrectCount() {
let result = await myIonMetricGetMatchingFrenzyCorrectCount();
await myIonStoreWrite(MATCHING_FRENZY_CORRECT_COUNT, JSON.stringify({ count: result.count + 1 }));
}
async function myIonMetricSetMatchingFrenzyCorrectCount(count: number) {
await myIonStoreWrite(MATCHING_FRENZY_CORRECT_COUNT, JSON.stringify({ count: count }));
}
async function myIonMetricGetMatchingFrenzyCorrectCountIgnore() {
let result = JSON.parse(await myIonStoreRead(`${MATCHING_FRENZY_CORRECT_COUNT}_ignore`, { count: 0 }));
return result;
}
async function myIonMetricSetMatchingFrenzyCorrectCountIgnore(count: number) {
await myIonStoreWrite(`${MATCHING_FRENZY_CORRECT_COUNT}_ignore`, JSON.stringify({ count: count }));
}
return (
<MatchingFrenzyCorrectCountContext.Provider
value={{
Helloworld,
// MatchingFrenzyCorrect
myIonMetricGetMatchingFrenzyCorrectCount,
myIonMetricResetMatchingFrenzyCorrectCount,
myIonMetricIncMatchingFrenzyCorrectCount,
//
myIonMetricSetMatchingFrenzyCorrectCount,
myIonMetricGetMatchingFrenzyCorrectCountIgnore,
myIonMetricSetMatchingFrenzyCorrectCountIgnore,
}}
>
{children}
</MatchingFrenzyCorrectCountContext.Provider>
);
};
export const useMatchingFrenzyCorrectCount = (): MatchingFrenzyCorrectCountContextProps => {
const context = useContext(MatchingFrenzyCorrectCountContext);
if (!context) {
throw new Error('useMatchingFrenzyCorrectCountContext must be used within a MyProvider');
}
return context;
};
interface MatchingFrenzyCorrectCountContextProps {
Helloworld: () => void;
// MatchingFrenzyCorrect
myIonMetricGetMatchingFrenzyCorrectCount: () => Promise<{ count: number }>;
myIonMetricResetMatchingFrenzyCorrectCount: () => Promise<void>;
myIonMetricIncMatchingFrenzyCorrectCount: () => Promise<void>;
//
myIonMetricSetMatchingFrenzyCorrectCount: (count: number) => Promise<void>;
myIonMetricGetMatchingFrenzyCorrectCountIgnore: () => Promise<{ count: number }>;
myIonMetricSetMatchingFrenzyCorrectCountIgnore: (count: number) => Promise<void>;
}

View File

@@ -0,0 +1,75 @@
import { Drivers, Storage } from '@ionic/storage';
import React, { createContext, ReactNode, useContext, useEffect } from 'react';
import { AppOnRecorderProvider } from '../AppOnRecorder';
import { AppUseTimeProvider } from './AppUseTime';
import { ConnectivesRevisionCorrectCountProvider } from './ConnectivesRevisionCorrectCount';
import { FullmarkCountProvider } from './FullmarkCount';
import { Helloworld } from './Helloworld';
import { ListeningPracticeTimeSpentProvider } from './ListeningPracticeTimeSpent';
import { MatchingFrenzyCorrectCountProvider } from './MatchingFrenzyCorrectCount';
const metric_store = new Storage({
name: 'app_metrics',
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage],
});
const MyContext = createContext<MyContextProps | undefined>(undefined);
export const MyIonMetricProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
async function myIonStoreRead(key: string, not_found_output: any = {}) {
const output = (await metric_store.get(key)) || JSON.stringify(not_found_output);
return output;
}
async function myIonStoreWrite(key: string, value: string) {
await metric_store.set(key, value);
}
useEffect(() => {
(async () => {
await metric_store.create();
})();
}, []);
return (
<MyContext.Provider
value={{
myIonStoreRead,
myIonStoreWrite,
//
Helloworld,
}}
>
<AppUseTimeProvider>
<AppOnRecorderProvider />
<FullmarkCountProvider>
<ListeningPracticeTimeSpentProvider>
<MatchingFrenzyCorrectCountProvider>
<ConnectivesRevisionCorrectCountProvider>
{/* */}
{children}
</ConnectivesRevisionCorrectCountProvider>
</MatchingFrenzyCorrectCountProvider>
</ListeningPracticeTimeSpentProvider>
</FullmarkCountProvider>
</AppUseTimeProvider>
</MyContext.Provider>
);
};
export const useMyIonMetric = (): MyContextProps => {
const context = useContext(MyContext);
if (!context) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
interface MyContextProps {
myIonStoreRead: (key: string, not_found_output: any) => Promise<any>;
myIonStoreWrite: (key: string, value: string) => Promise<void>;
//
//
Helloworld: () => void;
}

View File

@@ -0,0 +1,156 @@
import React, { createContext, ReactNode, useContext, useState } from 'react';
import { MATCH_FRENZY_SCOREBOARD_KEY } from '../constants';
import IListeningPracticeQuestion from '../interfaces/IListeningPracticeQuestion';
import { ConnectiveRevisionAllResult } from './ConnectiveRevisionRanking';
import { MatchingFrenzyResult, MatchingFrezyRanking } from './MatchingFrezyRanking';
import { useMyIonStore } from './MyIonStore';
const MyIonQuizContext = createContext<MyContextProps | undefined>(undefined);
export const MyIonQuizProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [my_context, setMyContext] = useState<string>('initial value');
const { myIonStoreRead, myIonStoreWrite } = useMyIonStore();
function Helloworld() {
listening_practice_correction_list;
}
const [listening_practice_result, setListeningPracticeResult] = useState<number>(0);
const [listening_practice_current_test, setListeningPracticeCurrentTest] = useState<number>(0);
const [listening_practice_correction_list, setListeningPracticeCorrectionList] = useState<any[] | []>([]);
//
function appendToListeningPracticeCorrectionList(question: IListeningPracticeQuestion) {
setListeningPracticeCorrectionList([...listening_practice_correction_list, question]);
}
//
function resetListeningPracticeCorrectionList() {
setListeningPracticeCorrectionList([]);
}
//
const [matching_frenzy_result, setMatchingFrenzyResult] = useState<number>(0);
const [matching_frenzy_current_test, setMatchingFrenzyCurrentTest] = useState<number>(0);
//
const getCategoryKey = (category: string) => `${MATCH_FRENZY_SCOREBOARD_KEY}/${category}`;
//
async function loadMatchingFrenzyScoreBoard(cat_name: string): Promise<MatchingFrezyRanking> {
let category_key = getCategoryKey(cat_name);
let current_result = JSON.parse(await myIonStoreRead(category_key));
if (!current_result || JSON.stringify(current_result) == '{}') {
current_result = { ranking: [] };
}
return current_result;
}
async function saveMatchingFrenzyResultToScoreBoard(cat_name: string, result: MatchingFrenzyResult) {
let category_key = getCategoryKey(cat_name);
let current_result: MatchingFrezyRanking = await loadMatchingFrenzyScoreBoard(cat_name);
current_result['ranking'] = [...current_result['ranking'], result];
current_result['ranking'].sort((a: MatchingFrenzyResult, b: MatchingFrenzyResult) => b.result - a.result).splice(3);
myIonStoreWrite(category_key, JSON.stringify(current_result));
}
//
const [connective_revision_progress, setConnectiveRevisionProgress] = useState<number>(0);
const [connective_revision_current_test, setConnectiveRevisionCurrentTest] = useState<number>(0);
//
const CONNECT_REVISION_SCOREBOARD_KEY = 'connective_revision_scoreboard';
async function loadConnectiveRevisionScoreBoard(): Promise<ConnectiveRevisionAllResult> {
let current_result = JSON.parse(await myIonStoreRead(CONNECT_REVISION_SCOREBOARD_KEY));
if (!current_result || JSON.stringify(current_result) == '{}') {
current_result = {};
}
return current_result;
}
const [connective_revision_score, setConnectiveRevisionScore] = useState<number>(0);
async function saveConnectiveRevisionResultToScoreBoard(quiz_index: string, progress: number) {
let current_result: ConnectiveRevisionAllResult = await loadConnectiveRevisionScoreBoard();
current_result = { ...current_result, [quiz_index]: progress };
myIonStoreWrite(CONNECT_REVISION_SCOREBOARD_KEY, JSON.stringify(current_result));
}
return (
<MyIonQuizContext.Provider
value={{
my_context,
setMyContext,
//
listening_practice_current_test,
setListeningPracticeCurrentTest,
//
listening_practice_result,
setListeningPracticeResult,
//
listening_practice_correction_list,
setListeningPracticeCorrectionList,
appendToListeningPracticeCorrectionList,
resetListeningPracticeCorrectionList,
//
matching_frenzy_current_test,
setMatchingFrenzyCurrentTest,
loadMatchingFrenzyScoreBoard,
saveMatchingFrenzyResultToScoreBoard,
matching_frenzy_result,
setMatchingFrenzyResult,
//
connective_revision_progress,
setConnectiveRevisionProgress,
//
connective_revision_score,
setConnectiveRevisionScore,
connective_revision_current_test,
setConnectiveRevisionCurrentTest,
loadConnectiveRevisionScoreBoard,
saveConnectiveRevisionResultToScoreBoard,
//
Helloworld,
}}
>
{children}
</MyIonQuizContext.Provider>
);
};
export const useMyIonQuizContext = (): MyContextProps => {
const context = useContext(MyIonQuizContext);
if (!context) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
interface MyContextProps {
my_context: string;
setMyContext: React.Dispatch<React.SetStateAction<string>>;
//
//
listening_practice_result: number;
setListeningPracticeResult: React.Dispatch<React.SetStateAction<number>>;
//
listening_practice_current_test: number;
setListeningPracticeCurrentTest: React.Dispatch<React.SetStateAction<number>>;
//
listening_practice_correction_list: IListeningPracticeQuestion[];
setListeningPracticeCorrectionList: React.Dispatch<React.SetStateAction<IListeningPracticeQuestion[]>>;
appendToListeningPracticeCorrectionList: (question: IListeningPracticeQuestion) => void;
resetListeningPracticeCorrectionList: () => void;
//
matching_frenzy_result: number;
setMatchingFrenzyResult: React.Dispatch<React.SetStateAction<number>>;
matching_frenzy_current_test: number;
setMatchingFrenzyCurrentTest: React.Dispatch<React.SetStateAction<number>>;
loadMatchingFrenzyScoreBoard: (cat_name: string) => Promise<MatchingFrezyRanking>;
saveMatchingFrenzyResultToScoreBoard: (cat_name: string, result: MatchingFrenzyResult) => Promise<void>;
//
connective_revision_progress: number;
setConnectiveRevisionProgress: React.Dispatch<React.SetStateAction<number>>;
//
connective_revision_score: number;
setConnectiveRevisionScore: React.Dispatch<React.SetStateAction<number>>;
connective_revision_current_test: number;
setConnectiveRevisionCurrentTest: React.Dispatch<React.SetStateAction<number>>;
loadConnectiveRevisionScoreBoard: () => Promise<ConnectiveRevisionAllResult>;
saveConnectiveRevisionResultToScoreBoard: (quiz_index: string, progress: number) => Promise<void>;
//
Helloworld: () => void;
}

View File

@@ -0,0 +1,113 @@
import { Drivers, Storage } from '@ionic/storage';
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import ILesson from '../interfaces/ILesson';
import { listLessonCategories } from '../public_data/listLessonCategories';
const user_store = new Storage({
name: '__mydb',
driverOrder: [Drivers.IndexedDB, Drivers.LocalStorage],
});
const MyContext = createContext<MyContextProps | undefined>(undefined);
export const MyIonStoreProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [db, setDB] = useState<[]>([]);
const [lesson_content, setLessonContent] = useState<ILesson[] | []>([]);
const [my_context, setMyContext] = useState<string>('initial value');
const [test_first, setTestFirst] = useState<IFirst | undefined>(undefined);
const [test_second, setTestSecond] = useState<ISecond | undefined>(undefined);
const [test_third, setTestThird] = useState<IThird | undefined>(undefined);
async function myIonStoreRead(key: string, not_found_output: string = '{}'): Promise<string> {
const output = (await user_store.get(key)) || not_found_output;
return output;
}
async function myIonStoreWrite(key: string, value: string): Promise<void> {
await user_store.set(key, value);
}
useEffect(() => {
(async () => {
let cats = await listLessonCategories();
setLessonContent(cats);
})();
(async () => {
await user_store.create();
})();
}, []);
return (
<MyContext.Provider
value={{
db,
setDB,
lesson_contents: lesson_content,
setLessonContent,
user_store,
//
myIonStoreRead,
myIonStoreWrite,
//
my_context,
setMyContext,
test_first,
setTestFirst,
test_second,
setTestSecond,
test_third,
setTestThird,
}}
>
{children}
</MyContext.Provider>
);
};
export const useMyIonStore = (): MyContextProps => {
const context = useContext(MyContext);
if (!context) {
throw new Error('useMyContext must be used within a MyProvider');
}
return context;
};
interface MyContextProps {
//
db: [];
setDB: React.Dispatch<React.SetStateAction<[]>>;
lesson_contents: ILesson[] | [];
setLessonContent: React.Dispatch<React.SetStateAction<ILesson[] | []>>;
user_store: Storage;
//
myIonStoreRead: (key: string, not_found_output?: any) => Promise<string>;
myIonStoreWrite: (key: string, value: string) => Promise<void>;
//
my_context: string;
setMyContext: React.Dispatch<React.SetStateAction<string>>;
test_first: IFirst | undefined;
setTestFirst: React.Dispatch<React.SetStateAction<IFirst | undefined>>;
test_second: ISecond | undefined;
setTestSecond: React.Dispatch<React.SetStateAction<ISecond | undefined>>;
test_third: IThird | undefined;
setTestThird: React.Dispatch<React.SetStateAction<IThird | undefined>>;
}
interface IFirst {
test_s: string;
test_i: number;
}
interface ISecond {
test_s: string;
test_i: number;
content: IFirst[] | [];
}
interface IThird {
test_s: string;
test_i: number;
content: ISecond[] | [];
}

View File

@@ -0,0 +1,26 @@
import { AppStateProvider } from './AppState';
import { MyIonFavoriteProvider } from './MyIonFavorite';
import { MyIonMetricProvider } from './MyIonMetric';
import { MyIonQuizProvider } from './MyIonQuiz';
import { MyIonStoreProvider } from './MyIonStore';
const ContextMeta = ({ children }: { children: React.ReactNode }) => {
return (
<>
<AppStateProvider>
<MyIonStoreProvider>
<MyIonFavoriteProvider>
<MyIonQuizProvider>
<MyIonMetricProvider>
{/* */}
{children}
</MyIonMetricProvider>
</MyIonQuizProvider>
</MyIonFavoriteProvider>
</MyIonStoreProvider>
</AppStateProvider>
</>
);
};
export default ContextMeta;

View File

@@ -0,0 +1,3 @@
.debug {
background-color: gold;
}

View File

@@ -0,0 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100..900&family=Noto+Sans+TC:wght@100..900&display=swap');
* {
font-family: 'Noto Sans TC', sans-serif;
font-optical-sizing: auto;
font-style: normal;
}

View File

@@ -0,0 +1,106 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// the translations
// (tip move them in a JSON file and import them)
const resources = {
en: {
translation: {
'Lesson': 'Lesson',
'Quiz': 'Quiz',
'Favorite': 'Favorite',
'Loading': 'Loading',
'menu.link-home': 'Home',
'menu.link-stats': 'Stats',
'home.title': 'Remove duplicate songs from your Spotify library.',
'home.description':
"Spotify Dedup cleans up your playlists and liked songs from your Spotify account. It's easy and fast.",
'home.review':
'Read what {{-supportersCount}} supporters think about Spotify Dedup on {{- linkOpen}}Buy Me a Coffee{{- linkClose}}',
'home.login-button': 'Log in with Spotify',
'meta.title': 'Spotify Dedup - Remove duplicate songs from your Spotify library',
'meta.description':
'Delete repeated songs from your Spotify playlists and liked songs automatically. Fix your music library. Quickly and easy.',
'features.find-remove.header': 'Find & remove',
'features.find-remove.body':
'Dedup checks your playlists and liked songs in {{- strongOpen}}your Spotify library{{- strongClose}}. Once Dedup finds duplicates you can remove them on a per-playlist basis.',
'features.safer.header': 'Safer',
'features.safer.body':
'Dedup will only remove {{- strongOpen}}duplicate songs{{- strongClose}}, leaving the rest of the playlist and liked songs untouched.',
'features.open-source.header': 'Open Source',
'features.open-source.body':
"You might want to have a look at the {{- linkGithubOpen}}source code on GitHub{{- linkGithubClose}}. This web app uses the {{- linkWebApiOpen}}Spotify Web API{{- linkWebApiClose}} to manage user's playlists and liked songs.",
'reviews.title': 'This is what users are saying',
'footer.author': 'Made with ♥ by {{- linkOpen}}JMPerez 👨‍💻{{- linkClose}}',
'footer.github': 'Check out the {{- linkOpen}}code on GitHub 📃{{- linkClose}}',
'footer.bmc': 'Support the project {{- linkOpen}}buying a coffee ☕{{- linkClose}}',
'bmc.button': 'Would you buy me a coffee?',
'result.duplicate.reason-same-id': 'Duplicate',
'result.duplicate.reason-same-data': 'Duplicate (same name, artist and duration)',
'result.duplicate.track': '<0>{{trackName}}</0> <2>by</2> <4>{{trackArtistName}}</4>',
'process.status.finding': 'Finding duplicates in your playlists and liked songs…',
'process.status.complete': 'Processing complete!',
'process.status.complete.body': 'Your playlists and liked songs have been processed!',
'process.status.complete.dups.body':
'Click on the {{- strongOpen}}Remove duplicates{{- strongClose}} button to get rid of duplicates in that playlist or liked songs collection.',
'process.status.complete.nodups.body': "Congrats! You don't have duplicates in your playlists nor liked songs.",
'process.reading-library': 'Going through your library, finding the playlists you own and your liked songs…',
'process.processing_one': 'Searching for duplicate songs, wait a sec. Still to process {{count}} playlist…',
'process.processing_other': 'Searching for duplicate songs, wait a sec. Still to process {{count}} playlists…',
'process.saved.title': 'liked songs in your library',
'process.saved.duplicates_one': 'This collection has {{count}} duplicate song',
'process.saved.duplicates_other': 'This collection has {{count}} duplicate songs',
'process.saved.remove-button': 'Remove duplicates from your liked songs',
'process.playlist.duplicates_one': 'This playlist has {{count}} duplicate song',
'process.playlist.duplicates_other': 'This playlist has {{count}} duplicate songs',
'process.playlist.remove-button': 'Remove duplicates from this playlist',
'process.items.removed': 'Duplicates removed',
'faq.section-title': 'Frequently asked questions',
'faq.question-1': 'What does this web application do?',
'faq.answer-1':
'Spotify Dedup helps you clean up your music libraries on Spotify by identifying and deleting duplicate songs across playlists and liked songs.',
'faq.question-2': 'How does it find duplicates?',
'faq.answer-2':
"Dedup finds duplicates based on the songs identifier, title, artist, and duration similarity. It identifies duplicates that Spotify's application does not catch.",
'faq.question-3': "How is Dedup better than Spotify's built-in duplicate detection?",
'faq.answer-3':
"Spotify's applications only warn about duplicates when adding a song to a playlit or liked songs with the exact same song identifier. However, the same song can have multiple identifiers on Spotify that both in the same release or in several ones. Dedup detects duplicates based on title, artist, and duration similarity.",
'faq.question-4': 'When duplicates are found, which songs are removed?',
'faq.answer-4': 'Dedup will keep the first song within a group of duplicate songs, and will remove the rest.',
'faq.question-5': 'Is my data safe with this web application?',
'faq.answer-5':
'Yes, this web application does not store any user data on its servers. It only requests the minimum set of permissions necessary to process your library.',
'faq.question-6': 'What permissions does this web application require?',
'faq.answer-6':
"This web application uses Spotify's authentication service to access your liked songs and playlists in your library.",
'faq.question-7': 'How has this tool been tested?',
'faq.answer-7':
'This tool has been battle-tested by thousands of users who have used it to identify duplicates in millions of playlists since 2014.',
'faq.question-8': 'Can this tool delete duplicates across multiple playlists?',
'faq.answer-8':
"This tool can identify and delete duplicates on all playlists in a library, but doesn't detect duplicates of a song across multiple playlists.",
'faq.question-9': 'How can I revoke the permissions granted to this web application?',
'faq.answer-9':
"You can revoke the permissions granted to this web application at any time on your Spotify account, under the 'Apps' section.",
'faq.question-10': 'Does this tool work with other music streaming services?',
'faq.answer-10': "No, this tool only works with Spotify through Spotify's Web API.",
},
},
};
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
// the translations
// (tip move them in a JSON file and import them,
// or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui)
resources,
lng: 'en', // if you're using a language detector, do not define the lng option
fallbackLng: 'en',
interpolation: {
escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
},
});
export default i18n;

View File

@@ -0,0 +1,5 @@
interface ContainerProps {
name: string;
}
export default ContainerProps;

View File

@@ -0,0 +1,11 @@
import IConnectivesRevisionQuestion from './IConnectivesRevisionQuestion';
interface IConnectivesRevisionCategory {
test_s: string;
test_i: number;
cat_info: string;
cat_name: string;
content: IConnectivesRevisionQuestion[] | [];
}
export default IConnectivesRevisionCategory;

View File

@@ -0,0 +1,12 @@
interface IConnectivesRevisionQuestion {
test_s: string;
test_i: number;
image: string;
sound: string;
word_c: string;
word: string;
sample_e: string;
sample_c: string;
}
export default IConnectivesRevisionQuestion;

View File

@@ -0,0 +1,11 @@
import ILessonCategory from './ILessonCategory';
interface ILesson {
test_s: string;
test_i: number;
name: string;
path: string;
content: ILessonCategory[] | [];
}
export default ILesson;

View File

@@ -0,0 +1,11 @@
import IWordCard from './IWordCard';
interface ILessonCategory {
test_s: string;
test_i: number;
cat_info: string;
cat_name: string;
content: IWordCard[] | [];
}
export default ILessonCategory;

View File

@@ -0,0 +1,11 @@
import IListeningPracticeQuestion from './IListeningPracticeQuestion';
interface IListeningPracticeCategory {
test_s: string;
test_i: number;
cat_info: string;
cat_name: string;
content: IListeningPracticeQuestion[] | [];
}
export default IListeningPracticeCategory;

View File

@@ -0,0 +1,12 @@
interface IListeningPracticeQuestion {
test_s: string;
test_i: number;
image: string;
sound: string;
word_c: string;
word: string;
sample_e: string;
sample_c: string;
}
export default IListeningPracticeQuestion;

View File

@@ -0,0 +1,11 @@
import IMatchingFrenzyQuestion from './IMatchingFrenzyQuestion';
interface IMatchingFrenzyCategory {
test_s: string;
test_i: number;
cat_info: string;
cat_name: string;
content: IMatchingFrenzyQuestion[] | [];
}
export default IMatchingFrenzyCategory;

View File

@@ -0,0 +1,12 @@
interface IMatchingFrenzyQuestion {
test_s: string;
test_i: number;
image: string;
sound: string;
word_c: string;
word: string;
sample_e: string;
sample_c: string;
}
export default IMatchingFrenzyQuestion;

View File

@@ -0,0 +1,14 @@
interface IWordCard {
test_s: string;
test_i: number;
image: string;
sound: string;
word_c: string;
word: string;
sample_e: string;
sample_c: string;
image_url: string;
sound_url: string;
}
export default IWordCard;

View File

@@ -0,0 +1,5 @@
interface LessonCategoriesProps {
name: string;
}
export default LessonCategoriesProps;

Some files were not shown because too many files have changed in this diff Show More