mirror of
https://github.com/Vikeo/LifeTrinket.git
synced 2025-11-11 13:46:21 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c522189f2 | ||
|
|
df3c30da31 | ||
|
|
5fff18e079 | ||
|
|
7203f0170a | ||
|
|
63fbceafe2 | ||
|
|
2ec9998a2a |
@@ -1,12 +1,12 @@
|
||||
index.html,1716462527631,3daeb4b4b2f195883f1e266f94c16156ee3c60a29c3eb8c44a8dcfdbb1fa0a03
|
||||
manifest.webmanifest,1716462527631,10e89b44378da695cb672bf7d801a4ade909383751f1665416f561bbe1434e5d
|
||||
manifest.json,1716462527513,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a
|
||||
logo192.png,1716462527512,14ac21c3975e11951c1eb7793eec18e1cc3274bfe7cf7858636d547a9a4efc1c
|
||||
registerSW.js,1716462527631,8db45a5ae8765ce12ec241d6c5bd5d30eb81dd9163b2685c5e1b867a0e487018
|
||||
robots.txt,1716462527513,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
|
||||
sw.js,1716462528602,b681e1b343578a0de67032920144d05430677997580429b7e2b749f6afff69ed
|
||||
workbox-3e911b1d.js,1716462528602,666146b896084273226c83dca0b93f99accb195688330d6aa5c8c570bd48a4ac
|
||||
assets/index-D9CdzROR.css,1716462527631,610c77754d47a35446b00c0a50488070c943f3a05e6d57658faefd943bc3fc46
|
||||
favicon.ico,1716462527511,c3d2b7ac7f6263cca7ee26f91725eb32e7539bf0564f3b31a1bfc23cc88e9739
|
||||
logo512.png,1716462527512,a9ebde1252bb76a5b474130ef07a7ed744448fde84221f715f3fec849eccbcd2
|
||||
assets/index-DgCoW5us.js,1716462527631,06a6d92ff20d7f9e1f5e0c4d3ad8f931d7d0636f109b5e2dbbe28abd3707bb50
|
||||
index.html,1716800495173,a60a0d61bc25aa1eb4446d628875d96224860fbd767a11682a6c1db79b9403e2
|
||||
manifest.webmanifest,1716800495173,10e89b44378da695cb672bf7d801a4ade909383751f1665416f561bbe1434e5d
|
||||
manifest.json,1716800495051,91ce94afb71f33a477f5d8d48c3f98bd7de422279c74f17b6500eec72003ac1a
|
||||
registerSW.js,1716800495173,8db45a5ae8765ce12ec241d6c5bd5d30eb81dd9163b2685c5e1b867a0e487018
|
||||
robots.txt,1716800495051,391d14b3c2f8c9143a27a28c7399585142228d4d1bdbe2c87ac946de411fa9a2
|
||||
sw.js,1716800496019,609c6e27a6431ece2081013efab535a218046977d97b8600a6f58ba7589373c1
|
||||
logo192.png,1716800495050,14ac21c3975e11951c1eb7793eec18e1cc3274bfe7cf7858636d547a9a4efc1c
|
||||
workbox-3e911b1d.js,1716800496020,666146b896084273226c83dca0b93f99accb195688330d6aa5c8c570bd48a4ac
|
||||
assets/index-B0S3b36T.css,1716800495173,1eb1cb3d1dacc339354071ee052cdacc07d1c831c61e08b082518436f3463d83
|
||||
favicon.ico,1716800495050,c3d2b7ac7f6263cca7ee26f91725eb32e7539bf0564f3b31a1bfc23cc88e9739
|
||||
logo512.png,1716800495051,a9ebde1252bb76a5b474130ef07a7ed744448fde84221f715f3fec849eccbcd2
|
||||
assets/index-2MMQ0HyH.js,1716800495173,f14516d3e15bb8fa5079d40fb7c1a7e0974d336340936bbed066d792ee1021e5
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "life-trinket",
|
||||
"private": true,
|
||||
"version": "0.9.95",
|
||||
"version": "0.9.96",
|
||||
"type": "commonjs",
|
||||
"engines": {
|
||||
"node": ">=20",
|
||||
@@ -21,6 +21,7 @@
|
||||
"ga-4-react": "^0.1.281",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-screen-wake-lock": "^3.0.2",
|
||||
"react-swipeable": "^7.0.1",
|
||||
"react-twc": "^1.3.0",
|
||||
|
||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-router-dom:
|
||||
specifier: ^6.23.1
|
||||
version: 6.23.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react-screen-wake-lock:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(react@18.2.0)
|
||||
@@ -1317,6 +1320,10 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@remix-run/router@1.16.1':
|
||||
resolution: {integrity: sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
'@rollup/plugin-babel@5.3.1':
|
||||
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -4024,6 +4031,19 @@ packages:
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-router-dom@6.23.1:
|
||||
resolution: {integrity: sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
react-dom: '>=16.8'
|
||||
|
||||
react-router@6.23.1:
|
||||
resolution: {integrity: sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
|
||||
react-screen-wake-lock@3.0.2:
|
||||
resolution: {integrity: sha512-f88vcfMG1AWYRSIWQ5Qx5YVboH6TSL0F4ZlFLERZp6aKiZRGVRAAJ9wedJdO5jqTMcCDZ4OXJ8PjcSkDmvGSBg==}
|
||||
engines: {node: '>=14.16'}
|
||||
@@ -6359,6 +6379,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.1
|
||||
|
||||
'@remix-run/router@1.16.1': {}
|
||||
|
||||
'@rollup/plugin-babel@5.3.1(@babel/core@7.24.5)(rollup@2.79.1)':
|
||||
dependencies:
|
||||
'@babel/core': 7.24.5
|
||||
@@ -9456,6 +9478,18 @@ snapshots:
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-router-dom@6.23.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@remix-run/router': 1.16.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-router: 6.23.1(react@18.2.0)
|
||||
|
||||
react-router@6.23.1(react@18.2.0):
|
||||
dependencies:
|
||||
'@remix-run/router': 1.16.1
|
||||
react: 18.2.0
|
||||
|
||||
react-screen-wake-lock@3.0.2(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
|
||||
40
src/App.tsx
40
src/App.tsx
@@ -1,14 +1,42 @@
|
||||
import { LifeTrinket } from './Components/LifeTrinket';
|
||||
import { twc } from 'react-twc';
|
||||
import { EmergencyResetButton } from './Components/LifeTrinket';
|
||||
import { Play } from './Components/Views/Play';
|
||||
import { GlobalSettingsProvider } from './Providers/GlobalSettingsProvider';
|
||||
import { PlayersProvider } from './Providers/PlayersProvider';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import StartMenu from './Components/Views/StartMenu/StartMenu';
|
||||
|
||||
const PlayWrapper = twc.div`relative z-0 max-w-fit max-h-fit portrait:rotate-90`;
|
||||
|
||||
const StartWrapper = twc.div`max-w-fit max-h-fit`;
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<PlayersProvider>
|
||||
<GlobalSettingsProvider>
|
||||
<LifeTrinket />
|
||||
</GlobalSettingsProvider>
|
||||
</PlayersProvider>
|
||||
<BrowserRouter>
|
||||
<PlayersProvider>
|
||||
<GlobalSettingsProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
Component={() => (
|
||||
<StartWrapper>
|
||||
<StartMenu />
|
||||
</StartWrapper>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/play"
|
||||
Component={() => (
|
||||
<PlayWrapper>
|
||||
<Play />
|
||||
<EmergencyResetButton />
|
||||
</PlayWrapper>
|
||||
)}
|
||||
/>
|
||||
</Routes>
|
||||
</GlobalSettingsProvider>
|
||||
</PlayersProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { twc } from 'react-twc';
|
||||
import { useAnalytics } from '../../Hooks/useAnalytics';
|
||||
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
|
||||
import { PreStartMode } from '../../Types/Settings';
|
||||
import { InstallPWAButton } from '../Misc/InstallPWAButton';
|
||||
import { Separator } from '../Misc/Separator';
|
||||
import { Paragraph } from '../Misc/TextComponents';
|
||||
import { ToggleButton } from '../Misc/ToggleButton';
|
||||
@@ -201,25 +202,25 @@ export const SettingsDialog = ({
|
||||
is enabled.
|
||||
</Description>
|
||||
</SettingContainer>
|
||||
<SettingContainer>
|
||||
<ToggleContainer>
|
||||
<label>
|
||||
Fullscreen on start <span className="text-xs">(Android only)</span>
|
||||
</label>
|
||||
<ToggleButton
|
||||
checked={settings.goFullscreenOnStart}
|
||||
onChange={() => {
|
||||
setSettings({
|
||||
...settings,
|
||||
goFullscreenOnStart: !settings.goFullscreenOnStart,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</ToggleContainer>
|
||||
<Description>
|
||||
Will enter fullscreen mode when starting a game if this is enabled.
|
||||
</Description>
|
||||
</SettingContainer>
|
||||
{(!window.isIOS || window.isIPad) && (
|
||||
<SettingContainer>
|
||||
<ToggleContainer>
|
||||
<label>Fullscreen on start</label>
|
||||
<ToggleButton
|
||||
checked={settings.goFullscreenOnStart}
|
||||
onChange={() => {
|
||||
setSettings({
|
||||
...settings,
|
||||
goFullscreenOnStart: !settings.goFullscreenOnStart,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</ToggleContainer>
|
||||
<Description>
|
||||
Will enter fullscreen mode when starting a game if this is enabled.
|
||||
</Description>
|
||||
</SettingContainer>
|
||||
)}
|
||||
|
||||
<SettingContainer>
|
||||
<ToggleContainer>
|
||||
@@ -255,21 +256,45 @@ export const SettingsDialog = ({
|
||||
</div>
|
||||
{!isPWA && (
|
||||
<>
|
||||
<Separator height="1px" />
|
||||
<SettingContainer>
|
||||
<ToggleContainer>
|
||||
<Paragraph>
|
||||
<b>Tip:</b> You can{' '}
|
||||
<b>add this webapp to your home page on iOS</b> or{' '}
|
||||
<b>install it on Android</b> to have it act just like a normal
|
||||
app!
|
||||
</Paragraph>
|
||||
</ToggleContainer>
|
||||
<Description className="mt-1">
|
||||
If you do, this app will work offline and the toolbar will be
|
||||
automatically hidden.
|
||||
</Description>
|
||||
</SettingContainer>
|
||||
{window.isIOS && (
|
||||
<>
|
||||
<Separator height="1px" />
|
||||
<SettingContainer>
|
||||
<ToggleContainer>
|
||||
<Paragraph>
|
||||
<b>Tip:</b> You can <b>add this webapp to your home page</b>{' '}
|
||||
to have it act just like a normal app!
|
||||
</Paragraph>
|
||||
</ToggleContainer>
|
||||
<Description className="mt-1">
|
||||
If you do, this web app will work offline and the toolbar will
|
||||
be automatically hidden.
|
||||
</Description>
|
||||
</SettingContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!window.isIOS && (
|
||||
<>
|
||||
<Separator height="1px" />
|
||||
<SettingContainer>
|
||||
<ToggleContainer>
|
||||
<Paragraph>
|
||||
<b>Tip:</b> You can <b>install this page as a PWA</b> to
|
||||
have it act just like a normal app!
|
||||
</Paragraph>
|
||||
</ToggleContainer>
|
||||
<Description className="mt-1">
|
||||
If you do, this web app will work offline and the toolbar will
|
||||
be automatically hidden. PWA stands for Progressive Web
|
||||
Application
|
||||
</Description>
|
||||
</SettingContainer>
|
||||
<div className="flex w-full justify-center">
|
||||
<InstallPWAButton />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Separator height="1px" />
|
||||
|
||||
@@ -7,7 +7,7 @@ const StartWrapper = twc.div`max-w-fit max-h-fit`;
|
||||
|
||||
const PlayWrapper = twc.div`relative z-0 max-w-fit max-h-fit portrait:rotate-90`;
|
||||
|
||||
const EmergencyResetButton = () => {
|
||||
export const EmergencyResetButton = () => {
|
||||
const { goToStart } = useGlobalSettings();
|
||||
|
||||
const EmergencyResetButton = twc.button`w-[100dvmax] h-[100dvmin] absolute top-0 z-[-1] bg-background-default`;
|
||||
|
||||
45
src/Components/Misc/InstallPWAButton.tsx
Normal file
45
src/Components/Misc/InstallPWAButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { BeforeInstallPromptEvent } from '../../global';
|
||||
import { useAnalytics } from '../../Hooks/useAnalytics';
|
||||
|
||||
export const InstallPWAButton = () => {
|
||||
const supportsPWARef = useRef<boolean>(false);
|
||||
const [promptInstall, setPromptInstall] =
|
||||
useState<BeforeInstallPromptEvent | null>(null);
|
||||
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const handler = (e: BeforeInstallPromptEvent) => {
|
||||
e.preventDefault();
|
||||
supportsPWARef.current = true;
|
||||
setPromptInstall(e);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('beforeinstallprompt', handler);
|
||||
|
||||
return () => window.removeEventListener('transitionend', handler);
|
||||
}, []);
|
||||
|
||||
if (!supportsPWARef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="mt-1 mb-1 bg-primary-main px-3 py-1 rounded-md duration-200 ease-in-out shadow-[1px_2px_4px_0px_rgba(0,0,0,0.3)] hover:bg-primary-dark font-bold"
|
||||
aria-label="Install app"
|
||||
title="Install app"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (!promptInstall) {
|
||||
return;
|
||||
}
|
||||
analytics.trackEvent('install_pwa_prompt_shown');
|
||||
promptInstall.prompt();
|
||||
}}
|
||||
>
|
||||
Install as a PWA
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -347,30 +347,35 @@ const PlayerMenu = ({
|
||||
>
|
||||
<Exit size={iconSize} style={{ rotate: '180deg' }} />
|
||||
</button>
|
||||
<div
|
||||
data-fullscreen={document.fullscreenElement ? true : false}
|
||||
className="flex
|
||||
{(!window.isIOS || window.isIPad) && (
|
||||
<div
|
||||
data-fullscreen={document.fullscreenElement ? true : false}
|
||||
className="flex
|
||||
data-[fullscreen=true]:bg-secondary-dark rounded-lg border border-transparent
|
||||
data-[fullscreen=true]:border-primary-main"
|
||||
>
|
||||
<IconCheckbox
|
||||
className="p-1"
|
||||
name="fullscreen"
|
||||
checked={document.fullscreenElement ? true : false}
|
||||
icon={
|
||||
<FullscreenOff
|
||||
size={iconSize}
|
||||
className="text-primary-main"
|
||||
/>
|
||||
}
|
||||
checkedIcon={
|
||||
<FullscreenOn size={iconSize} className="text-primary-main" />
|
||||
}
|
||||
onChange={toggleFullscreen}
|
||||
aria-checked={document.fullscreenElement ? true : false}
|
||||
aria-label="Fullscreen"
|
||||
/>
|
||||
</div>
|
||||
>
|
||||
<IconCheckbox
|
||||
className="p-1"
|
||||
name="fullscreen"
|
||||
checked={document.fullscreenElement ? true : false}
|
||||
icon={
|
||||
<FullscreenOff
|
||||
size={iconSize}
|
||||
className="text-primary-main"
|
||||
/>
|
||||
}
|
||||
checkedIcon={
|
||||
<FullscreenOn
|
||||
size={iconSize}
|
||||
className="text-primary-main"
|
||||
/>
|
||||
}
|
||||
onChange={toggleFullscreen}
|
||||
aria-checked={document.fullscreenElement ? true : false}
|
||||
aria-label="Fullscreen"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
data-wake-lock-active={settings.keepAwake}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { usePlayers } from '../../Hooks/usePlayers';
|
||||
import { Orientation, PreStartMode } from '../../Types/Settings';
|
||||
import { Players } from '../Players/Players';
|
||||
import { PreStart } from '../PreStartGame/PreStart';
|
||||
import { createInitialPlayers } from '../../Data/getInitialPlayers';
|
||||
|
||||
const MainWrapper = twc.div`w-[100dvmax] h-[100dvmin] overflow-hidden, setPlayers`;
|
||||
|
||||
@@ -94,6 +95,12 @@ export const Play = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Fall back to creating players if there are none, in the case where someone's first visit to the
|
||||
// site is to the /play route
|
||||
if (players.length <= 0) {
|
||||
setPlayers(createInitialPlayers(initialGameSettings));
|
||||
}
|
||||
|
||||
if (
|
||||
players.length > 1 &&
|
||||
!preStartCompleted &&
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { twc } from 'react-twc';
|
||||
import { createInitialPlayers } from '../../../Data/getInitialPlayers';
|
||||
import { useAnalytics } from '../../../Hooks/useAnalytics';
|
||||
@@ -49,6 +50,7 @@ export const LabelText = twc.div`text-md text-text-primary font-medium`;
|
||||
let tracked = false;
|
||||
|
||||
const Start = () => {
|
||||
const navigate = useNavigate();
|
||||
const { setPlayers } = usePlayers();
|
||||
const analytics = useAnalytics();
|
||||
const {
|
||||
@@ -186,6 +188,10 @@ const Start = () => {
|
||||
setShowPlay(true);
|
||||
setPlaying(false);
|
||||
tracked = false;
|
||||
|
||||
console.log('haha');
|
||||
|
||||
navigate('/play');
|
||||
};
|
||||
|
||||
const doResumeGame = () => {
|
||||
@@ -218,6 +224,8 @@ const Start = () => {
|
||||
setShowPlay(true);
|
||||
setPlaying(true);
|
||||
tracked = false;
|
||||
|
||||
navigate('/play');
|
||||
};
|
||||
|
||||
const openInfo = () => {
|
||||
@@ -387,7 +395,7 @@ const Start = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!isPWA && (
|
||||
{!isPWA && window.isIOS && (
|
||||
<p className="text-center text-xs text-text-primary w-11/12 mt-4">
|
||||
If you're on iOS, this page works better if you{' '}
|
||||
<strong>hide the toolbar</strong> or{' '}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useWakeLock } from 'react-screen-wake-lock';
|
||||
import { gte as semverGreaterThanOrEqual } from 'semver';
|
||||
import {
|
||||
GlobalSettingsContext,
|
||||
GlobalSettingsContextType,
|
||||
@@ -14,13 +16,13 @@ import {
|
||||
initialGameSettingsSchema,
|
||||
settingsSchema,
|
||||
} from '../Types/Settings';
|
||||
import { gte as semverGreaterThanOrEqual } from 'semver';
|
||||
|
||||
export const GlobalSettingsProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const localSavedGame = localStorage.getItem('savedGame');
|
||||
@@ -101,6 +103,7 @@ export const GlobalSettingsProvider = ({
|
||||
localStorage.removeItem('showPlay');
|
||||
localStorage.removeItem('preStartComplete');
|
||||
|
||||
//TODO Playing can be removed after routes are implemented
|
||||
setPlaying(false);
|
||||
setShowPlay(false);
|
||||
setPreStartCompleted(false);
|
||||
@@ -187,6 +190,8 @@ export const GlobalSettingsProvider = ({
|
||||
}
|
||||
|
||||
await removeLocalStorage();
|
||||
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const toggleWakeLock = async () => {
|
||||
|
||||
21
src/global.d.ts
vendored
Normal file
21
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
export {};
|
||||
|
||||
export interface BeforeInstallPromptEvent extends Event {
|
||||
readonly platforms: string[];
|
||||
readonly userChoice: Promise<{
|
||||
outcome: 'accepted' | 'dismissed';
|
||||
platform: string;
|
||||
}>;
|
||||
prompt(): Promise<void>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
isIOS: boolean;
|
||||
isIPad: boolean;
|
||||
}
|
||||
interface WindowEventMap {
|
||||
beforeinstallprompt: BeforeInstallPromptEvent;
|
||||
transitionend: BeforeInstallPromptEvent;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@ import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
window.isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
|
||||
window.isIPad = /iPad/.test(navigator.userAgent);
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user