Compare commits

...

1 Commits
0.9.1 ... 0.9.2

Author SHA1 Message Date
Viktor Rådberg
3276dc81fc Clearer way to see if there is an update, more fun tracking 2024-03-30 14:04:12 +01:00
16 changed files with 9903 additions and 8982 deletions

View File

@@ -7,7 +7,7 @@ jobs:
build_and_deploy:
runs-on: ubuntu-latest
env:
REPO_READ_ACCESS_TOKEN: ${{ secrets.REPO_READ_ACCESS_TOKEN }}
VITE_REPO_READ_ACCESS_TOKEN: ${{ secrets.REPO_READ_ACCESS_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v3

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
node-linker=hoisted

1
env.d.ts vendored
View File

@@ -1 +0,0 @@
declare const APP_VERSION: string;

View File

@@ -1,11 +1,12 @@
{
"name": "life-trinket",
"private": true,
"version": "0.9.1",
"version": "0.9.2",
"type": "commonjs",
"engines": {
"node": ">=18",
"npm": "please use bun or yarn :) "
"yarn": "use pnpm",
"npm": "please use pnpm"
},
"scripts": {
"dev": "vite",

9603
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -133,10 +133,12 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
trackMouse: true,
onSwipedDown: (e) => {
e.event.stopPropagation();
analytics.trackEvent('open_player_menu_swipe');
setShowPlayerMenu(true);
},
onSwipedUp: (e) => {
e.event.stopPropagation();
analytics.trackEvent('close_player_menu_swipe');
setShowPlayerMenu(false);
},
@@ -227,6 +229,7 @@ const LifeCounter = ({ player, opponents }: LifeCounterProps) => {
{settings.showPlayerMenuCog && (
<SettingsButton
onClick={() => {
analytics.trackEvent('open_player_menu_button');
setShowPlayerMenu(!showPlayerMenu);
}}
rotation={player.settings.rotation}

View File

@@ -3,6 +3,8 @@ import { twc } from 'react-twc';
import { Separator } from './Separator';
import { Paragraph } from './TextComponents';
import { Cross } from '../../Icons/generated';
import { useEffect } from 'react';
import { useAnalytics } from '../../Hooks/useAnalytics';
export const ModalWrapper = twc.div`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[85vh] bg-background-default p-4 overflow-scroll rounded-2xl border-none text-text-primary w-[95vw] max-w-[548px]`;
@@ -12,6 +14,17 @@ type InfoModalProps = {
};
export const InfoModal = ({ isOpen, closeModal }: InfoModalProps) => {
const analytics = useAnalytics();
useEffect(() => {
if (!isOpen) {
return;
}
analytics.trackEvent('info_opened');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
return (
<Modal
open={isOpen}

View File

@@ -1,5 +1,5 @@
import { Button, Modal, Switch } from '@mui/material';
import { useEffect, useState } from 'react';
import { Modal, Switch } from '@mui/material';
import { useEffect } from 'react';
import { twc } from 'react-twc';
import { useGlobalSettings } from '../../Hooks/useGlobalSettings';
import { Cross } from '../../Icons/generated';
@@ -7,76 +7,55 @@ import { PreStartMode } from '../../Types/Settings';
import { ModalWrapper } from './InfoModal';
import { Separator } from './Separator';
import { Paragraph } from './TextComponents';
import { useAnalytics } from '../../Hooks/useAnalytics';
const SettingContainer = twc.div`w-full flex flex-col mb-2`;
const ToggleContainer = twc.div`flex flex-row justify-between items-center -mb-1`;
const Container = twc.div`flex flex-col items-center w-full`;
const Container = twc.div`flex flex-col items-start w-full`;
const Description = twc.p`mr-16 text-xs text-left text-text-secondary`;
const baseGithubReleasesUrl =
'https://github.com/Vikeo/LifeTrinket/releases/tag/';
type SettingsModalProps = {
isOpen: boolean;
closeModal: () => void;
};
export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
const { settings, setSettings, isPWA } = useGlobalSettings();
const [isLatestVersion, setIsLatestVersion] = useState(false);
const [newVersion, setNewVersion] = useState<string | undefined>(undefined);
const { settings, setSettings, isPWA, version } = useGlobalSettings();
const analytics = useAnalytics();
useEffect(() => {
if (!isOpen) {
return;
}
async function checkIfLatestVersion() {
try {
const result = await fetch(
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
{
headers: {
/* @ts-expect-error is defined in vite.config.ts*/
Authorization: `Bearer ${REPO_READ_ACCESS_TOKEN}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
const data = await result.json();
if (!data.name) {
setNewVersion(undefined);
setIsLatestVersion(false);
return;
}
setNewVersion(data.name);
/* @ts-expect-error is defined in vite.config.ts*/
if (data.name === APP_VERSION) {
setIsLatestVersion(true);
return;
}
setIsLatestVersion(false);
} catch (error) {
console.error('error getting latest version string', error);
}
}
checkIfLatestVersion();
analytics.trackEvent('settings_opened');
version.checkForNewVersion('settings');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
return (
<Modal
open={isOpen}
onClose={closeModal}
onClose={() => {
analytics.trackEvent('settings_outside_clicked');
closeModal();
}}
className="w-full flex justify-center"
>
<>
<div className="flex justify-center items-center relative w-full max-w-[532px]">
<button
onClick={closeModal}
onClick={() => {
analytics.trackEvent('settings_cross_clicked');
closeModal();
}}
className="flex absolute top-12 right-0 z-10 w-10 h-10 bg-primary-main items-center justify-center rounded-full border-solid border-primary-dark border-2"
>
<Cross size="16px" className="text-text-primary " />
@@ -84,31 +63,71 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</div>
<ModalWrapper>
<Container>
<h2 className="text-center text-2xl mb-2"> Settings </h2>
<SettingContainer>
<Paragraph>
{/* @ts-expect-error is defined in vite.config.ts*/}
Current version: {APP_VERSION}{' '}
{isLatestVersion && (
<h2 className="text-center text-2xl mb-2 w-full"> Settings </h2>
<div className="flex flex-col mb-2 w-full">
<div className="text-text-primary flex items-center gap-2">
Current version: {version.installedVersion}{' '}
{version.isLatest && (
<span className="text-sm text-text-secondary">(latest)</span>
)}
</Paragraph>
{!isLatestVersion && newVersion && (
<Paragraph className="text-text-secondary text-lg text-center">
New version ({newVersion}) is available!{' '}
</Paragraph>
<div className="text-xs text-text-primary opacity-75">
(
<a
href={baseGithubReleasesUrl + version.installedVersion}
target="_blank"
className="underline"
onClick={() => {
analytics.trackEvent(
`current_change_log_clicked_v${version.installedVersion}`
);
}}
>
Release notes
</a>
)
</div>
</div>
{!version.isLatest && version.remoteVersion && (
<>
<div className="flex gap-2 items-center mt-2">
<Paragraph className="text-text-secondary">
{version.remoteVersion} available!
</Paragraph>
<div className="text-xs text-text-primary opacity-75">
(
<a
href={baseGithubReleasesUrl + version.remoteVersion}
target="_blank"
className="underline"
onClick={() => {
analytics.trackEvent(
`new_change_log_clicked_v${version.remoteVersion}`
);
}}
>
Release notes
</a>
)
</div>
</div>
<button
className="flex justify-center items-center self-start mt-2 bg-primary-main px-3 py-1 rounded-md"
onClick={() => {
{
analytics.trackEvent(`pressed_update`, {
toVersion: version.remoteVersion,
fromVersion: version.installedVersion,
});
window?.location?.reload();
}
}}
>
<span className="text-sm">Update</span>
<span className="text-xs">&nbsp;(reload app)</span>
</button>
</>
)}
</SettingContainer>
{!isLatestVersion && newVersion && (
<Button
variant="contained"
style={{ marginTop: '0.25rem', marginBottom: '0.25rem' }}
onClick={() => window?.location?.reload()}
>
<span>Update</span>
<span className="text-xs">&nbsp;(reload app)</span>
</Button>
)}
</div>
<Separator height="1px" />
<SettingContainer>
@@ -149,7 +168,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</SettingContainer>
<SettingContainer>
<div className="flex flex-row justify-between items-center mb-1">
<label htmlFor="pre-start-modes">Pre-Start mode</label>
<label htmlFor="pre-start-modes">Player selection style</label>
<select
name="pre-start-modes"
id="pre-start-modes"
@@ -163,9 +182,11 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
}}
disabled={!settings.showStartingPlayer}
>
<option value={PreStartMode.None}>None</option>
<option value={PreStartMode.RandomKing}>Random King</option>
<option value={PreStartMode.FingerGame}>Finger Game</option>
<option value={PreStartMode.None}>Instant</option>
<option value={PreStartMode.RandomKing}>Royal Shuffle</option>
<option value={PreStartMode.FingerGame}>
Touch Roulette
</option>
</select>
</div>
<div className="text-xs text-left text-text-secondary">
@@ -175,13 +196,13 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
{settings.preStartMode === PreStartMode.None && (
<div className="text-xs text-left text-text-secondary mt-1">
<span className="text-text-primary">None:</span> The starting
player will simply be shown.
<span className="text-text-primary">Instant:</span> A random
starting player will simply be shown on start.
</div>
)}
{settings.preStartMode === PreStartMode.RandomKing && (
<div className="text-xs text-left text-text-secondary mt-1">
<span className="text-text-primary">Random King:</span>{' '}
<span className="text-text-primary">Royal Shuffle:</span>{' '}
Randomly pass a crown between all players, press the screen to
stop it. The player who has the crown when it stops gets to
start.
@@ -189,7 +210,7 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
)}
{settings.preStartMode === PreStartMode.FingerGame && (
<div className="text-xs text-left text-text-secondary mt-1">
<span className="text-text-primary">Finger Game:</span> All
<span className="text-text-primary">Touch Roulette:</span> All
players put a finger on the screen, one will be chosen at
random.
</div>
@@ -234,6 +255,16 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
enabled.
</Description>
</SettingContainer>
<Separator height="1px" />
<button
className="flex justify-center self-center items-center mt-1 mb-1 bg-primary-main px-3 py-1 rounded-md"
onClick={() => {
analytics.trackEvent('settings_save_clicked');
closeModal();
}}
>
<span className="text-sm">Save and Close</span>
</button>
{!isPWA && (
<>
<Separator height="1px" />
@@ -254,14 +285,6 @@ export const SettingsModal = ({ isOpen, closeModal }: SettingsModalProps) => {
</>
)}
<Separator height="1px" />
<Button
variant="contained"
onClick={closeModal}
style={{ marginTop: '0.25rem' }}
>
Save and Close
</Button>
</Container>
</ModalWrapper>
</>

View File

@@ -18,6 +18,7 @@ import {
} from '../../Icons/generated';
import { Player, Rotation } from '../../Types/Player';
import { RotationDivProps } from '../Buttons/CommanderDamage';
import { useAnalytics } from '../../Hooks/useAnalytics';
const PlayerMenuWrapper = twc.div`
flex
@@ -111,6 +112,8 @@ const PlayerMenu = ({
setRandomizingPlayer,
} = useGlobalSettings();
const analytics = useAnalytics();
const { updatePlayer, resetCurrentGame } = usePlayers();
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -130,6 +133,7 @@ const PlayerMenu = ({
setShowPlayerMenu(false);
setPlaying(false);
setRandomizingPlayer(true);
analytics.trackEvent('reset_game');
};
const handleGoToStart = () => {
@@ -173,7 +177,10 @@ const PlayerMenu = ({
ref={settingsContainerRef}
>
<button
onClick={() => setShowPlayerMenu(false)}
onClick={() => {
analytics.trackEvent('close_player_menu_button');
setShowPlayerMenu(false);
}}
className="flex absolute top-0 right-2 z-10 bg-transparent items-center justify-center rounded-full border-solid border-primary-main border-2 p-[0.2rem]"
>
<Cross size={buttonFontSize} className="text-primary-main " />
@@ -187,6 +194,11 @@ const PlayerMenu = ({
type="color"
className="size-[200%] absolute -left-2 -top-2"
value={player.color}
onClick={() => {
analytics.trackEvent('color_picker_opened', {
player: player.index,
});
}}
/>
</ColorPickerButton>
{player.settings.useCommanderDamage && (
@@ -210,7 +222,12 @@ const PlayerMenu = ({
strokeWidth="30"
/>
}
onChange={handleSettingsChange}
onChange={(e) => {
analytics.trackEvent('toggle_partner', {
checked: e.target.checked,
});
handleSettingsChange(e);
}}
role="checkbox"
aria-checked={player.settings.usePartner}
aria-label="Partner"
@@ -237,7 +254,12 @@ const PlayerMenu = ({
strokeWidth="30"
/>
}
onChange={handleSettingsChange}
onChange={(e) => {
analytics.trackEvent('toggle_poison', {
checked: e.target.checked,
});
handleSettingsChange(e);
}}
role="checkbox"
aria-checked={player.settings.usePoison}
aria-label="Poison"
@@ -263,7 +285,12 @@ const PlayerMenu = ({
strokeWidth="15"
/>
}
onChange={handleSettingsChange}
onChange={(e) => {
analytics.trackEvent('toggle_energy', {
checked: e.target.checked,
});
handleSettingsChange(e);
}}
role="checkbox"
aria-checked={player.settings.useEnergy}
aria-label="Energy"
@@ -289,7 +316,12 @@ const PlayerMenu = ({
strokeWidth="15"
/>
}
onChange={handleSettingsChange}
onChange={(e) => {
analytics.trackEvent('toggle_experience', {
checked: e.target.checked,
});
handleSettingsChange(e);
}}
role="checkbox"
aria-checked={player.settings.useExperience}
aria-label="Experience"

View File

@@ -91,6 +91,7 @@ const Start = () => {
settings,
isPWA,
setRandomizingPlayer,
version,
} = useGlobalSettings();
const [openInfoModal, setOpenInfoModal] = useState(false);
@@ -100,6 +101,29 @@ const Start = () => {
initialGameSettings || defaultInitialGameSettings
);
let tracked = false;
// Check for new version on mount
useEffect(() => {
if (!tracked) {
console.log('checking version');
version.checkForNewVersion('start_menu');
// eslint-disable-next-line react-hooks/exhaustive-deps
tracked = true;
}
}, []);
useEffect(() => {
setInitialGameSettings(playerOptions);
}, [playerOptions, setInitialGameSettings]);
useEffect(() => {
setPlayerOptions({
...playerOptions,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playerOptions.numberOfPlayers]);
const doStartGame = () => {
if (!initialGameSettings) {
return;
@@ -127,21 +151,10 @@ const Start = () => {
localStorage.setItem('showPlay', 'true');
};
useEffect(() => {
setInitialGameSettings(playerOptions);
}, [playerOptions, setInitialGameSettings]);
const valuetext = (value: number) => {
const valueText = (value: number) => {
return `${value}`;
};
useEffect(() => {
setPlayerOptions({
...playerOptions,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playerOptions.numberOfPlayers]);
return (
<MainWrapper>
<Info
@@ -169,8 +182,12 @@ const Start = () => {
<SupportMe />
<h1 className="text-3xl block font-bold mt-6 mb-5 text-text-primary">
<h1 className="relative flex flex-col text-3xl font-bold mt-6 mb-6 text-text-primary justify-center items-center">
Life Trinket
<div className="h-[1px] w-[120%] bg-common-white opacity-50" />
<div className="flex absolute text-xs font-medium -bottom-4">
v{version.installedVersion}
</div>
</h1>
<div className="overflow-hidden items-center flex flex-col max-w-[548px] w-full mb-8 px-4">
@@ -183,7 +200,7 @@ const Start = () => {
min={1}
aria-label="Custom marks"
value={playerOptions?.numberOfPlayers ?? 4}
getAriaValueText={valuetext}
getAriaValueText={valueText}
step={null}
marks={playerMarks}
onChange={(_e, value) => {
@@ -204,7 +221,7 @@ const Start = () => {
min={20}
aria-label="Custom marks"
value={playerOptions?.startingLifeTotal ?? 40}
getAriaValueText={valuetext}
getAriaValueText={valueText}
step={10}
marks={healthMarks}
onChange={(_e, value) =>
@@ -247,15 +264,32 @@ const Start = () => {
}}
/>
</ToggleContainer>
<Button
variant="contained"
style={{ height: '2rem' }}
onClick={() => {
setOpenSettingsModal(true);
}}
>
<Cog /> &nbsp; Other settings
</Button>
<div className="flex flex-nowrap text-nowrap relative justify-center items-start">
<Button
variant="contained"
style={{ height: '2rem' }}
onClick={() => {
setOpenSettingsModal(true);
}}
>
<Cog /> &nbsp; Game Settings
</Button>
<div
data-not-latest-version={
!version.isLatest && !!version.remoteVersion
}
className="absolute flex justify-center text-text-primary text-xxs -bottom-5 bg-primary-dark px-2 rounded-md
opacity-0 transition-all duration-200 delay-500
data-[not-latest-version=true]:opacity-100
"
>
<div className="absolute bg-primary-dark rotate-45 size-2 -top-[2px] z-0" />
<span className="z-10">
v{version.remoteVersion} available!
</span>
</div>
</div>
</ToggleButtonsWrapper>
<FormLabel>Layout</FormLabel>

View File

@@ -1,6 +1,13 @@
import { createContext } from 'react';
import { InitialGameSettings, Settings } from '../Types/Settings';
type Version = {
installedVersion: string;
isLatest: boolean;
checkForNewVersion: (source: 'settings' | 'start_menu') => Promise<void>;
remoteVersion?: string;
};
export type GlobalSettingsContextType = {
fullscreen: {
isFullscreen: boolean;
@@ -29,6 +36,8 @@ export type GlobalSettingsContextType = {
isPWA: boolean;
preStartCompleted: boolean;
setPreStartCompleted: (completed: boolean) => void;
version: Version;
};
export const GlobalSettingsContext =

View File

@@ -142,6 +142,11 @@ export const GlobalSettingsProvider = ({
};
}, []);
const [isLatestVersion, setIsLatestVersion] = useState(false);
const [remoteVersion, setRemoteVersion] = useState<string | undefined>(
undefined
);
const { isSupported, release, released, request, type } = useWakeLock();
const active = settings.keepAwake;
@@ -195,6 +200,46 @@ export const GlobalSettingsProvider = ({
localStorage.setItem('playing', String(playing));
};
async function checkForNewVersion(source: 'settings' | 'start_menu') {
try {
const result = await fetch(
'https://api.github.com/repos/Vikeo/LifeTrinket/releases/latest',
{
headers: {
Authorization: `Bearer ${
import.meta.env.VITE_REPO_READ_ACCESS_TOKEN
}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
}
);
const data = await result.json();
if (!data.name) {
setRemoteVersion(undefined);
setIsLatestVersion(false);
return;
}
setRemoteVersion(data.name);
if (data.name === import.meta.env.VITE_APP_VERSION) {
setIsLatestVersion(true);
return;
}
analytics.trackEvent(`${source}_has_new_version`, {
remoteVersion: data.name,
installedVersion: import.meta.env.VITE_APP_VERSION,
});
setIsLatestVersion(false);
} catch (error) {
console.error('error getting latest version string', error);
}
}
return {
fullscreen: { isFullscreen, enableFullscreen, disableFullscreen },
wakeLock: {
@@ -219,6 +264,13 @@ export const GlobalSettingsProvider = ({
isPWA: window?.matchMedia('(display-mode: standalone)').matches,
preStartCompleted,
setPreStartCompleted: setPreStartCompletedAndLocalStorage,
version: {
installedVersion: import.meta.env.VITE_APP_VERSION,
remoteVersion,
isLatest: isLatestVersion,
checkForNewVersion,
},
};
}, [
isFullscreen,
@@ -233,6 +285,8 @@ export const GlobalSettingsProvider = ({
settings,
randomizingPlayer,
preStartCompleted,
remoteVersion,
isLatestVersion,
analytics,
]);

9
src/vite-env.d.ts vendored
View File

@@ -1 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_VERSION: string;
readonly VITE_REPO_READ_ACCESS_TOKEN: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -103,6 +103,9 @@ export default {
animation: {
fadeOut: 'fadeOut 3s 1s ease-out forwards',
},
fontSize: {
xxs: ['0.625rem', '1rem'],
},
},
},
plugins: [tailwindcssGridAreas],

View File

@@ -21,7 +21,11 @@ export default defineConfig({
},
},
define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version),
REPO_READ_ACCESS_TOKEN: JSON.stringify(process.env.REPO_READ_ACCESS_TOKEN),
'import.meta.env.VITE_APP_VERSION': JSON.stringify(
process.env.npm_package_version
),
VITE_REPO_READ_ACCESS_TOKEN: JSON.stringify(
process.env.VITE_REPO_READ_ACCESS_TOKEN
),
},
});

8867
yarn.lock

File diff suppressed because it is too large Load Diff