``update Add QR code generation feature with dynamic sizing and styling, implement screen-width-based conditional rendering, update i18n translations, and adjust context providers structure
``
This commit is contained in:
19
002_source/ionic_mobile/package-lock.json
generated
19
002_source/ionic_mobile/package-lock.json
generated
@@ -32,6 +32,7 @@
|
|||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"pocketbase": "^0.26.0",
|
"pocketbase": "^0.26.0",
|
||||||
|
"qr-code-styling": "^1.9.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "7.50.1",
|
"react-hook-form": "7.50.1",
|
||||||
@@ -13787,6 +13788,24 @@
|
|||||||
"teleport": ">=0.2.0"
|
"teleport": ">=0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qr-code-styling": {
|
||||||
|
"version": "1.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz",
|
||||||
|
"integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"qrcode-generator": "^1.4.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode-generator": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.1",
|
"version": "6.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
|
||||||
|
@@ -43,14 +43,15 @@
|
|||||||
"@types/react-router": "^5.1.20",
|
"@types/react-router": "^5.1.20",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
"react-i18next": "^15.2.0",
|
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"pocketbase": "^0.26.0",
|
"pocketbase": "^0.26.0",
|
||||||
|
"qr-code-styling": "^1.9.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "7.50.1",
|
"react-hook-form": "7.50.1",
|
||||||
|
"react-i18next": "^15.2.0",
|
||||||
"react-markdown": "^9.0.3",
|
"react-markdown": "^9.0.3",
|
||||||
"react-router": "^5.3.4",
|
"react-router": "^5.3.4",
|
||||||
"react-router-dom": "^5.3.4",
|
"react-router-dom": "^5.3.4",
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { DEBUG, DEBUG_LINK, QUIZ_MAIN_MENU_LINK, RECORD_LINK, SETTING_LINK } from './constants';
|
import { DEBUG_LINK, isDevelop, QUIZ_MAIN_MENU_LINK, RECORD_LINK, SETTING_LINK } from './constants';
|
||||||
/* Core CSS required for Ionic components to work properly */
|
/* Core CSS required for Ionic components to work properly */
|
||||||
|
|
||||||
import '@ionic/react/css/core.css';
|
import '@ionic/react/css/core.css';
|
||||||
@@ -139,7 +139,7 @@ const TabButtons: React.FC = () => {
|
|||||||
<IonIcon aria-hidden="true" icon={settingsOutline} size="large" />
|
<IonIcon aria-hidden="true" icon={settingsOutline} size="large" />
|
||||||
</IonTabButton>
|
</IonTabButton>
|
||||||
|
|
||||||
{DEBUG ? (
|
{isDevelop ? (
|
||||||
<IonTabButton
|
<IonTabButton
|
||||||
tab="debug"
|
tab="debug"
|
||||||
onClick={() => goSwitchPage(DEBUG_LINK)}
|
onClick={() => goSwitchPage(DEBUG_LINK)}
|
||||||
|
25
002_source/ionic_mobile/src/components/Footer/index.tsx
Normal file
25
002_source/ionic_mobile/src/components/Footer/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function Footer(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100vw',
|
||||||
|
//
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '3rem',
|
||||||
|
//
|
||||||
|
fontWeight: '300',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
opacity: '0.9',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
2025 louislabs
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer;
|
@@ -90,6 +90,8 @@ export const COL_USER_METAS = 'UserMetas';
|
|||||||
//
|
//
|
||||||
export const RUNNING_PLATFORM = Capacitor.getPlatform();
|
export const RUNNING_PLATFORM = Capacitor.getPlatform();
|
||||||
|
|
||||||
|
export const isDevelop = import.meta.env.DEV;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
//
|
//
|
||||||
API_URL,
|
API_URL,
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
import QRCodeStyling from 'qr-code-styling';
|
||||||
|
import AvatarJpg from './avatar.jpg';
|
||||||
|
|
||||||
|
const qrCode = new QRCodeStyling({
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
image: AvatarJpg,
|
||||||
|
dotsOptions: {
|
||||||
|
color: '#4267b2',
|
||||||
|
type: 'rounded',
|
||||||
|
},
|
||||||
|
imageOptions: {
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
margin: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [url, setUrl] = useState(window.location.href);
|
||||||
|
const [fileExt, setFileExt] = useState('png');
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
qrCode.append(ref.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
qrCode.update({
|
||||||
|
data: url,
|
||||||
|
});
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
const onUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setUrl(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExtensionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFileExt(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div ref={ref} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,4 @@
|
|||||||
|
.App {
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
@@ -0,0 +1,43 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import QrHere from './QrHere';
|
||||||
|
import { IonText } from '@ionic/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Footer from '../../components/Footer';
|
||||||
|
|
||||||
|
export interface ProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckScreenWidth({ children }: ProviderProps): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [showQrCode, setShowQrCode] = React.useState(false);
|
||||||
|
React.useEffect(() => {
|
||||||
|
setShowQrCode(window.screen.width > 800);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showQrCode ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<QrHere />
|
||||||
|
|
||||||
|
<IonText>{t('please-use-mobile-for-better-experience')}</IonText>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>{children}</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -4,16 +4,15 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import '../i18n';
|
import '../i18n';
|
||||||
|
|
||||||
export interface I18nProviderProps {
|
export interface I18nProviderProps {
|
||||||
children: React.ReactNode;
|
|
||||||
language?: string;
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function I18nProvider({ children, language = 'en' }: I18nProviderProps): React.JSX.Element {
|
export function I18nProvider({ language = 'en' }: I18nProviderProps): React.JSX.Element {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
//
|
//
|
||||||
}, [i18n, language]);
|
}, [i18n, language]);
|
||||||
|
|
||||||
return <>{children}</>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { PocketBaseProvider } from '../hooks/usePocketBase';
|
import { PocketBaseProvider } from '../hooks/usePocketBase';
|
||||||
import { AppStateProvider } from './AppState';
|
import { AppStateProvider } from './AppState';
|
||||||
import { UserProvider } from './auth/user-context';
|
import { UserProvider } from './auth/user-context';
|
||||||
|
import { CheckScreenWidth } from './CheckScreenWidth';
|
||||||
import { I18nProvider } from './I18nProvider';
|
import { I18nProvider } from './I18nProvider';
|
||||||
import { MyIonFavoriteProvider } from './MyIonFavorite';
|
import { MyIonFavoriteProvider } from './MyIonFavorite';
|
||||||
import { MyIonMetricProvider } from './MyIonMetric';
|
import { MyIonMetricProvider } from './MyIonMetric';
|
||||||
@@ -13,7 +14,8 @@ const queryClient = new QueryClient();
|
|||||||
const ContextMeta = ({ children }: { children: React.ReactNode }) => {
|
const ContextMeta = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<I18nProvider>
|
<I18nProvider></I18nProvider>
|
||||||
|
<CheckScreenWidth>
|
||||||
<AppStateProvider>
|
<AppStateProvider>
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<MyIonStoreProvider>
|
<MyIonStoreProvider>
|
||||||
@@ -32,7 +34,7 @@ const ContextMeta = ({ children }: { children: React.ReactNode }) => {
|
|||||||
</MyIonStoreProvider>
|
</MyIonStoreProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</AppStateProvider>
|
</AppStateProvider>
|
||||||
</I18nProvider>
|
</CheckScreenWidth>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -2,9 +2,10 @@
|
|||||||
// (tip move them in a JSON file and import them)
|
// (tip move them in a JSON file and import them)
|
||||||
const en = {
|
const en = {
|
||||||
translation: {
|
translation: {
|
||||||
hello: 'world',
|
'hello': 'world',
|
||||||
Loading: 'loading',
|
'Loading': 'loading',
|
||||||
lesson: 'lesson',
|
'lesson': 'lesson',
|
||||||
|
'please-use-mobile-for-better-experience': 'Please use mobile for better experience',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
import en from './en';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
en,
|
|
||||||
};
|
|
@@ -2,9 +2,10 @@
|
|||||||
// (tip move them in a JSON file and import them)
|
// (tip move them in a JSON file and import them)
|
||||||
const zh = {
|
const zh = {
|
||||||
translation: {
|
translation: {
|
||||||
hello: '你好',
|
'hello': '你好',
|
||||||
Loading: 'loading',
|
'Loading': '加載中',
|
||||||
lesson: 'lesson',
|
'lesson': '課程',
|
||||||
|
'please-use-mobile-for-better-experience': '為了更好的體驗,請使用手機瀏覽 😊',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user