Stian Fredrik Aune
2 years ago
69 changed files with 4015 additions and 65 deletions
-
26webui-react/.gitignore
-
14webui-react/index.html
-
30webui-react/package.json
-
BINwebui-react/public/2046.png
-
1webui-react/public/vite.svg
-
14webui-react/src/App.sass
-
34webui-react/src/App.tsx
-
47webui-react/src/actions/devices.ts
-
20webui-react/src/actions/programs.ts
-
26webui-react/src/actions/runtime.ts
-
63webui-react/src/actions/shared.ts
-
50webui-react/src/actions/workouts.ts
-
35webui-react/src/contexts/DeviceContext.tsx
-
35webui-react/src/contexts/ProgramContext.tsx
-
213webui-react/src/contexts/RuntimeContext.tsx
-
94webui-react/src/contexts/WorkoutContext.tsx
-
50webui-react/src/helpers/dates.ts
-
3webui-react/src/helpers/misc.ts
-
36webui-react/src/hooks/keyboard.ts
-
9webui-react/src/main.tsx
-
5webui-react/src/models/Devices.ts
-
50webui-react/src/models/Programs.ts
-
53webui-react/src/models/Shared.ts
-
66webui-react/src/models/Workouts.ts
-
363webui-react/src/pages/DevicePage.tsx
-
118webui-react/src/pages/IndexPage.tsx
-
40webui-react/src/pages/LoadingPage.tsx
-
209webui-react/src/pages/PlayPage.tsx
-
105webui-react/src/pages/WorkoutPage.tsx
-
106webui-react/src/pages/runtime/ControlsBoi.tsx
-
13webui-react/src/pages/runtime/MessageBoi.tsx
-
56webui-react/src/pages/runtime/ProgramBoi.sass
-
159webui-react/src/pages/runtime/ProgramBoi.tsx
-
17webui-react/src/pages/runtime/hooks.tsx
-
37webui-react/src/primitives/Pallette.sass
-
34webui-react/src/primitives/Shared.sass
-
17webui-react/src/primitives/Shared.tsx
-
134webui-react/src/primitives/blob/Blob.sass
-
130webui-react/src/primitives/blob/Blob.tsx
-
43webui-react/src/primitives/boi/Boi.sass
-
30webui-react/src/primitives/boi/Boi.tsx
-
37webui-react/src/primitives/header/Header.sass
-
55webui-react/src/primitives/header/Header.tsx
-
10webui-react/src/primitives/misc/Misc.sass
-
8webui-react/src/primitives/misc/Misc.tsx
-
45webui-react/src/primitives/page/Page.sass
-
76webui-react/src/primitives/page/Page.tsx
-
8webui-react/src/vite-env.d.ts
-
21webui-react/tsconfig.json
-
9webui-react/tsconfig.node.json
-
24webui-react/vite.config.ts
-
977webui-react/yarn.lock
-
1ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/domain/models/Workout.kt
-
2ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/domain/runtime/Command.kt
-
2ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/domain/runtime/Event.kt
-
52ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/drivers/ProgramEnforcer.kt
-
28ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/drivers/Skipper.kt
-
4ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/drivers/WorkoutWriter.kt
-
2ykonsole-core/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/testing/TestDriver.kt
-
11ykonsole-iconsole/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/IConsole.kt
-
10ykonsole-ktor/src/main/kotlin/net/aiterp/git/ykonsole2/application/routes/Shared.kt
-
37ykonsole-ktor/src/main/kotlin/net/aiterp/git/ykonsole2/application/routes/Workouts.kt
-
2ykonsole-ktor/src/main/kotlin/net/aiterp/git/ykonsole2/application/routes/ws/Connection.kt
-
2ykonsole-ktor/src/main/kotlin/net/aiterp/git/ykonsole2/application/routes/ws/SocketInput.kt
-
2ykonsole-mysql/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/repositories/MySqlProgramRepository.kt
-
45ykonsole-mysql/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/repositories/MySqlWorkoutRepository.kt
-
6ykonsole-mysql/src/main/kotlin/net/aiterp/git/ykonsole2/infrastructure/repositories/MySqlWorkoutStateRepository.kt
-
13ykonsole-mysql/src/main/resources/migrations/tables/workout.xml
-
6ykonsole-server/src/main/kotlin/net/aiterp/git/ykonsole2/Server.kt
@ -0,0 +1,26 @@ |
|||
# Logs |
|||
logs |
|||
*.log |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
pnpm-debug.log* |
|||
lerna-debug.log* |
|||
|
|||
node_modules |
|||
dist |
|||
dist-webapp |
|||
dist-chrome |
|||
dist-ssr |
|||
*.local |
|||
|
|||
# Editor directories and files |
|||
.vscode/* |
|||
!.vscode/extensions.json |
|||
.idea |
|||
.DS_Store |
|||
*.suo |
|||
*.ntvs* |
|||
*.njsproj |
|||
*.sln |
|||
*.sw? |
@ -0,0 +1,14 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|||
<meta name="theme-color" content="#203764" /> |
|||
<title>YKonsole</title> |
|||
</head> |
|||
<body> |
|||
<div id="root"></div> |
|||
<script type="module" src="/src/main.tsx"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,30 @@ |
|||
{ |
|||
"name": "webui-react", |
|||
"private": true, |
|||
"version": "0.0.0", |
|||
"type": "module", |
|||
"scripts": { |
|||
"dev": "VITE_MODE=webapp vite --host", |
|||
"build-webapp": "tsc && VITE_MODE=webapp vite build", |
|||
"build-chrome-plugin": "tsc && VITE_MODE=chrome-plugin vite build", |
|||
"preview": "vite preview" |
|||
}, |
|||
"dependencies": { |
|||
"@fortawesome/fontawesome-svg-core": "^6.1.1", |
|||
"@fortawesome/free-solid-svg-icons": "^6.1.1", |
|||
"@fortawesome/react-fontawesome": "^0.2.0", |
|||
"axios": "^0.27.2", |
|||
"react": "^18.0.0", |
|||
"react-dom": "^18.0.0", |
|||
"react-router": "^6.3.0", |
|||
"react-router-dom": "^6.3.0", |
|||
"sass": "^1.53.0" |
|||
}, |
|||
"devDependencies": { |
|||
"@types/react": "^18.0.17", |
|||
"@types/react-dom": "^18.0.6", |
|||
"@vitejs/plugin-react": "^2.0.1", |
|||
"typescript": "^4.6.4", |
|||
"vite": "^3.0.7" |
|||
} |
|||
} |
After Width: 1920 | Height: 1080 | Size: 1.7 MiB |
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> |
@ -0,0 +1,14 @@ |
|||
@import "primitives/Shared" |
|||
|
|||
html, body, #root |
|||
margin: 0 |
|||
width: 100% |
|||
height: 100% |
|||
background-color: $body-background |
|||
color: $body-foreground |
|||
font-family: $font-global |
|||
font-weight: 300 |
|||
|
|||
.App |
|||
width: 100% |
|||
height: 100% |
@ -0,0 +1,34 @@ |
|||
import {useEffect, useState} from 'react' |
|||
import IndexPage from "./pages/IndexPage"; |
|||
import "./App.sass"; |
|||
import {ProgramContextProvider} from "./contexts/ProgramContext"; |
|||
import {BrowserRouter, Route, Routes} from "react-router-dom"; |
|||
import {DeviceContextProvider} from "./contexts/DeviceContext"; |
|||
import DevicePage from "./pages/DevicePage"; |
|||
import {RuntimeContextProvider} from "./contexts/RuntimeContext"; |
|||
import {WorkoutContextProvider} from "./contexts/WorkoutContext"; |
|||
import WorkoutPage from "./pages/WorkoutPage"; |
|||
import PlayPage from "./pages/PlayPage"; |
|||
|
|||
function App() { |
|||
return ( |
|||
<BrowserRouter> |
|||
<DeviceContextProvider> |
|||
<ProgramContextProvider> |
|||
<RuntimeContextProvider> |
|||
<WorkoutContextProvider> |
|||
<Routes> |
|||
<Route path="/" element={<IndexPage/>}/> |
|||
<Route path="/devices/:id" element={<DevicePage/>}/> |
|||
<Route path="/workouts/:id" element={<WorkoutPage/>}/> |
|||
<Route path="/play" element={<PlayPage/>}/> |
|||
</Routes> |
|||
</WorkoutContextProvider> |
|||
</RuntimeContextProvider> |
|||
</ProgramContextProvider> |
|||
</DeviceContextProvider> |
|||
</BrowserRouter> |
|||
) |
|||
} |
|||
|
|||
export default App |
@ -0,0 +1,47 @@ |
|||
import {Device} from "../models/Devices"; |
|||
import {deleteRequest, getRequest, postRequest, putRequest} from "./shared"; |
|||
|
|||
interface DeviceRepository { |
|||
fetchAll(): Promise<Device[]> |
|||
save(device: Partial<Device>): Promise<boolean> |
|||
delete(id: string): Promise<boolean> |
|||
} |
|||
|
|||
export default function deviceRepo(): DeviceRepository { |
|||
switch (import.meta.env.VITE_MODE) { |
|||
case "webapp": |
|||
return defaultImpl; |
|||
case "chrome-plugin": |
|||
default: |
|||
throw new Error("Not implemented"); |
|||
} |
|||
} |
|||
|
|||
const defaultImpl: DeviceRepository = { |
|||
fetchAll() { |
|||
return getRequest("/devices") || null; |
|||
}, |
|||
async save({id, name, connectionString}: Partial<Device>): Promise<boolean> { |
|||
try { |
|||
if (id) { |
|||
await putRequest(`/devices/${id}`, {name, connectionString}); |
|||
return true; |
|||
} else if (name && connectionString) { |
|||
await postRequest("/devices", {name, connectionString}); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} catch (e) { |
|||
return false; |
|||
} |
|||
}, |
|||
async delete(id: string): Promise<boolean> { |
|||
try { |
|||
await deleteRequest(`/devices/${id}`); |
|||
return true; |
|||
} catch (e) { |
|||
return false; |
|||
} |
|||
}, |
|||
}; |
@ -0,0 +1,20 @@ |
|||
import {Program} from "../models/Programs"; |
|||
import {getRequest} from "./shared"; |
|||
|
|||
interface ProgramRepository { |
|||
fetchAll(): Promise<Program[]> |
|||
} |
|||
|
|||
export default function programRepo(): ProgramRepository { |
|||
switch (import.meta.env.VITE_MODE) { |
|||
case "webapp": |
|||
return defaultImpl; |
|||
case "chrome-plugin": |
|||
default: |
|||
throw new Error("Not implemented"); |
|||
} |
|||
} |
|||
|
|||
const defaultImpl: ProgramRepository = { |
|||
fetchAll: () => getRequest("/programs") || null, |
|||
}; |
@ -0,0 +1,26 @@ |
|||
interface RuntimeRepository { |
|||
openWebsocket(): WebSocket |
|||
} |
|||
|
|||
export default function runtimeRepo(): RuntimeRepository { |
|||
switch (import.meta.env.VITE_MODE) { |
|||
case "webapp": |
|||
if (window.location.hostname === "localhost" || window.location.hostname === "10.24.10.24") { |
|||
return makeRuntimeRepo(`${window.location.hostname}:8080`); |
|||
} else { |
|||
return makeRuntimeRepo(window.location.hostname); |
|||
} |
|||
case "chrome-plugin": |
|||
return makeRuntimeRepo("127.0.0.1:9999"); |
|||
default: |
|||
throw new Error("Not implemented"); |
|||
} |
|||
} |
|||
|
|||
function makeRuntimeRepo(host: string): RuntimeRepository { |
|||
return { |
|||
openWebsocket(): WebSocket { |
|||
return new WebSocket(`ws://${host}/api/workouts/active`); |
|||
}, |
|||
}; |
|||
} |
@ -0,0 +1,63 @@ |
|||
import Axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from "axios"; |
|||
|
|||
interface Response<T> { |
|||
code: number |
|||
message: string |
|||
data: T |
|||
} |
|||
|
|||
export async function getRequest<T>(url: string): Promise<T> { |
|||
return sendRequest<T>({ |
|||
method: "GET", |
|||
url: `/api${url}`, |
|||
responseType: "json", |
|||
}); |
|||
} |
|||
|
|||
export function postRequest<T>(url: string, data: object = {}): Promise<T> { |
|||
return sendRequest<T>({ |
|||
method: "POST", |
|||
url: `/api${url}`, |
|||
responseType: "json", |
|||
data, |
|||
}); |
|||
} |
|||
|
|||
export function putRequest<T>(url: string, data: object): Promise<T> { |
|||
return sendRequest<T>({ |
|||
method: "PUT", |
|||
url: `/api${url}`, |
|||
responseType: "json", |
|||
data, |
|||
}); |
|||
} |
|||
|
|||
export function deleteRequest<T>(url: string): Promise<T> { |
|||
return sendRequest<T>({ |
|||
method: "DELETE", |
|||
url: `/api${url}`, |
|||
responseType: "json", |
|||
}); |
|||
} |
|||
|
|||
async function sendRequest<T>(config: AxiosRequestConfig): Promise<T> { |
|||
try { |
|||
const res = await Axios(config); |
|||
|
|||
return await handleResponse(res); |
|||
} catch (e) { |
|||
return await handleResponse((e as AxiosError).response as AxiosResponse); |
|||
} |
|||
} |
|||
|
|||
function handleResponse<T>(response: AxiosResponse): Promise<T> { |
|||
const obj: Response<T> = response.data; |
|||
|
|||
if (![200, 201].includes(response.status)) { |
|||
if (response.status === 403) { |
|||
return Promise.reject(obj.message); |
|||
} |
|||
} |
|||
|
|||
return Promise.resolve(obj.data); |
|||
} |
@ -0,0 +1,50 @@ |
|||
import {getRequest, postRequest} from "./shared"; |
|||
import {PastWorkout, WorkoutState} from "../models/Workouts"; |
|||
|
|||
interface WorkoutFilter { |
|||
daysBack: number |
|||
includeTest: boolean |
|||
} |
|||
|
|||
export interface CreateWorkoutOptions { |
|||
deviceId: string |
|||
programId?: string |
|||
test?: boolean |
|||
} |
|||
|
|||
interface WorkoutRepository { |
|||
findById(workoutId: string): Promise<PastWorkout | null> |
|||
fetchByFilter(filter: WorkoutFilter): Promise<PastWorkout[]> |
|||
fetchStates(workoutId: string): Promise<WorkoutState[]> |
|||
createWorkout(options: CreateWorkoutOptions): Promise<boolean> |
|||
} |
|||
|
|||
export default function workoutRepo(): WorkoutRepository { |
|||
switch (import.meta.env.VITE_MODE) { |
|||
case "webapp": |
|||
return defaultImpl; |
|||
case "chrome-plugin": |
|||
default: |
|||
throw new Error("Not implemented"); |
|||
} |
|||
} |
|||
|
|||
const defaultImpl: WorkoutRepository = { |
|||
findById(workoutId: string): Promise<PastWorkout | null> { |
|||
return getRequest<PastWorkout>(`/workouts/${workoutId}`).catch(() => null); |
|||
}, |
|||
fetchByFilter({daysBack, includeTest}: WorkoutFilter): Promise<PastWorkout[]> { |
|||
return getRequest(`/workouts?daysBack=${daysBack}&includeTest=${includeTest}`); |
|||
}, |
|||
fetchStates(workoutId: string): Promise<WorkoutState[]> { |
|||
return getRequest(`/workouts/${workoutId}/states`); |
|||
}, |
|||
async createWorkout(options: CreateWorkoutOptions) { |
|||
try { |
|||
await postRequest("/workouts", options); |
|||
return true; |
|||
} catch (e) { |
|||
return false; |
|||
} |
|||
}, |
|||
}; |
@ -0,0 +1,35 @@ |
|||
import {createContext, useCallback, useEffect, useState} from "react"; |
|||
import {unimplemented} from "../helpers/misc"; |
|||
import {Device} from "../models/Devices"; |
|||
import {WithChildren} from "../primitives/Shared"; |
|||
import deviceRepo from "../actions/devices"; |
|||
|
|||
interface DeviceContextValue { |
|||
devices: Device[] | null |
|||
refreshDevices(): void |
|||
} |
|||
|
|||
const DeviceContext = createContext<DeviceContextValue>({ |
|||
devices: null, |
|||
refreshDevices: unimplemented, |
|||
}); |
|||
|
|||
export function DeviceContextProvider({children}: WithChildren) { |
|||
const [devices, setDevices] = useState<Device[] | null>(null); |
|||
|
|||
const refreshDevices = useCallback(() => { |
|||
deviceRepo().fetchAll().then(setDevices); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
refreshDevices(); |
|||
}, []); |
|||
|
|||
return ( |
|||
<DeviceContext.Provider value={{devices, refreshDevices}}> |
|||
{children} |
|||
</DeviceContext.Provider> |
|||
); |
|||
} |
|||
|
|||
export default DeviceContext; |
@ -0,0 +1,35 @@ |
|||
import {createContext, useCallback, useEffect, useState} from "react"; |
|||
import {WithChildren} from "../primitives/Shared"; |
|||
import {Program} from "../models/Programs"; |
|||
import {unimplemented} from "../helpers/misc"; |
|||
import programRepo from "../actions/programs"; |
|||
|
|||
interface ProgramContextValue { |
|||
programs: Program[] | null |
|||
refreshPrograms(): void |
|||
} |
|||
|
|||
const ProgramContext = createContext<ProgramContextValue>({ |
|||
programs: null, |
|||
refreshPrograms: unimplemented, |
|||
}); |
|||
|
|||
export function ProgramContextProvider({children}: WithChildren) { |
|||
const [programs, setPrograms] = useState<Program[] | null>(null); |
|||
|
|||
const refreshPrograms = useCallback(() => { |
|||
programRepo().fetchAll().then(setPrograms); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
refreshPrograms(); |
|||
}, []); |
|||
|
|||
return ( |
|||
<ProgramContext.Provider value={{programs, refreshPrograms}}> |
|||
{children} |
|||
</ProgramContext.Provider> |
|||
); |
|||
} |
|||
|
|||
export default ProgramContext; |
@ -0,0 +1,213 @@ |
|||
import {CurrentWorkout, WorkoutState, WorkoutStatus} from "../models/Workouts"; |
|||
import {createContext, useCallback, useEffect, useState} from "react"; |
|||
import {unimplemented} from "../helpers/misc"; |
|||
import {WithChildren} from "../primitives/Shared"; |
|||
import {Values} from "../models/Shared"; |
|||
import runtimeRepo from "../actions/runtime"; |
|||
import workoutRepo, {CreateWorkoutOptions} from "../actions/workouts"; |
|||
|
|||
interface RuntimeContextValue { |
|||
workout: CurrentWorkout | null |
|||
states: WorkoutState[] |
|||
error: string | null |
|||
|
|||
lastEvent: SocketOutput | null |
|||
|
|||
active: boolean |
|||
ready: boolean |
|||
ended: boolean |
|||
|
|||
connect(): void |
|||
disconnect(): void |
|||
start(): void |
|||
stop(): void |
|||
setLevel(level: number): void |
|||
|
|||
reset(): void |
|||
resume(): void |
|||
create(options: CreateWorkoutOptions): void |
|||
} |
|||
|
|||
const RuntimeContext = createContext<RuntimeContextValue>({ |
|||
workout: null, |
|||
states: [], |
|||
error: null, |
|||
lastEvent: null, |
|||
active: false, |
|||
ready: false, |
|||
ended: false, |
|||
connect: unimplemented, |
|||
disconnect: unimplemented, |
|||
start: unimplemented, |
|||
stop: unimplemented, |
|||
setLevel: unimplemented, |
|||
reset: unimplemented, |
|||
resume: unimplemented, |
|||
create: unimplemented, |
|||
}); |
|||
|
|||
interface SocketInput { |
|||
start?: true |
|||
stop?: true |
|||
connect?: true |
|||
disconnect?: true |
|||
setValue?: Values |
|||
} |
|||
|
|||
function socketInput(obj: SocketInput): string { |
|||
return JSON.stringify(obj); |
|||
} |
|||
|
|||
export type RuntimeEvent = SocketOutput |
|||
interface SocketOutput { |
|||
sentAt: string |
|||
workout: CurrentWorkout | null |
|||
workoutStates: WorkoutState[] | null |
|||
event: { name: string } | null |
|||
error: { message: string } | null |
|||
} |
|||
|
|||
function socketOutput(str: string): SocketOutput { |
|||
return JSON.parse(str); |
|||
} |
|||
|
|||
export function RuntimeContextProvider({children}: WithChildren): JSX.Element { |
|||
const [workout, setWorkout] = useState<CurrentWorkout | null>(null); |
|||
const [states, setStates] = useState<WorkoutState[]>([]); |
|||
const [error, setError] = useState<string | null>(null); |
|||
const [lastEvent, setLastEvent] = useState<SocketOutput | null>(null); |
|||
const [active, setActive] = useState<boolean>(false); |
|||
const [ready, setReady] = useState<boolean>(false); |
|||
const [ended, setEnded] = useState<boolean>(false); |
|||
const [socket, setSocket] = useState<WebSocket | null>(null); |
|||
|
|||
const sendCommand = useCallback((input: SocketInput) => { |
|||
if (socket && socket.readyState === socket.OPEN && workout) { |
|||
socket.send(socketInput(input)); |
|||
} |
|||
}, [socket, workout]); |
|||
|
|||
const connect = useCallback(() => { |
|||
sendCommand({connect: true}); |
|||
}, [sendCommand]); |
|||
|
|||
const disconnect = useCallback(() => { |
|||
sendCommand({disconnect: true}); |
|||
}, [sendCommand]); |
|||
|
|||
const start = useCallback(() => { |
|||
sendCommand({start: true}); |
|||
}, [sendCommand]); |
|||
|
|||
const stop = useCallback(() => { |
|||
sendCommand({stop: true}); |
|||
}, [sendCommand]); |
|||
|
|||
const setLevel = useCallback((level: number) => { |
|||
sendCommand({ |
|||
setValue: {level}, |
|||
}); |
|||
}, [sendCommand]); |
|||
|
|||
const reset = useCallback(() => { |
|||
setActive(false); |
|||
setEnded(false); |
|||
setReady(false); |
|||
}, []); |
|||
|
|||
const resume = useCallback(() => { |
|||
setWorkout(null); |
|||
setStates([]); |
|||
setError(null); |
|||
setReady(false); |
|||
setActive(true); |
|||
setEnded(false); |
|||
setLastEvent(null); |
|||
|
|||
const socket = runtimeRepo().openWebsocket(); |
|||
socket.onmessage = event => { |
|||
const data = socketOutput(event.data); |
|||
setLastEvent(data); |
|||
setReady(true); |
|||
|
|||
if (data.workout) { |
|||
setWorkout(data.workout); |
|||
} |
|||
|
|||
if (data.workoutStates && data.workoutStates.length > 0) { |
|||
setStates(prev => { |
|||
if (prev.length === 0) { |
|||
return data.workoutStates!; |
|||
} else { |
|||
const lastOld = prev[prev.length - 1]; |
|||
const firstNew = data.workoutStates![0]; |
|||
if (lastOld.time <= firstNew.time) { |
|||
return [ |
|||
...prev.filter(p => p.time < firstNew.time), |
|||
...data.workoutStates!, |
|||
]; |
|||
} else { |
|||
return [...prev, ...data.workoutStates!]; |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
if (data.event) { |
|||
const eventNameMap: Record<string, WorkoutStatus> = { |
|||
"Connected": WorkoutStatus.Connected, |
|||
"Started": WorkoutStatus.Started, |
|||
"Stopped": WorkoutStatus.Stopped, |
|||
"Disconnected": WorkoutStatus.Disconnected, |
|||
}; |
|||
|
|||
const newStatus = eventNameMap[data.event.name]; |
|||
if (newStatus) { |
|||
setWorkout(prev => prev ? ({...prev, status: newStatus}) : prev); |
|||
} |
|||
} |
|||
|
|||
if (data.error) { |
|||
setError(data.error.message); |
|||
setReady(true); |
|||
} |
|||
}; |
|||
|
|||
socket.onclose = () => { |
|||
setEnded(true); |
|||
}; |
|||
|
|||
setSocket(socket); |
|||
}, [socket]); |
|||
|
|||
const create = useCallback((options: CreateWorkoutOptions) => { |
|||
workoutRepo().createWorkout(options).then(success => { |
|||
if (success) { |
|||
resume(); |
|||
} else { |
|||
setError("Kunne ikke opprette økten!"); |
|||
setEnded(true); |
|||
} |
|||
}); |
|||
}, [resume]); |
|||
|
|||
useEffect(() => { |
|||
if (ready && workout?.status === WorkoutStatus.Created) { |
|||
connect(); |
|||
} |
|||
}, [ready, workout]); |
|||
|
|||
return ( |
|||
<RuntimeContext.Provider value={{ |
|||
workout, states, error, |
|||
lastEvent, |
|||
active, ready, ended, |
|||
connect, disconnect, start, stop, setLevel, |
|||
reset, resume, create, |
|||
}}> |
|||
{children} |
|||
</RuntimeContext.Provider> |
|||
); |
|||
} |
|||
|
|||
export default RuntimeContext; |
@ -0,0 +1,94 @@ |
|||
import {PastWorkout, WorkoutState} from "../models/Workouts"; |
|||
import {createContext, useCallback, useContext, useEffect, useState} from "react"; |
|||
import {unimplemented} from "../helpers/misc"; |
|||
import {WithChildren} from "../primitives/Shared"; |
|||
import workoutRepo from "../actions/workouts"; |
|||
|
|||
interface WorkoutContextValue { |
|||
workouts: PastWorkout[] |
|||
loadingWorkouts: boolean |
|||
|
|||
getWorkout(workoutId: string): PastWorkout | null |
|||
fetchWorkout(workoutId: string): void |
|||
|
|||
getStates(workoutId: string): WorkoutState[] | null |
|||
fetchStates(workoutId: string): void |
|||
|
|||
refreshWorkouts(): void |
|||
showMoreWorkouts(): void |
|||
} |
|||
|
|||
const WorkoutContext = createContext<WorkoutContextValue>({ |
|||
workouts: [], |
|||
loadingWorkouts: false, |
|||
getWorkout: unimplemented, |
|||
fetchWorkout: unimplemented, |
|||
getStates: unimplemented, |
|||
fetchStates: unimplemented, |
|||
refreshWorkouts: unimplemented, |
|||
showMoreWorkouts: unimplemented, |
|||
}); |
|||
|
|||
export function WorkoutContextProvider({children}: WithChildren) { |
|||
const [workouts, setWorkouts] = useState<PastWorkout[]>([]); |
|||
const [cache, setCache] = useState<Record<string, PastWorkout>>({}); |
|||
const [loadingWorkouts, setLoadingWorkouts] = useState<boolean>(false); |
|||
const [days, setDays] = useState(6); |
|||
const [ver, setVer] = useState(0); |
|||
const [stateMap, setStateMap] = useState<Record<string, WorkoutState[]>>({}); |
|||
|
|||
const getWorkout = useCallback((workoutId: string) => { |
|||
return cache[workoutId] || workouts.find(w => w.id === workoutId); |
|||
}, [cache, workouts]); |
|||
|
|||
const fetchWorkout = useCallback((workoutId: string) => { |
|||
workoutRepo().findById(workoutId) |
|||
.then(result => { |
|||
if (result) { |
|||
setCache(prev => ({...prev, [workoutId]: result})); |
|||
} |
|||
}); |
|||
}, []); |
|||
|
|||
const getStates = useCallback((workoutId: string) => { |
|||
return stateMap[workoutId] || null; |
|||
}, [stateMap]); |
|||
|
|||
const fetchStates = useCallback((workoutId: string) => { |
|||
workoutRepo().fetchStates(workoutId) |
|||
.then(result => setStateMap(prev => ({...prev, [workoutId]: result}))); |
|||
}, []); |
|||
|
|||
const refreshWorkouts = useCallback(() => { |
|||
setVer(prev => prev + 1); |
|||
}, []); |
|||
|
|||
const showMoreWorkouts = useCallback(() => { |
|||
setDays(prev => prev + (prev > 30 ? 30 : 7)); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
if (ver === 0) { |
|||
return; |
|||
} |
|||
|
|||
setLoadingWorkouts(true); |
|||
workoutRepo().fetchByFilter({daysBack: days, includeTest: false}) |
|||
.then(setWorkouts) |
|||
.finally(() => setLoadingWorkouts(false)); |
|||
}, [days, ver]); |
|||
|
|||
return ( |
|||
<WorkoutContext.Provider value={{ |
|||
workouts, loadingWorkouts, |
|||
getWorkout, fetchWorkout, |
|||
getStates, fetchStates, |
|||
refreshWorkouts, showMoreWorkouts, |
|||
}}> |
|||
{children} |
|||
</WorkoutContext.Provider> |
|||
); |
|||
} |
|||
|
|||
export default WorkoutContext; |
|||
|
@ -0,0 +1,50 @@ |
|||
const oneDay = 1000 * 3600 * 24; |
|||
const sevenDays = 7 * oneDay; |
|||
|
|||
export function relativeDateTime(raw: string): string { |
|||
const date = new Date(raw); |
|||
const now = new Date(); |
|||
|
|||
const isThisYear = date.getFullYear() === now.getFullYear(); |
|||
const isThisMonth = isThisYear && (date.getMonth() === now.getMonth()); |
|||
const isToday = isThisMonth && (date.getDate() === now.getDate()); |
|||
|
|||
if (isToday) { |
|||
return formatTime(date); |
|||
} |
|||
|
|||
return formatDate(raw.substring(0, 10)); |
|||
} |
|||
|
|||
export function formatTime(date: string | Date): string { |
|||
if (typeof date === "string") { |
|||
date = new Date(date); |
|||
} |
|||
|
|||
const hour = `${date.getHours()}`.padStart(2, "0"); |
|||
const minute = `${date.getMinutes()}`.padStart(2, "0"); |
|||
|
|||
return `${hour}:${minute}`; |
|||
} |
|||
|
|||
export function formatDate(raw: string): string { |
|||
const date = new Date(raw); |
|||
const now = new Date(); |
|||
|
|||
const isLastWeek = date.getTime() > (now.getTime() - sevenDays); |
|||
if (isLastWeek) { |
|||
return weekDayNames[date.getDay()]; |
|||
} |
|||
|
|||
const isThisYear = date.getFullYear() === (new Date()).getFullYear(); |
|||
|
|||
let dateName = `${date.getDate()}. ${monthNames[date.getMonth()]}`; |
|||
if (!isThisYear) { |
|||
dateName += ` ${date.getFullYear()}` |
|||
} |
|||
|
|||
return dateName; |
|||
} |
|||
|
|||
const weekDayNames = ["Søndag", "Mandag", "Tirsdag", "Onsdag", "Torsdag", "Fredag", "Lørdag", "Søndag"]; |
|||
const monthNames = ["jan", "feb", "mar", "apr", "mai", "jun", "jul", "aug", "sep", "okt", "nov", "des"]; |
@ -0,0 +1,3 @@ |
|||
export function unimplemented<T>(): T { |
|||
throw new Error("unimplemented"); |
|||
} |
@ -0,0 +1,36 @@ |
|||
import {Dispatch, SetStateAction, useEffect, useMemo, useState} from "react"; |
|||
|
|||
export function useKey(keys: string | string[], func: () => void, deps: any[] = []) { |
|||
useEffect(() => { |
|||
const f = (ev: KeyboardEvent) => { |
|||
const fixedKey = Array.isArray(keys) ? keys : [keys]; |
|||
|
|||
if (fixedKey.includes(ev.key)) { |
|||
func(); |
|||
} |
|||
}; |
|||
|
|||
window.addEventListener("keydown", f); |
|||
|
|||
return () => { |
|||
window.removeEventListener("keydown", f); |
|||
}; |
|||
}, [keys, func, ...deps]); |
|||
} |
|||
|
|||
export function usePlusMinus(length: number): [number, Dispatch<SetStateAction<number>>] { |
|||
const [current, setCurrent] = useState(0); |
|||
|
|||
useKey("-", () => setCurrent(prev => prev <= 0 ? length - 1 : prev - 1), [length]); |
|||
useKey("+", () => setCurrent(prev => prev >= length - 1 ? 0 : prev + 1), [length]); |
|||
|
|||
useEffect(() => { |
|||
if (current < 0) { |
|||
setCurrent(0); |
|||
} else if (current > length - 1) { |
|||
setCurrent(length - 1); |
|||
} |
|||
}, [length]); |
|||
|
|||
return [current, setCurrent]; |
|||
} |
@ -0,0 +1,9 @@ |
|||
import React from 'react' |
|||
import ReactDOM from 'react-dom/client' |
|||
import App from './App' |
|||
|
|||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( |
|||
<React.StrictMode> |
|||
<App /> |
|||
</React.StrictMode> |
|||
) |
@ -0,0 +1,5 @@ |
|||
export interface Device { |
|||
id: string |
|||
name: string |
|||
connectionString: string |
|||
} |
@ -0,0 +1,50 @@ |
|||
import {Values} from "./Shared"; |
|||
|
|||
export interface Program { |
|||
id: string |
|||
name: string |
|||
steps: ProgramStep[] |
|||
} |
|||
|
|||
export interface ProgramStep { |
|||
index: number |
|||
values: Values, |
|||
duration?: Values, |
|||
} |
|||
|
|||
export function weighting(step: ProgramStep) { |
|||
if (step.duration) { |
|||
if (step.duration.time) { |
|||
return 8 * step.duration.time; |
|||
} else if (step.duration.calories) { |
|||
return 4 * step.duration.calories; |
|||
} else if (step.duration.distance) { |
|||
return step.duration.distance; |
|||
} |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
export function subTitleOfProgram(program: Program): string { |
|||
let secSum = 0; |
|||
let kcalSum = 0; |
|||
let mSum = 0; |
|||
let hasCustom = false; |
|||
|
|||
for (const step of program.steps) { |
|||
if (step.duration?.time) secSum += step.duration.time; |
|||
if (step.duration?.calories) kcalSum += step.duration.calories; |
|||
if (step.duration?.distance) mSum += step.duration.distance; |
|||
|
|||
hasCustom = hasCustom || (!(step.duration?.time) && !(step.duration?.calories) && !(step.duration?.distance)); |
|||
} |
|||
|
|||
const parts = []; |
|||
if (secSum > 0) parts.push(`${secSum} kcal`); |
|||
if (kcalSum > 0) parts.push(`${kcalSum} kcal`); |
|||
if (mSum > 0) parts.push(`${mSum} kcal`); |
|||
if (hasCustom) parts.push("Custom"); |
|||
|
|||
return parts.join(" + "); |
|||
} |
@ -0,0 +1,53 @@ |
|||
export enum Size { |
|||
Mobile = "mobile", |
|||
Tablet = "tablet", |
|||
Desktop = "desktop", |
|||
Any = "any", |
|||
} |
|||
|
|||
export interface ValuesWithTime extends Values { |
|||
time: number |
|||
} |
|||
|
|||
export interface Values { |
|||
time?: number |
|||
calories?: number |
|||
distance?: number |
|||
level?: number |
|||
} |
|||
|
|||
export type ColorName = "gray" | "green" | "blue" | "red" | "yellow" | "indigo"; |
|||
|
|||
export function diffLinearValues<T extends Values>(a: Values, b: Values): T { |
|||
const c: Values = {}; |
|||
if (a.time) c.time = a.time - (b.time || 0); |
|||
if (a.calories) c.calories = a.calories - (b.calories || 0); |
|||
if (a.distance) c.distance = a.distance - (b.distance || 0); |
|||
if (a.level) c.level = a.level; |
|||
|
|||
return c as T; |
|||
} |
|||
|
|||
export function formatValue(raw: number, type: keyof Values): string { |
|||
if (type === "time") { |
|||
const minutes = Math.floor(raw / 60).toString(10).padStart(2, "0"); |
|||
const seconds = (raw % 60).toString(10).padStart(2, "0"); |
|||
|
|||
return `${minutes}'${seconds}"`; |
|||
} |
|||
if (type === "calories") { |
|||
return `${raw} kcal`; |
|||
} |
|||
if (type === "distance") { |
|||
if (raw >= 100) { |
|||
return `${(raw / 1000).toFixed(1)} km` |
|||
} else { |
|||
return `${raw} m`; |
|||
} |
|||
} |
|||
if (type === "level") { |
|||
return `Lvl. ${raw}`; |
|||
} |
|||
|
|||
return ""; |
|||
} |
@ -0,0 +1,66 @@ |
|||
import {Program} from "./Programs"; |
|||
import {Device} from "./Devices"; |
|||
import {ColorName, formatValue, Values, ValuesWithTime} from "./Shared"; |
|||
|
|||
interface Workout<D> { |
|||
id: string |
|||
createdAt: string |
|||
device: Device | null |
|||
program: Program | null |
|||
status: WorkoutStatus |
|||
message: string |
|||
test: string |
|||
} |
|||
export type CurrentWorkout = Workout<Device>; |
|||
export type PastWorkout = Workout<Device | null>; |
|||
|
|||
export type WorkoutState = ValuesWithTime; |
|||
|
|||
export enum WorkoutStatus { |
|||
Created = "Created", |
|||
Connected = "Connected", |
|||
Started = "Started", |
|||
Stopped = "Stopped", |
|||
Disconnected = "Disconnected", |
|||
} |
|||
|
|||
export function colorOf(workout: CurrentWorkout | PastWorkout): ColorName { |
|||
if (workout.message) { |
|||
return "red"; |
|||
} |
|||
|
|||
else return colorByStatus[workout.status]; |
|||
} |
|||
|
|||
const colorByStatus: Record<WorkoutStatus, ColorName> = { |
|||
[WorkoutStatus.Created]: "indigo", |
|||
[WorkoutStatus.Connected]: "blue", |
|||
[WorkoutStatus.Started]: "green", |
|||
[WorkoutStatus.Stopped]: "yellow", |
|||
[WorkoutStatus.Disconnected]: "gray", |
|||
} |
|||
|
|||
export function firstKey(state: WorkoutState): (keyof WorkoutState) | null { |
|||
if (state.time !== undefined) return "time"; |
|||
if (state.calories !== undefined) return "calories"; |
|||
if (state.distance !== undefined) return "distance"; |
|||
if (state.level !== undefined) return "level"; |
|||
return null; |
|||
} |
|||
|
|||
export function stateString(state: Values, type: keyof WorkoutState): string | null { |
|||
if (type === "time" && state.time) { |
|||
return formatValue(state.time, type); |
|||
} |
|||
if (type === "calories" && state.calories !== undefined) { |
|||
return formatValue(state.calories, type); |
|||
} |
|||
if (type === "distance" && state.distance !== undefined) { |
|||
return formatValue(state.distance, type); |
|||
} |
|||
if (type === "level" && state.level !== undefined) { |
|||
return formatValue(state.level, type); |
|||
} |
|||
|
|||
return null; |
|||
} |
@ -0,0 +1,363 @@ |
|||
import Page, {PageBody, PageFlexColumn, PageFlexRow} from "../primitives/page/Page"; |
|||
import Header, {HeaderButton, HeaderTitle} from "../primitives/header/Header"; |
|||
import {TitleLine} from "../primitives/misc/Misc"; |
|||
import {Size} from "../models/Shared"; |
|||
import Blob, {BlobInput, BlobText} from "../primitives/blob/Blob"; |
|||
import {Icon} from "../primitives/Shared"; |
|||
import { |
|||
faChain, |
|||
faCheck, faChevronDown, |
|||
faChevronLeft, |
|||
faChevronUp, faCircleInfo, faInfoCircle, faMessage, |
|||
faPencilAlt, |
|||
faTag, |
|||
faTrashCan |
|||
} from "@fortawesome/free-solid-svg-icons"; |
|||
import {useCallback, useContext, useEffect, useMemo, useState} from "react"; |
|||
import LoadingPage from "./LoadingPage"; |
|||
import {useNavigate, useParams} from "react-router"; |
|||
import DeviceContext from "../contexts/DeviceContext"; |
|||
import {useSearchParams} from "react-router-dom"; |
|||
import {Device} from "../models/Devices"; |
|||
import deviceRepo from "../actions/devices"; |
|||
import RuntimeContext from "../contexts/RuntimeContext"; |
|||
|
|||
export default function DevicePage(): JSX.Element { |
|||
const {devices} = useContext(DeviceContext); |
|||
const navigate = useNavigate(); |
|||
const {id} = useParams(); |
|||
const [search] = useSearchParams(); |
|||
|
|||
const device = useMemo(() => devices?.find(d => d.id === id) || null, [devices]); |
|||
const edit = useMemo(() => search.has("edit") && search.get("edit") === "true", [search, device]); |
|||
|
|||
if (edit && id === "new") { |
|||
return <NewDevicePage/>; |
|||
} else if (edit && device) { |
|||
return <EditDevicePage device={device}/>; |
|||
} else if (device !== null) { |
|||
return <ViewDevicePage device={device}/>; |
|||
} else { |
|||
if (devices !== null) { |
|||
navigate("/"); |
|||
} |
|||
|
|||
return <LoadingPage text="Henter enhet"/>; |
|||
} |
|||
} |
|||
|
|||
function NewDevicePage(): JSX.Element { |
|||
const navigate = useNavigate(); |
|||
const {refreshDevices} = useContext(DeviceContext); |
|||
|
|||
const [name, setName] = useState<string>(""); |
|||
const [connectionString, setConnectionString] = useState<string>(""); |
|||
const [wait, setWait] = useState<boolean>(); |
|||
|
|||
const onSave = useCallback(async () => { |
|||
setWait(true); |
|||
if (await deviceRepo().save({name, connectionString})) { |
|||
refreshDevices(); |
|||
navigate("/"); |
|||
} else { |
|||
setWait(false); |
|||
} |
|||
}, [name, connectionString]); |
|||
|
|||
if (wait) { |
|||
return <LoadingPage text="Legger til enhet"/>; |
|||
} |
|||
|
|||
return ( |
|||
<Page> |
|||
<Header> |
|||
<HeaderButton onClick={() => navigate("/")}> |
|||
<Icon value={faChevronLeft}/> |
|||
</HeaderButton> |
|||
<HeaderTitle>Ny enhet</HeaderTitle> |
|||
</Header> |
|||
<PageBody> |
|||
<PageFlexRow collapseOn={Size.Mobile}> |
|||
<PageFlexColumn flex={1}> |
|||
<TitleLine>Enhet</TitleLine> |
|||
<Blob fillOn={Size.Any}> |
|||
<BlobText>Navn</BlobText> |
|||
<BlobInput flex={1} type="text" value={name} onChange={setName}/> |
|||
</Blob> |
|||
<Blob fillOn={Size.Any}> |
|||
<BlobText>Adresse</BlobText> |
|||
<BlobInput flex={1} type="text" value={connectionString} onChange={setConnectionString}/> |
|||
</Blob> |
|||
<Blob color="green" onClick={onSave}> |
|||
<BlobText> |
|||
<Icon value={faCheck}/> Lagre |
|||
</BlobText> |
|||
</Blob> |
|||
</PageFlexColumn> |
|||
<PageFlexColumn flex={1}/> |
|||
</PageFlexRow> |
|||
</PageBody> |
|||
</Page> |
|||
); |
|||
} |
|||
|
|||
|
|||
interface EditDevicePageProps { |
|||
device: Device |
|||
} |
|||
|
|||
function EditDevicePage({device}: EditDevicePageProps): JSX.Element { |
|||
const navigate = useNavigate(); |
|||
const {refreshDevices} = useContext(DeviceContext); |
|||
|
|||
const [name, setName] = useState<string>(device.name); |
|||
const [connectionString, setConnectionString] = useState<string>(device.connectionString); |
|||
const [wait, setWait] = useState<boolean>(); |
|||
|
|||
const onSave = useCallback(async () => { |
|||
setWait(true); |
|||
if (await deviceRepo().save({id: device.id, name, connectionString})) { |
|||
refreshDevices(); |
|||
navigate(`/devices/${device.id}`); |
|||
} else { |
|||
setWait(false); |
|||
} |
|||
}, [device, name, connectionString]); |
|||
|
|||
if (wait) { |
|||
return <LoadingPage text="Oppdaterer enhet"/>; |
|||
} |
|||
|
|||
return ( |
|||
<Page> |
|||
<Header> |
|||
<HeaderButton onClick={() => navigate(`/devices/${device.id}`)}> |
|||
<Icon value={faChevronLeft}/> |
|||
</HeaderButton> |
|||
<HeaderTitle>Endre «{device.name}»</HeaderTitle> |
|||
</Header> |
|||
<PageBody> |
|||
<PageFlexRow collapseOn={Size.Mobile}> |
|||
<PageFlexColumn flex={1}> |
|||
<TitleLine>Enhet</TitleLine> |
|||
<Blob fillOn={Size.Any}> |
|||
<BlobText>Navn</BlobText> |
|||
<BlobInput flex={1} type="text" value={name} onChange={setName}/> |
|||
</Blob> |
|||
<Blob fillOn={Size.Any}> |
|||
<BlobText>Adresse</BlobText> |
|||
<BlobInput flex={1} type="text" value={connectionString} onChange={setConnectionString}/> |
|||
</Blob> |
|||
<Blob color="green" onClick={onSave}> |
|||
<BlobText> |
|||
<Icon value={faCheck}/> Lagre |
|||
</BlobText> |
|||
</Blob> |
|||
</PageFlexColumn> |
|||
<PageFlexColumn flex={1}/> |
|||
</PageFlexRow> |
|||
</PageBody> |
|||
</Page> |
|||
); |
|||
} |
|||
|
|||
interface DevicePageProps { |
|||
device: Device |
|||
} |
|||
|
|||
function ViewDevicePage({device}: DevicePageProps): JSX.Element { |
|||
const navigate = useNavigate(); |
|||
const {refreshDevices} = useContext(DeviceContext); |
|||
const {active, ended, error, create} = useContext(RuntimeContext); |
|||
|
|||
const onDelete = useCallback(() => { |
|||
if (!window.confirm("Vil du fjerne denne enheten?")) return; |
|||
|
|||
deviceRepo().delete(device.id).then(() => { |
|||
refreshDevices(); |
|||
navigate("/"); |
|||
}) |
|||
}, [device, navigate, refreshDevices]); |
|||
|
|||
return ( |
|||
<Page> |
|||
<Header> |
|||
<HeaderButton onClick={() => navigate("/")}> |
|||
<Icon value={faChevronLeft}/> |
|||
</HeaderButton> |
|||
<HeaderTitle>{device.name}</HeaderTitle> |
|||
</Header> |
|||
<PageBody> |
|||
<PageFlexRow collapseOn={Size.Mobile}> |
|||
<PageFlexColumn flex={1}> |
|||
<TitleLine>Enhet</TitleLine> |
|||
<Blob> |
|||
<BlobText> |
|||
<Icon value={faTag}/> {device.name} |
|||
</BlobText> |
|||
</Blob> |
|||
<Blob> |
|||
<BlobText> |
|||
<Icon value={faChain}/> {device.connectionString} |
|||
</BlobText> |
|||
</Blob> |
|||
<Blob color="indigo" onClick={() => navigate(`/devices/${device.id}?edit=true`)}> |
|||
<BlobText> |
|||
<Icon value={faPencilAlt}/> |
|||
</BlobText> |
|||
</Blob> |
|||
<Blob color="red" onClick={onDelete}> |
|||
<BlobText> |
|||
<Icon value={faTrashCan}/> |
|||
</BlobText> |
|||
</Blob> |
|||
</PageFlexColumn> |
|||
<PageFlexColumn flex={1}> |
|||
<TitleLine>Test</TitleLine> |
|||
{active && !ended ? <TestSection/> : ( |
|||
<Blob |
|||
color={error ? "red" : (ended ? "green" : "yellow")} |
|||
onClick={() => create({deviceId: device.id, test: true})} |
|||
> |
|||
{ended && error && <BlobText>Prøv igjen</BlobText>} |
|||
{ended && !error && <BlobText>Vellykket</BlobText>} |
|||
{!ended && <BlobText>Kjør</BlobText>} |
|||
</Blob> |
|||
)} |
|||
</PageFlexColumn> |
|||
</PageFlexRow> |
|||
</PageBody> |
|||
</Page> |
|||
); |
|||
} |
|||
|
|||
interface Event { |
|||
createdAt: string, |
|||
type: "up" | "down" | "log", |
|||
message: string |
|||
} |
|||
|
|||
function currentTime(): string { |
|||
return (new Date().toTimeString().substring(0, 8)); |
|||
} |
|||
|
|||
function TestSection(): JSX.Element { |
|||
const {lastEvent, connect, disconnect, start, stop} = useContext(RuntimeContext); |
|||
const [events, setEvents] = useState<Event[]>([]); |
|||
|
|||
useEffect(() => { |
|||
const timeouts: number[] = []; |
|||
|
|||
if (lastEvent) { |
|||
const createdAt = (new Date().toTimeString().substring(0, 8)); |
|||
|
|||
if (lastEvent.workout) { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt, type: "down", message: `Opprettet økt ${lastEvent.workout!.id}`}, |
|||
{createdAt, type: "log", message: "Vil koble til om 5 sekunder"}, |
|||
]); |
|||
|
|||
timeouts.push(setTimeout(() => { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt: currentTime(), type: "up", message: `Kobler til`}, |
|||
]); |
|||
connect(); |
|||
}, 5000)); |
|||
} |
|||
if (lastEvent.workoutStates && lastEvent.workoutStates.length > 0) { |
|||
const last = lastEvent.workoutStates[lastEvent.workoutStates.length - 1]; |
|||
|
|||
setEvents(prev => [...prev, { |
|||
createdAt, type: "down", message: `Ny tilstand: ${last.time} s, ${last.calories} kcal`, |
|||
}]) |
|||
} |
|||
if (lastEvent.event) { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt, type: "down", message: `Ny hendelse: ${lastEvent.event!.name}`}, |
|||
]); |
|||
|
|||
if (lastEvent.event.name === "Connected") { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt, type: "log", message: "Vil starte om 5 sekunder..."}, |
|||
]); |
|||
|
|||
timeouts.push(setTimeout(() => { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt: currentTime(), type: "up", message: "Starter"}, |
|||
]); |
|||
start(); |
|||
}, 5000)); |
|||
} |
|||
|
|||
if (lastEvent.event.name === "Started") { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt, type: "log", message: "Vil stoppe om 30 sekunder..."}, |
|||
]); |
|||
|
|||
timeouts.push(setTimeout(() => { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt: currentTime(), type: "up", message: "Stopper"}, |
|||
]); |
|||
stop(); |
|||
}, 30000)); |
|||
} |
|||
|
|||
if (lastEvent.event.name === "Stopped") { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt, type: "log", message: "Vil koble fra om 5 sekunder..."}, |
|||
]); |
|||
|
|||
timeouts.push(setTimeout(() => { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt: currentTime(), type: "up", message: "Kobler fra"}, |
|||
]); |
|||
disconnect(); |
|||
}, 5000)); |
|||
} |
|||
|
|||
|
|||
if (lastEvent.event.name === "Disconnected") { |
|||
setEvents(prev => [ |
|||
...prev, |
|||
{createdAt, type: "log", message: "Frakoblet"}, |
|||
]); |
|||
} |
|||
} |
|||
|
|||
} else { |
|||
setEvents([]); |
|||
} |
|||
}, [lastEvent]); |
|||
|
|||
return ( |
|||
<> |
|||
{events.map((e, i) => ( |
|||
<PageFlexRow key={i}> |
|||
<Blob> |
|||
<BlobText>{e.createdAt}</BlobText> |
|||
</Blob> |
|||
<Blob> |
|||
<BlobText> |
|||
{e.type === "up" && <Icon value={faChevronUp}/>} |
|||
{e.type === "down" && <Icon value={faChevronDown}/>} |
|||
{e.type === "log" && <Icon value={faMessage}/>} |
|||
</BlobText> |
|||
</Blob> |
|||
<Blob flex={1}> |
|||
<BlobText>{e.message}</BlobText> |
|||
</Blob> |
|||
</PageFlexRow> |
|||
))} |
|||
<div style={{height: "100px"}}/> |
|||
</> |
|||
); |
|||
} |
@ -0,0 +1,118 @@ |
|||
import Page, {PageBody, PageFlexColumn, PageFlexRow} from "../primitives/page/Page"; |
|||
import Header, {HeaderTitle} from "../primitives/header/Header"; |
|||
import {TitleLine} from "../primitives/misc/Misc"; |
|||
import {Size} from "../models/Shared"; |
|||
import Blob, {BlobText, BlobTextLine} from "../primitives/blob/Blob"; |
|||
import {Icon} from "../primitives/Shared"; |
|||
import {faChevronDown, faPlay, faPlus} from "@fortawesome/free-solid-svg-icons"; |
|||
import {useContext, useEffect, useMemo} from "react"; |
|||
import ProgramContext from "../contexts/ProgramContext"; |
|||
import LoadingPage from "./LoadingPage"; |
|||
import {subTitleOfProgram} from "../models/Programs"; |
|||
import {useNavigate} from "react-router"; |
|||
import DeviceContext from "../contexts/DeviceContext"; |
|||
import WorkoutContext from "../contexts/WorkoutContext"; |
|||
import {formatDate, formatTime} from "../helpers/dates"; |
|||
import {colorOf, WorkoutStatus} from "../models/Workouts"; |
|||
import {faSpinner} from "@fortawesome/free-solid-svg-icons/faSpinner"; |
|||
import {useKey} from "../hooks/keyboard"; |
|||
import {Boi} from "../primitives/boi/Boi"; |
|||
|
|||
export default function IndexPage(): JSX.Element { |
|||
const {devices} = useContext(DeviceContext); |
|||
const {programs} = useContext(ProgramContext); |
|||
const {workouts, loadingWorkouts, showMoreWorkouts, refreshWorkouts} = useContext(WorkoutContext); |
|||
const navigate = useNavigate(); |
|||
|
|||
const isRunning = useMemo(() => workouts.some(w => w.status !== WorkoutStatus.Disconnected), [workouts]); |
|||
|
|||
useEffect(() => { |
|||
refreshWorkouts(); |
|||
}, [refreshWorkouts]); |
|||
|
|||
useKey(["/", "*"], () => navigate("/play"), [navigate]); |
|||
|
|||
if (programs === null) { |
|||
return <LoadingPage text="Henter programmer"/> |
|||
} else if (devices === null) { |
|||
return <LoadingPage text="Henter enheter"/> |
|||
} |
|||
|
|||
return ( |
|||
<Page> |
|||
<Header> |
|||
<HeaderTitle>YKonsole</HeaderTitle> |
|||
</Header> |
|||
<Boi vertical="bottom" horizontal="left"> |
|||
<Blob onClick={() => navigate("/play")} color={isRunning ? "yellow" : "green"}> |
|||
<BlobText> |
|||
<Icon value={faPlay}/> {isRunning ? "Fortsett" : "Start"} |
|||
</BlobText> |
|||
</Blob> |
|||
</Boi> |
|||
<PageBody> |
|||
<PageFlexRow collapseOn={Size.Tablet}> |
|||
<PageFlexColumn flex={1}> |
|||
<TitleLine>Siste økter ({workouts.length})</TitleLine> |
|||
{workouts.map(w => ( |
|||
<Blob key={w.id} color={colorOf(w)} onClick={() => navigate(`/workouts/${w.id}`)}> |
|||
<BlobText> |
|||
<BlobTextLine>{formatDate(w.createdAt)} {formatTime(w.createdAt)}</BlobTextLine> |
|||
<BlobTextLine secondary> |
|||
{w.program ? w.program.name : (w.device?.name || "Ukjent enhet")} |
|||
</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
))} |
|||
<PageFlexRow> |
|||
<Blob onClick={loadingWorkouts ? undefined : () => showMoreWorkouts()}> |
|||
<BlobText> |
|||
{loadingWorkouts && <Icon value={faSpinner} spin/> } |
|||
{!loadingWorkouts && <><Icon value={faChevronDown}/> Vis flere</> } |
|||
</BlobText> |
|||
</Blob> |
|||
</PageFlexRow> |
|||
</PageFlexColumn> |
|||
|
|||
<PageFlexColumn flex={1}> |
|||
<TitleLine>Programmer ({programs.length})</TitleLine> |
|||
{programs.map(p => ( |
|||
<Blob key={p.id} onClick={() => navigate(`/programs/${p.id}`)}> |
|||
<BlobText> |
|||
<BlobTextLine>{p.name}</BlobTextLine> |
|||
<BlobTextLine secondary>{subTitleOfProgram(p)}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
))} |
|||
<Blob color="green" onClick={() => navigate(`/programs/new?edit=true`)}> |
|||
<BlobText> |
|||
<BlobTextLine> |
|||
<Icon value={faPlus}/> |
|||
</BlobTextLine> |
|||
<BlobTextLine secondary>Legg til</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
|
|||
<TitleLine>Enheter ({devices.length})</TitleLine> |
|||
{devices.map(d => ( |
|||
<Blob key={d.id} onClick={() => navigate(`/devices/${d.id}`)}> |
|||
<BlobText> |
|||
<BlobTextLine>{d.name}</BlobTextLine> |
|||
<BlobTextLine secondary>{d.connectionString}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
))} |
|||
<Blob color="green" onClick={() => navigate(`/devices/new?edit=true`)}> |
|||
<BlobText> |
|||
<BlobTextLine> |
|||
<Icon value={faPlus}/> |
|||
</BlobTextLine> |
|||
<BlobTextLine secondary>Legg til</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
</PageFlexColumn> |
|||
</PageFlexRow> |
|||
</PageBody> |
|||
</Page> |
|||
); |
|||
} |
@ -0,0 +1,40 @@ |
|||
import Page, {PageBody, PageFlexColumn, PageFlexRow} from "../primitives/page/Page"; |
|||
import Header, {HeaderTitle} from "../primitives/header/Header"; |
|||
import {Icon} from "../primitives/Shared"; |
|||
import {faSpinner} from "@fortawesome/free-solid-svg-icons/faSpinner"; |
|||
import {Boi} from "../primitives/boi/Boi"; |
|||
|
|||
interface LoadingPageProps { |
|||
text?: string |
|||
} |
|||
|
|||
function LoadingPage({text}: LoadingPageProps) { |
|||
return ( |
|||
<Page> |
|||
<Header> |
|||
<HeaderTitle>YKonsole</HeaderTitle> |
|||
</Header> |
|||
<PageBody> |
|||
<PageFlexRow vertical> |
|||
<PageFlexRow flex={1}/> |
|||
<LoadingSection text={text}/> |
|||
<PageFlexRow flex={1}/> |
|||
</PageFlexRow> |
|||
</PageBody> |
|||
</Page> |
|||
); |
|||
} |
|||
|
|||
interface LoadingSectionProps { |
|||
text?: string |
|||
} |
|||
|
|||
export function LoadingSection({text}: LoadingSectionProps) { |
|||
return ( |
|||
<Boi vertical="center" horizontal="center"> |
|||
<Icon value={faSpinner} spin/> {text ? `${text}` : ""} |
|||
</Boi> |
|||
); |
|||
} |
|||
|
|||
export default LoadingPage; |
@ -0,0 +1,209 @@ |
|||
import Page, {PageFlexRow} from "../primitives/page/Page"; |
|||
import {useCallback, useContext, useEffect, useMemo, useState} from "react"; |
|||
import RuntimeContext from "../contexts/RuntimeContext"; |
|||
import {useNavigate} from "react-router"; |
|||
import Header, {HeaderButton, HeaderTitle} from "../primitives/header/Header"; |
|||
import LoadingPage from "./LoadingPage"; |
|||
import DeviceContext from "../contexts/DeviceContext"; |
|||
import ProgramContext from "../contexts/ProgramContext"; |
|||
import {Device} from "../models/Devices"; |
|||
import {Program, subTitleOfProgram} from "../models/Programs"; |
|||
import {useKey, usePlusMinus} from "../hooks/keyboard"; |
|||
import {TitleLine} from "../primitives/misc/Misc"; |
|||
import Blob, {BlobText, BlobTextLine} from "../primitives/blob/Blob"; |
|||
import {Icon} from "../primitives/Shared"; |
|||
import {faChevronLeft, faClose, faPlay} from "@fortawesome/free-solid-svg-icons"; |
|||
import {stateString, WorkoutStatus} from "../models/Workouts"; |
|||
import {Boi} from "../primitives/boi/Boi"; |
|||
import {useLastState} from "./runtime/hooks"; |
|||
import {ControlsBoi} from "./runtime/ControlsBoi"; |
|||
import MessageBoi from "./runtime/MessageBoi"; |
|||
import ProgramBoi from "./runtime/ProgramBoi"; |
|||
|
|||
function PlayPage(): JSX.Element { |
|||
const {active, ready, ended, workout, reset, resume} = useContext(RuntimeContext); |
|||
const navigate = useNavigate(); |
|||
|
|||
useEffect(() => { |
|||
if (!active) { |
|||
resume(); |
|||
} else if (active && ended) { |
|||
if (workout) { |
|||
navigate(`/workouts/${workout.id}`, {replace: true}); |
|||
reset(); |
|||
} |
|||
} |
|||
}, [active, ready, ended, workout, resume]); |
|||
|
|||
if (active && ready && workout === null) { |
|||
return <CreatePlayPage/>; |
|||
} |
|||
|
|||
if (active && workout !== null) { |
|||
return <RunPlayPage/>; |
|||
} |
|||
|
|||
return <LoadingPage text="Starter økt"/>; |
|||
} |
|||
|
|||
const noProgram: Program = { |
|||
id: "", |
|||
name: "Uten program", |
|||
steps: [{index: 0, values: {}, duration: undefined}], |
|||
} |
|||
|
|||
function CreatePlayPage(): JSX.Element { |
|||
const {devices} = useContext(DeviceContext); |
|||
const {programs} = useContext(ProgramContext); |
|||
const {create} = useContext(RuntimeContext); |
|||
const programWithFake = useMemo(() => programs ? [noProgram, ...programs] : null, [programs]); |
|||
const navigate = useNavigate(); |
|||
|
|||
const [device, setDevice] = useState<Device | null>(null); |
|||
const [program, setProgram] = useState<Program | null>(null); |
|||
|
|||
const [sel, setSel] = usePlusMinus((device ? (program ? 2 : programWithFake?.length) : devices?.length) || 1); |
|||
|
|||
const confirmSelection = useCallback((idx: number) => { |
|||
if (program && device) { |
|||
if (idx === 0) { |
|||
create({deviceId: device.id, programId: program.id || undefined, test: false}) |
|||
} else { |
|||
navigate("/"); |
|||
} |
|||
} else if (device && programWithFake) { |
|||
setProgram(programWithFake[idx] || null); |
|||
setSel(0); |
|||
} else if (devices !== null) { |
|||
setDevice(devices[idx] || null); |
|||
setSel(0); |
|||
} |
|||
}, [create, device, program, devices, programWithFake]); |
|||
|
|||
useKey("Enter", () => { |
|||
confirmSelection(sel); |
|||
}, [confirmSelection, sel]); |
|||
|
|||
useKey("Escape", () => { |
|||
navigate("/"); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
if (devices && devices.length === 0) { |
|||
navigate("/"); |
|||
} |
|||
}, [devices]); |
|||
|
|||
if (devices === null || programWithFake === null) { |
|||
return <LoadingPage text="Laster inn"/> |
|||
} |
|||
|
|||
return ( |
|||
<Page background={"2046"}> |
|||
<Header> |
|||
<HeaderButton onClick={() => navigate("/")}> |
|||
<Icon value={faChevronLeft}/> |
|||
</HeaderButton> |
|||
<HeaderTitle>Ny økt</HeaderTitle> |
|||
</Header> |
|||
{device === null && ( |
|||
<Boi vertical="center" horizontal="center" style={{fontSize: undefined}}> |
|||
<TitleLine>Velg enhet</TitleLine> |
|||
{devices.map((d, i) => ( |
|||
<Blob key={d.id} onClick={() => confirmSelection(i)} color={sel === i ? "indigo" : "gray"}> |
|||
<BlobText> |
|||
<BlobTextLine>{d.name}</BlobTextLine> |
|||
<BlobTextLine secondary>{d.connectionString}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
))} |
|||
</Boi> |
|||
)} |
|||
|
|||
{device !== null && program === null && ( |
|||
<Boi vertical="center" horizontal="center" style={{fontSize: undefined}}> |
|||
<TitleLine>Velg program</TitleLine> |
|||
{programWithFake.map((p, i) => ( |
|||
<Blob key={p.id} onClick={() => confirmSelection(i)} color={sel === i ? "indigo" : "gray"}> |
|||
<BlobText> |
|||
<BlobTextLine>{p.name}</BlobTextLine> |
|||
<BlobTextLine secondary>{subTitleOfProgram(p)}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
))} |
|||
</Boi> |
|||
)} |
|||
|
|||
{device && program && ( |
|||
<Boi vertical="center" horizontal="center" style={{fontSize: undefined}}> |
|||
<TitleLine>Oppsumering</TitleLine> |
|||
{device && ( |
|||
<Blob> |
|||
<BlobText> |
|||
<BlobTextLine>{device.name}</BlobTextLine> |
|||
<BlobTextLine secondary>{device.connectionString}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
)} |
|||
{program && ( |
|||
<Blob> |
|||
<BlobText> |
|||
<BlobTextLine>{program.name}</BlobTextLine> |
|||
<BlobTextLine secondary>{subTitleOfProgram(program)}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
)} |
|||
<PageFlexRow> |
|||
{device && program && ( |
|||
<> |
|||
<Blob onClick={() => confirmSelection(0)} color={sel === 0 ? "indigo" : "gray"}> |
|||
<BlobText> |
|||
<Icon value={faPlay}/> Start |
|||
</BlobText> |
|||
</Blob> |
|||
<Blob onClick={() => confirmSelection(1)} color={sel === 1 ? "indigo" : "gray"}> |
|||
<BlobText> |
|||
<Icon value={faClose}/> Avbryt |
|||
</BlobText> |
|||
</Blob> |
|||
</> |
|||
)} |
|||
</PageFlexRow> |
|||
</Boi> |
|||
)} |
|||
</Page> |
|||
); |
|||
} |
|||
|
|||
function RunPlayPage(): JSX.Element { |
|||
const {workout} = useContext(RuntimeContext); |
|||
const lastState = useLastState(); |
|||
|
|||
if (!workout || workout.status === WorkoutStatus.Created) { |
|||
return <LoadingPage/>; |
|||
} |
|||
|
|||
return ( |
|||
<Page title="YKonsole" background={"2046"}> |
|||
<ControlsBoi/> |
|||
{lastState && ( |
|||
<Boi vertical="center" horizontal="left"> |
|||
{stateString(lastState, "time")} |
|||
<br/> |
|||
{stateString(lastState, "calories")} |
|||
<br/> |
|||
{stateString(lastState, "distance")} |
|||
<br/> |
|||
{stateString(lastState, "level")} |
|||
</Boi> |
|||
)} |
|||
{workout?.status === WorkoutStatus.Connected && <MessageBoi text="Trykk Enter for å begynne"/>} |
|||
{workout?.status === WorkoutStatus.Stopped && <MessageBoi text="Pause"/>} |
|||
{workout.program && workout.program.steps.length > 0 && <ProgramBoi/>} |
|||
</Page> |
|||
); |
|||
} |
|||
|
|||
|
|||
|
|||
export default PlayPage; |
@ -0,0 +1,105 @@ |
|||
import {useContext, useEffect, useMemo} from "react"; |
|||
import DeviceContext from "../contexts/DeviceContext"; |
|||
import {useNavigate, useParams} from "react-router"; |
|||
import {useSearchParams} from "react-router-dom"; |
|||
import WorkoutContext from "../contexts/WorkoutContext"; |
|||
import {PastWorkout, stateString} from "../models/Workouts"; |
|||
import Page, {PageBody, PageFlexColumn, PageFlexRow} from "../primitives/page/Page"; |
|||
import LoadingPage, {LoadingSection} from "./LoadingPage"; |
|||
import Header, {HeaderButton, HeaderTitle} from "../primitives/header/Header"; |
|||
import {Icon} from "../primitives/Shared"; |
|||
import {faChevronLeft, faClock, faClockFour, faClockRotateLeft, faTableList} from "@fortawesome/free-solid-svg-icons"; |
|||
import {Size} from "../models/Shared"; |
|||
import {TitleLine} from "../primitives/misc/Misc"; |
|||
import Blob, {BlobText, BlobTextLine} from "../primitives/blob/Blob"; |
|||
import {subTitleOfProgram} from "../models/Programs"; |
|||
import {formatDate, formatTime} from "../helpers/dates"; |
|||
|
|||
export default function WorkoutPage(): JSX.Element { |
|||
const {getWorkout, fetchWorkout, getStates, fetchStates} = useContext(WorkoutContext); |
|||
const navigate = useNavigate(); |
|||
const {id} = useParams(); |
|||
|
|||
const workout = useMemo(() => getWorkout(id || "random"), [getWorkout, id]); |
|||
const states = useMemo(() => getStates(id || "random"), [getStates, id]); |
|||
|
|||
useEffect(() => { |
|||
fetchWorkout(id || "random"); |
|||
fetchStates(id || "random"); |
|||
}, [id]); |
|||
|
|||
return ( |
|||
<Page title={`Økt ${id}`}> |
|||
<Header> |
|||
<HeaderButton onClick={() => navigate("/")}> |
|||
<Icon value={faChevronLeft}/> |
|||
</HeaderButton> |
|||
<HeaderTitle>Øktdetaljer</HeaderTitle> |
|||
</Header> |
|||
<PageBody> |
|||
<PageFlexRow collapseOn={Size.Tablet}> |
|||
<PageFlexColumn flex={1}> |
|||
<TitleLine>Økt</TitleLine> |
|||
{workout ? ( |
|||
<> |
|||
<Blob> |
|||
<BlobText> |
|||
<BlobTextLine> |
|||
<Icon value={faClockFour}/> {formatTime(workout.createdAt)} |
|||
</BlobTextLine> |
|||
<BlobTextLine secondary>{formatDate(workout.createdAt)}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
{workout.message && ( |
|||
<Blob color="red"> |
|||
<BlobText> |
|||
<BlobTextLine>Det oppsto en feil!</BlobTextLine> |
|||
<BlobTextLine secondary>{workout.message.trim() || "(Ingen melding)"}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
)} |
|||
{workout.device && ( |
|||
<Blob> |
|||
<BlobText> |
|||
<BlobTextLine>{workout.device.name}</BlobTextLine> |
|||
<BlobTextLine secondary>{workout.device.connectionString}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
)} |
|||
{workout.program && ( |
|||
<Blob> |
|||
<BlobText> |
|||
<BlobTextLine>{workout.program.name}</BlobTextLine> |
|||
<BlobTextLine secondary>{subTitleOfProgram(workout.program)}</BlobTextLine> |
|||
</BlobText> |
|||
</Blob> |
|||
)} |
|||
</> |
|||
) : <LoadingSection/>} |
|||
</PageFlexColumn> |
|||
<PageFlexColumn flex={1}> |
|||
<TitleLine>Målinger</TitleLine> |
|||
{states ? ( |
|||
states.map(s => ( |
|||
<PageFlexRow> |
|||
<Blob> |
|||
<BlobText>{stateString(s, "time")}</BlobText> |
|||
</Blob> |
|||
<Blob> |
|||
<BlobText>{stateString(s, "calories")}</BlobText> |
|||
</Blob> |
|||
<Blob> |
|||
<BlobText>{stateString(s, "distance")}</BlobText> |
|||
</Blob> |
|||
<Blob> |
|||
<BlobText>{stateString(s, "level")}</BlobText> |
|||
</Blob> |
|||
</PageFlexRow> |
|||
)) |
|||
) : <LoadingSection/>} |
|||
</PageFlexColumn> |
|||
</PageFlexRow> |
|||
</PageBody> |
|||
</Page> |
|||
); |
|||
} |
@ -0,0 +1,106 @@ |
|||
import {useContext, useEffect, useMemo, useState} from "react"; |
|||
import RuntimeContext from "../../contexts/RuntimeContext"; |
|||
import {WorkoutStatus} from "../../models/Workouts"; |
|||
import {faPause, faPlay} from "@fortawesome/free-solid-svg-icons"; |
|||
import {useKey, usePlusMinus} from "../../hooks/keyboard"; |
|||
import {Boi} from "../../primitives/boi/Boi"; |
|||
import {TitleLine} from "../../primitives/misc/Misc"; |
|||
import Blob, {BlobText} from "../../primitives/blob/Blob"; |
|||
import {Icon} from "../../primitives/Shared"; |
|||
import {useLastState} from "./hooks"; |
|||
import {IconDefinition} from "@fortawesome/fontawesome-svg-core"; |
|||
import MessageBoi from "./MessageBoi"; |
|||
|
|||
interface Option { |
|||
icon?: IconDefinition |
|||
text?: string |
|||
|
|||
onClick(): void |
|||
} |
|||
|
|||
export function ControlsBoi() { |
|||
const {workout, disconnect, start, stop, setLevel} = useContext(RuntimeContext); |
|||
const lastState = useLastState(); |
|||
|
|||
const [mode, setMode] = useState<"default" | "level">("default"); |
|||
|
|||
const options: Option[] = useMemo(() => { |
|||
if (!workout) return []; |
|||
|
|||
const isStopped = workout.status === WorkoutStatus.Connected || workout.status === WorkoutStatus.Stopped; |
|||
|
|||
const btnList: Option[] = []; |
|||
if (isStopped) { |
|||
btnList.push({icon: faPlay, onClick: start}) |
|||
} |
|||
|
|||
if (workout.status === WorkoutStatus.Started) { |
|||
btnList.push({icon: faPause, onClick: stop}); |
|||
|
|||
if (!workout.program) { |
|||
btnList.push({ |
|||
text: "Motstand", onClick: () => { |
|||
setMode("level"); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
if (isStopped) { |
|||
btnList.push({text: "Avslutt", onClick: disconnect}); |
|||
} |
|||
|
|||
return btnList; |
|||
}, [workout]); |
|||
|
|||
const [sel, setSel] = usePlusMinus(mode === "level" ? 32 : options.length); |
|||
|
|||
useKey("Enter", () => { |
|||
if (mode === "level") { |
|||
setLevel(sel + 1); |
|||
setSel(0); |
|||
setMode("default"); |
|||
} else { |
|||
if (options[sel]) { |
|||
options[sel].onClick(); |
|||
} |
|||
} |
|||
|
|||
setSel(0); |
|||
}, [options, sel]); |
|||
|
|||
useKey("Escape", () => { |
|||
if (!workout) return; |
|||
|
|||
const isStopped = workout.status === WorkoutStatus.Connected || workout.status === WorkoutStatus.Stopped; |
|||
|
|||
if (isStopped) { |
|||
disconnect(); |
|||
} |
|||
|
|||
}, [workout, disconnect]); |
|||
|
|||
useEffect(() => { |
|||
if (lastState?.level && mode === "level") { |
|||
setSel(lastState.level - 1); |
|||
} |
|||
}, [mode]); |
|||
|
|||
if (mode === "level") { |
|||
return ( |
|||
<MessageBoi text={`${sel + 1} +/-`}/> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<Boi vertical="top" horizontal="right" style={{fontSize: "2vmax"}}> |
|||
{options.map((o, i) => ( |
|||
<Blob key={i} color={sel === i ? "indigo" : "gray"} onClick={o.onClick}> |
|||
<BlobText> |
|||
{o.icon && <Icon value={o.icon}/>} {o.text} |
|||
</BlobText> |
|||
</Blob> |
|||
))} |
|||
</Boi> |
|||
); |
|||
} |
@ -0,0 +1,13 @@ |
|||
import {Boi} from "../../primitives/boi/Boi"; |
|||
|
|||
interface MessageBoiProps { |
|||
text: string |
|||
} |
|||
|
|||
export default function MessageBoi({text}: MessageBoiProps) { |
|||
return ( |
|||
<Boi vertical="center" horizontal="center" style={{fontSize: "5vmax", fontWeight: "400", paddingTop: "0.125em"}}> |
|||
{text} |
|||
</Boi> |
|||
); |
|||
} |
@ -0,0 +1,56 @@ |
|||
@import "../../primitives/Shared" |
|||
|
|||
.HealthBar |
|||
display: flex |
|||
align-items: flex-end |
|||
position: fixed |
|||
bottom: 0 |
|||
width: 100% |
|||
|
|||
.HealthBar-entry |
|||
background-color: rgba(0, 0, 0, 0.5) |
|||
|
|||
.HealthBar-entry-text |
|||
padding: 2px |
|||
font-size: 3vmax |
|||
|
|||
&:first-child |
|||
padding: 4px |
|||
|
|||
|
|||
.ProgressBar |
|||
padding: 2px |
|||
height: 16px |
|||
opacity: 0.5 |
|||
box-sizing: border-box |
|||
display: flex |
|||
|
|||
&:first-child |
|||
padding-left: 4px |
|||
|
|||
&:last-child |
|||
padding-right: 4px |
|||
|
|||
div |
|||
height: 100% |
|||
|
|||
.ProgressBar-bg |
|||
background-color: black |
|||
|
|||
.ProgressBar-level-5 |
|||
background-color: $red-3 |
|||
|
|||
.ProgressBar-level-4 |
|||
background-color: $yellow-3 |
|||
|
|||
.ProgressBar-level-3 |
|||
background-color: $green-3 |
|||
|
|||
.ProgressBar-level-2 |
|||
background-color: $cyan-3 |
|||
|
|||
.ProgressBar-level-1 |
|||
background-color: $blue-3 |
|||
|
|||
.ProgressBar-level-0 |
|||
background-color: $indigo-3 |
@ -0,0 +1,159 @@ |
|||
import "./ProgramBoi.sass"; |
|||
|
|||
import {useContext, useEffect, useReducer} from "react"; |
|||
import RuntimeContext from "../../contexts/RuntimeContext"; |
|||
import {ProgramStep, weighting} from "../../models/Programs"; |
|||
import {firstKey, stateString, WorkoutState} from "../../models/Workouts"; |
|||
import {diffLinearValues, formatValue} from "../../models/Shared"; |
|||
import {Simulate} from "react-dom/test-utils"; |
|||
import touchMove = Simulate.touchMove; |
|||
|
|||
interface StepMeta { |
|||
actualDuration?: WorkoutState |
|||
} |
|||
|
|||
interface ProgressState { |
|||
steps: (ProgramStep & StepMeta)[] |
|||
currentIndex: number |
|||
lastTransition: WorkoutState |
|||
lastValue: WorkoutState |
|||
toNext: {current: number, max: number} |
|||
stopped: boolean |
|||
} |
|||
|
|||
interface ProgressChange { |
|||
skip?: boolean |
|||
workoutState?: WorkoutState |
|||
} |
|||
|
|||
function programReducer(state: ProgressState, change: ProgressChange) { |
|||
let {steps, currentIndex, lastTransition, lastValue, toNext, stopped} = state; |
|||
|
|||
// Stop working if after program
|
|||
if (stopped) { |
|||
return state; |
|||
} else if (currentIndex > steps.length - 1) { |
|||
return {...state, stopped: true}; |
|||
} |
|||
|
|||
// Skip
|
|||
if (change.skip) { |
|||
steps[currentIndex].actualDuration = diffLinearValues(lastValue, lastTransition); |
|||
|
|||
return { |
|||
...state, |
|||
steps, |
|||
lastTransition: lastValue, |
|||
currentIndex: currentIndex + 1, |
|||
}; |
|||
} |
|||
|
|||
// Workout state
|
|||
if (change.workoutState) { |
|||
lastValue = change.workoutState; |
|||
const step = steps[currentIndex]; |
|||
|
|||
if (step.duration) { |
|||
if (step.duration.time) { |
|||
toNext.current = lastValue.time - lastTransition.time; |
|||
toNext.max = step.duration.time; |
|||
} else if (step.duration.calories && lastTransition.calories !== undefined && lastValue.calories !== undefined) { |
|||
toNext.current = lastValue.calories - lastTransition.calories; |
|||
toNext.max = step.duration.calories; |
|||
} else if (step.duration.distance && lastTransition.distance !== undefined && lastValue.distance !== undefined) { |
|||
toNext.current = lastValue.distance - lastTransition.distance; |
|||
toNext.max = step.duration.distance; |
|||
} |
|||
|
|||
if (toNext.current >= toNext.max) { |
|||
steps[currentIndex].actualDuration = diffLinearValues(lastValue, lastTransition); |
|||
currentIndex += 1; |
|||
lastTransition = lastValue; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return {steps, currentIndex, lastTransition, lastValue, toNext, stopped}; |
|||
} |
|||
|
|||
export default function ProgramBoi() { |
|||
const {workout, lastEvent} = useContext(RuntimeContext); |
|||
const program = workout!.program!; |
|||
const [progress, dispatch] = useReducer(programReducer, { |
|||
steps: program.steps, |
|||
currentIndex: 0, |
|||
lastTransition: {time: 0, distance: 0, calories: 0}, |
|||
lastValue: {time: 0}, |
|||
toNext: {current: 0, max: 1}, |
|||
stopped: false, |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
if (lastEvent) { |
|||
for (const workoutState of lastEvent?.workoutStates || []) { |
|||
dispatch({workoutState}); |
|||
} |
|||
|
|||
if (lastEvent?.event?.name === "Skipped") { |
|||
dispatch({skip: true}); |
|||
} |
|||
} |
|||
}, [lastEvent]); |
|||
|
|||
if (progress.steps.some(s => weighting(s) === 0)) { |
|||
// TODO: Non-finite mode
|
|||
return null; |
|||
} |
|||
|
|||
return <HealthBarProgress progress={progress}/>; |
|||
} |
|||
|
|||
interface ProgressProps { |
|||
progress: ProgressState |
|||
} |
|||
|
|||
function HealthBarProgress({progress}: ProgressProps) { |
|||
const offset = 6 - progress.steps.length; |
|||
const steps = [...progress.steps]; |
|||
steps.reverse(); |
|||
|
|||
return ( |
|||
<div className="HealthBar"> |
|||
{steps.map((step) => { |
|||
const stepIndex = progress.steps.indexOf(step); |
|||
const level = progress.steps.indexOf(step) + offset; |
|||
|
|||
const max = Math.max(1, progress.toNext.max); |
|||
const key = firstKey(step.duration as WorkoutState); |
|||
|
|||
const duration = step.duration![key!]!; |
|||
|
|||
const durationStr = formatValue(duration - progress.toNext.current, key!); |
|||
|
|||
return ( |
|||
<div key={step.index} className="HealthBar-entry" style={{flex: weighting(step)}}> |
|||
{stepIndex === progress.currentIndex && ( |
|||
<div className="HealthBar-entry-text"> |
|||
{durationStr} |
|||
</div> |
|||
)} |
|||
<div className="ProgressBar"> |
|||
{stepIndex > progress.currentIndex && ( |
|||
<div className={`ProgressBar-level-${level}`} style={{flex: 1}}/> |
|||
)} |
|||
{stepIndex === progress.currentIndex && ( |
|||
<> |
|||
<div className={`ProgressBar-level-${level}`} style={{flex: max - progress.toNext.current}}/> |
|||
<div className={`ProgressBar-bg`} style={{flex: progress.toNext.current}}/> |
|||
</> |
|||
)} |
|||
{stepIndex < progress.currentIndex && ( |
|||
<div className={`ProgressBar-bg`} style={{flex: 1}}/> |
|||
)} |
|||
</div> |
|||
</div> |
|||
); |
|||
})} |
|||
</div> |
|||
); |
|||
} |
@ -0,0 +1,17 @@ |
|||
import {useContext, useEffect, useState} from "react"; |
|||
import RuntimeContext from "../../contexts/RuntimeContext"; |
|||
import {WorkoutState} from "../../models/Workouts"; |
|||
|
|||
export function useLastState() { |
|||
const {lastEvent} = useContext(RuntimeContext); |
|||
|
|||
const [lastState, setLastState] = useState<WorkoutState | null>(null); |
|||
|
|||
useEffect(() => { |
|||
if (lastEvent?.workoutStates && lastEvent.workoutStates.length > 0) { |
|||
setLastState(lastEvent.workoutStates[lastEvent.workoutStates.length - 1]); |
|||
} |
|||
}, [lastEvent]); |
|||
|
|||
return lastState; |
|||
} |
@ -0,0 +1,37 @@ |
|||
// Raw colors |
|||
$green-1: #375623 |
|||
$green-2: #548235 |
|||
$green-3: #70ad47 |
|||
$green-4: #a9d08e |
|||
$green-5: #c6e0b4 |
|||
$green-6: #e2efda |
|||
$blue-1: #1f4e78 |
|||
$blue-2: #2f75b5 |
|||
$blue-3: #5b9bd5 |
|||
$blue-4: #9bc2e6 |
|||
$blue-5: #bdd7ee |
|||
$blue-6: #ddebf7 |
|||
$cyan-1: #1e484f |
|||
$cyan-2: #3c848d |
|||
$cyan-3: #29b3cc |
|||
$cyan-4: #68c8d0 |
|||
$cyan-5: #86f6fc |
|||
$cyan-6: #c7fcff |
|||
$yellow-1: #806000 |
|||
$yellow-2: #bf8f00 |
|||
$yellow-3: #ffc000 |
|||
$yellow-4: #ffd966 |
|||
$yellow-5: #ffe699 |
|||
$yellow-6: #fff2cc |
|||
$red-1: #833c0c |
|||
$red-2: #c65911 |
|||
$red-3: #ed7d31 |
|||
$red-4: #f4b084 |
|||
$red-5: #f8cbad |
|||
$red-6: #fce4d6 |
|||
$indigo-1: #203764 |
|||
$indigo-2: #305496 |
|||
$indigo-3: #4472c4 |
|||
$indigo-4: #8ea9db |
|||
$indigo-5: #b4c6e7 |
|||
$indigo-6: #d9e1f2 |
@ -0,0 +1,34 @@ |
|||
@import "./Pallette" |
|||
|
|||
// Colors |
|||
$header-background-gray: #424242 |
|||
$header-background: $indigo-1 |
|||
$header-background-focus: $indigo-2 |
|||
$header-foreground: $indigo-6 |
|||
$header-foreground-focus: #fff |
|||
|
|||
$body-background: rgb(24, 24, 24) |
|||
$body-foreground: #e0e0e0 |
|||
$body-foreground-dark: $header-background-gray |
|||
|
|||
$title-line: #c0c0c0 |
|||
|
|||
|
|||
$blob-background: #383838 |
|||
$blob-background-hover: #525252 |
|||
$blob-background-dark: #262626 |
|||
$blob-foreground: #e6e6e6 |
|||
$blob-foreground-clickable: $header-foreground |
|||
$blob-foreground-clickable-hover: $header-foreground-focus |
|||
|
|||
// Sizes |
|||
$width-internal: 1200px |
|||
|
|||
// Sizes |
|||
$mobile-max: 550px |
|||
$tablet-max: 850px |
|||
$desktop-max: 1050px |
|||
|
|||
// Fonts |
|||
$font-global: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif |
|||
$font-header: "Bitstream Vera Serif", serif |
@ -0,0 +1,17 @@ |
|||
import React, {CSSProperties, PropsWithChildren} from "react"; |
|||
import {IconProp} from "@fortawesome/fontawesome-svg-core"; |
|||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; |
|||
|
|||
export type WithChildren = PropsWithChildren<Record<never, never>>; |
|||
|
|||
export type WithStyle = { style?: CSSProperties } |
|||
|
|||
interface IconProps { |
|||
value: IconProp |
|||
spin?: boolean |
|||
flash?: boolean |
|||
} |
|||
|
|||
export function Icon({value, spin, flash}: IconProps) { |
|||
return <FontAwesomeIcon icon={value} spin={spin} beatFade={flash}/>; |
|||
} |
@ -0,0 +1,134 @@ |
|||
@import "../Shared.sass" |
|||
|
|||
.Blob |
|||
margin-top: 0.5em |
|||
border-radius: 0.5em |
|||
display: inline-block |
|||
cursor: default |
|||
margin-left: 0.25em |
|||
margin-right: 0.25em |
|||
|
|||
&.Blob-clickable |
|||
cursor: pointer |
|||
|
|||
&:hover |
|||
color: $blob-foreground-clickable-hover |
|||
|
|||
&.Blob-gray |
|||
background-color: $blob-background |
|||
color: $blob-foreground |
|||
|
|||
&.Blob-clickable:hover |
|||
background-color: $blob-background-hover |
|||
|
|||
&.Blob-green |
|||
background-color: $green-1 |
|||
color: $green-6 |
|||
|
|||
&.Blob-clickable:hover |
|||
background-color: $green-2 |
|||
|
|||
&.Blob-blue |
|||
background-color: $blue-1 |
|||
color: $blue-6 |
|||
|
|||
&.Blob-clickable:hover |
|||
background-color: $blue-2 |
|||
|
|||
&.Blob-cyan |
|||
background-color: $cyan-1 |
|||
color: $cyan-6 |
|||
|
|||
&.Blob-clickable:hover |
|||
background-color: $cyan-2 |
|||
|
|||
&.Blob-yellow |
|||
background-color: $yellow-1 |
|||
color: $yellow-6 |
|||
|
|||
&.Blob-clickable:hover |
|||
background-color: $yellow-2 |
|||
|
|||
&.Blob-red |
|||
background-color: $red-1 |
|||
color: $red-6 |
|||
|
|||
&.Blob-clickable:hover |
|||
background-color: $red-2 |
|||
|
|||
&.Blob-indigo |
|||
background-color: $indigo-1 |
|||
color: $indigo-6 |
|||
|
|||
&.Blob-clickable:hover |
|||
background-color: $indigo-2 |
|||
|
|||
|
|||
&.Blob-fill-on-any |
|||
display: block |
|||
|
|||
@media screen and (max-width: $mobile-max) |
|||
&.Blob-fill-on-mobile |
|||
display: block |
|||
|
|||
@media screen and (max-width: $tablet-max) |
|||
&.Blob-fill-on-tablet |
|||
display: block |
|||
|
|||
@media screen and (max-width: $desktop-max) |
|||
&.Blob-fill-on-desktop |
|||
display: block |
|||
|
|||
.Blob-body |
|||
display: flex |
|||
flex-direction: row |
|||
border-radius: inherit |
|||
|
|||
.BlobText |
|||
padding: 0.5em |
|||
|
|||
&.BlobText-centered |
|||
text-align: center |
|||
|
|||
&.BlobText-text-secondary |
|||
opacity: 0.75 |
|||
|
|||
.BlobTextLine |
|||
&.BlobTextLine-secondary |
|||
opacity: 0.5 |
|||
|
|||
&:not(:last-child) |
|||
padding-bottom: 0.2em |
|||
|
|||
input.BlobInput |
|||
font: inherit |
|||
background-color: $blob-background-dark |
|||
color: inherit |
|||
border: none |
|||
padding: 0.5em |
|||
border-radius: inherit |
|||
|
|||
&:not(:first-child) |
|||
border-bottom-left-radius: 0 |
|||
border-top-left-radius: 0 |
|||
|
|||
&:not(:last-child) |
|||
border-bottom-right-radius: 0 |
|||
border-top-right-radius: 0 |
|||
|
|||
div |
|||
text-wrap: avoid |
|||
|
|||
.BlobGroup |
|||
display: inline-block |
|||
|
|||
.Blob:not(:first-child) |
|||
margin-left: 0 |
|||
border-top-left-radius: 0 |
|||
border-bottom-left-radius: 0 |
|||
|
|||
.Blob:not(:last-child) |
|||
margin-right: 0 |
|||
border-top-right-radius: 0 |
|||
border-bottom-right-radius: 0 |
|||
|
@ -0,0 +1,130 @@ |
|||
import "./Blob.sass"; |
|||
|
|||
import {WithChildren} from "../Shared"; |
|||
import {CSSProperties, useCallback, useMemo} from "react"; |
|||
import {Size} from "../../models/Shared"; |
|||
|
|||
interface BlobProps extends WithChildren { |
|||
onClick?: () => void |
|||
disabled?: boolean |
|||
fillOn?: Size |
|||
flex?: number |
|||
color?: "gray" | "green" | "blue" | "red" | "yellow" | "indigo" |
|||
} |
|||
|
|||
function Blob({onClick, fillOn, disabled, flex, color, children}: BlobProps) { |
|||
const style: CSSProperties = useMemo(() => { |
|||
return flex ? {flex} : {}; |
|||
}, [flex]); |
|||
const classNames = useMemo(() => { |
|||
const val = ["Blob"]; |
|||
if (onClick !== undefined && !disabled) { |
|||
val.push("Blob-clickable") |
|||
} |
|||
if (fillOn) { |
|||
val.push(`Blob-fill-on-${fillOn}`); |
|||
} |
|||
val.push(`Blob-${color || "gray"}`) |
|||
|
|||
return val; |
|||
}, [onClick, fillOn, disabled, color]); |
|||
|
|||
return ( |
|||
<div style={style} className={classNames.join(" ")} onClick={disabled ? undefined : onClick}> |
|||
<div className="Blob-body"> |
|||
{children} |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
interface BlobTextProps extends WithChildren { |
|||
centered?: boolean |
|||
flex?: number |
|||
} |
|||
|
|||
export function BlobText({centered, children, flex}: BlobTextProps) { |
|||
const clazz = useMemo(() => { |
|||
const classNames = ["BlobText"]; |
|||
if (centered) { |
|||
classNames.push("BlobText-centered"); |
|||
} |
|||
|
|||
return classNames.join(" "); |
|||
}, [centered]); |
|||
|
|||
return ( |
|||
<div className={clazz} style={{flex}}> |
|||
{children} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
interface BlobTextLineProps extends WithChildren { |
|||
secondary?: boolean |
|||
flex?: number |
|||
} |
|||
|
|||
export function BlobTextLine({secondary, flex, children}: BlobTextLineProps) { |
|||
const clazz = useMemo(() => { |
|||
const classNames = ["BlobTextLine"]; |
|||
if (secondary) { |
|||
classNames.push("BlobTextLine-secondary"); |
|||
} |
|||
|
|||
return classNames.join(" "); |
|||
}, [secondary]); |
|||
|
|||
return ( |
|||
<div className={clazz} style={{flex}}> |
|||
{children} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
type BlobInputProps = |
|||
| BaseBlobInputProps<"number", number> |
|||
| BaseBlobInputProps<"text" | "password" | undefined, string> |
|||
|
|||
interface BaseBlobInputProps<T, V> { |
|||
type: T |
|||
name?: string |
|||
flex?: number |
|||
value: V |
|||
disabled?: boolean |
|||
onChange?: (newValue: V) => void |
|||
} |
|||
|
|||
export function BlobInput({type, name, flex, disabled, value, onChange}: BlobInputProps) { |
|||
const actualOnChange = useCallback((input: string) => { |
|||
if (onChange === undefined) return; |
|||
|
|||
if (type === "number") { |
|||
onChange(parseInt(input, 10) || 0); |
|||
} else { |
|||
onChange(input); |
|||
} |
|||
}, [onChange]); |
|||
|
|||
return ( |
|||
<input |
|||
disabled={disabled} |
|||
className="BlobInput" |
|||
name={name} |
|||
type={type || "text"} |
|||
value={`${value}`} |
|||
style={{flex}} |
|||
onChange={e => actualOnChange(e.target.value || "")} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
export function BlobGroup({children}: WithChildren) { |
|||
return ( |
|||
<div className="BlobGroup"> |
|||
{children} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default Blob; |
@ -0,0 +1,43 @@ |
|||
@import "../Shared.sass" |
|||
|
|||
.Boi |
|||
background-color: rgba(0, 0, 0, 0.33) |
|||
z-index: 9999 |
|||
padding: 0.25vmax 0.75vmax 1.5vmax |
|||
box-sizing: border-box |
|||
|
|||
.TitleLine |
|||
margin-bottom: 0.25em |
|||
|
|||
&.Boi-v-top |
|||
position: fixed |
|||
top: 0 |
|||
|
|||
&.Boi-v-center |
|||
position: fixed |
|||
top: 50% |
|||
|
|||
&:not(.Boi-h-center) |
|||
transform: translate(0, -50%) |
|||
&.Boi-h-center |
|||
transform: translate(-50%, -50%) |
|||
|
|||
&.Boi-v-bottom |
|||
position: fixed |
|||
bottom: 0 |
|||
|
|||
&.Boi-h-left |
|||
position: fixed |
|||
left: 0 |
|||
|
|||
&.Boi-h-center |
|||
position: fixed |
|||
left: 50% |
|||
&:not(.Boi-v-center) |
|||
transform: translate(-50%, 0) |
|||
|
|||
&.Boi-h-right |
|||
position: fixed |
|||
right: 0 |
|||
|
|||
|
@ -0,0 +1,30 @@ |
|||
import "./Boi.sass"; |
|||
import {WithChildren, WithStyle} from "../Shared"; |
|||
import {useMemo} from "react"; |
|||
|
|||
interface BoiProps extends WithChildren, WithStyle { |
|||
vertical: "top" | "center" | "bottom" |
|||
horizontal: "left" | "center" | "right" |
|||
} |
|||
|
|||
const defaultStyle = {fontSize: "3vmax"}; |
|||
|
|||
export function Boi({horizontal, vertical, children, style}: BoiProps) { |
|||
const className = useMemo(() => { |
|||
const list = [ |
|||
"Boi", |
|||
`Boi-h-${horizontal}`, |
|||
`Boi-v-${vertical}`, |
|||
]; |
|||
|
|||
return list.join(" "); |
|||
}, [horizontal, vertical]); |
|||
|
|||
return ( |
|||
<div className={className}> |
|||
<div className="Boi-content" style={{...defaultStyle, ...(style || {})}}> |
|||
{children} |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
@ -0,0 +1,37 @@ |
|||
@import "../Shared.sass" |
|||
|
|||
.Header |
|||
background-color: $header-background |
|||
color: $header-foreground |
|||
font-family: $font-header |
|||
font-size: 200% |
|||
|
|||
.Header-body |
|||
display: flex |
|||
flex-direction: row |
|||
|
|||
.HeaderTitle |
|||
padding: 0.1em 0.2em |
|||
font-weight: 400 |
|||
|
|||
.HeaderButton |
|||
padding: 0.1em 0.2em |
|||
|
|||
.HeaderTitle |
|||
flex: 1 |
|||
white-space: nowrap |
|||
overflow: hidden |
|||
text-overflow: ellipsis |
|||
|
|||
.HeaderTitle-interior |
|||
cursor: default |
|||
user-select: none |
|||
|
|||
.HeaderButton |
|||
min-width: 0.8em |
|||
text-align: center |
|||
cursor: pointer |
|||
|
|||
&:hover |
|||
background-color: $header-background-focus |
|||
color: $header-foreground-focus |
@ -0,0 +1,55 @@ |
|||
import "./Header.sass"; |
|||
|
|||
import React, {useEffect} from "react"; |
|||
import {WithChildren} from "../Shared"; |
|||
|
|||
export function HeaderTitle({children}: WithChildren) { |
|||
return ( |
|||
<div className="HeaderTitle"> |
|||
<span className="HeaderTitle-interior"> |
|||
{children} |
|||
</span> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
interface HeaderButtonProps extends WithChildren { |
|||
title?: string |
|||
onClick?: () => void |
|||
shortcut?: string |
|||
} |
|||
|
|||
export function HeaderButton({title, onClick, shortcut, children}: HeaderButtonProps) { |
|||
useEffect(() => { |
|||
if (!shortcut || !onClick) { |
|||
return; |
|||
} |
|||
|
|||
const handler = (e: KeyboardEvent) => { |
|||
if (e.key === shortcut) { |
|||
onClick(); |
|||
} |
|||
}; |
|||
|
|||
window.addEventListener("keypress", handler); |
|||
return () => window.removeEventListener("keypress", handler); |
|||
}, [shortcut, onClick]); |
|||
|
|||
return ( |
|||
<div className="HeaderButton" title={title} onClick={onClick}> |
|||
{children} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function Header({children}: WithChildren) { |
|||
return ( |
|||
<div className="Header"> |
|||
<div className="Header-body"> |
|||
{children} |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default Header; |
@ -0,0 +1,10 @@ |
|||
@import "../Shared" |
|||
|
|||
.TitleLine |
|||
margin-left: 0.125em |
|||
margin-right: 0.125em |
|||
font-size: 200% |
|||
margin-top: 0.5em |
|||
padding-bottom: 0.1em |
|||
color: $title-line |
|||
border-bottom: $blob-background 1px solid |
@ -0,0 +1,8 @@ |
|||
import "./Misc.sass"; |
|||
import {WithChildren} from "../Shared"; |
|||
|
|||
export function TitleLine({children}: WithChildren) { |
|||
return ( |
|||
<div className="TitleLine">{children}</div> |
|||
); |
|||
} |
@ -0,0 +1,45 @@ |
|||
@import "../Shared.sass" |
|||
|
|||
.Page |
|||
width: 100% |
|||
height: 100% |
|||
display: flex |
|||
flex-direction: column |
|||
|
|||
&.Page-bg-2046 |
|||
background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url("/2046.png") no-repeat center |
|||
background-size: cover |
|||
|
|||
.PageBody |
|||
flex: 1 |
|||
width: 100% |
|||
max-width: 1200px |
|||
height: 100% |
|||
margin: 0 auto |
|||
|
|||
.PageBody-padding |
|||
height: 2em |
|||
|
|||
.PageFlexRow |
|||
display: flex |
|||
flex-direction: row |
|||
align-items: stretch |
|||
|
|||
&:not(.PageFlexRow-vertical) |
|||
width: 100% |
|||
|
|||
&.PageFlexRow-vertical |
|||
flex-direction: column |
|||
height: 100% |
|||
|
|||
@media screen and (max-width: $mobile-max) |
|||
&.PageFlexRow-collapse-on-mobile |
|||
display: block |
|||
|
|||
@media screen and (max-width: $tablet-max) |
|||
&.PageFlexRow-collapse-on-tablet |
|||
display: block |
|||
|
|||
@media screen and (max-width: $desktop-max) |
|||
&.PageFlexRow-collapse-on-desktop |
|||
display: block |
@ -0,0 +1,76 @@ |
|||
import "./Page.sass"; |
|||
|
|||
import {WithChildren, WithStyle} from "../Shared"; |
|||
import {useLayoutEffect, useMemo} from "react"; |
|||
import {Size} from "../../models/Shared"; |
|||
|
|||
interface PageProps extends WithChildren { |
|||
title?: string |
|||
background?: "2046" |
|||
} |
|||
|
|||
function Page({title, background, children}: PageProps) { |
|||
useLayoutEffect(() => { |
|||
document.title = title ? `${title} - Green` : "Green"; |
|||
}, [title]); |
|||
|
|||
let cls = "Page"; |
|||
if (background) { |
|||
cls += ` Page-bg-${background}`; |
|||
} |
|||
|
|||
return ( |
|||
<div className={cls}> |
|||
{children} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export function PageBody({children, style}: WithChildren & WithStyle) { |
|||
return ( |
|||
<div className="PageBody" style={style}> |
|||
{children} |
|||
<div className="PageBody-padding"/> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
interface PageFlexRowProps extends WithChildren, WithStyle { |
|||
collapseOn?: Size |
|||
flex?: number |
|||
vertical?: boolean |
|||
} |
|||
|
|||
export function PageFlexRow({children, collapseOn, flex, vertical, style}: PageFlexRowProps) { |
|||
const clazz = useMemo(() => { |
|||
const classNames = ["PageFlexRow"]; |
|||
if (collapseOn) { |
|||
classNames.push(`PageFlexRow-collapse-on-${collapseOn}`); |
|||
} |
|||
if (vertical) { |
|||
classNames.push("PageFlexRow-vertical"); |
|||
} |
|||
|
|||
return classNames.join(" "); |
|||
}, [collapseOn, vertical]); |
|||
|
|||
return ( |
|||
<div className={clazz} style={style ? {...style, flex} : {flex}}> |
|||
{children} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
interface PageFlexColumnProps extends WithChildren { |
|||
flex?: number |
|||
} |
|||
|
|||
export function PageFlexColumn({flex = 1, children}: PageFlexColumnProps) { |
|||
const actualFlex = (flex && flex > 0) ? flex : undefined; |
|||
|
|||
return ( |
|||
<span style={{flex: actualFlex}}>{children}</span> |
|||
); |
|||
} |
|||
|
|||
export default Page; |
@ -0,0 +1,8 @@ |
|||
/// <reference types="vite/client" />
|
|||
interface ImportMetaEnv { |
|||
readonly VITE_MODE: "webapp" | "chrome-plugin" |
|||
} |
|||
|
|||
interface ImportMeta { |
|||
readonly env: ImportMetaEnv |
|||
} |
@ -0,0 +1,21 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "ESNext", |
|||
"useDefineForClassFields": true, |
|||
"lib": ["DOM", "DOM.Iterable", "ESNext"], |
|||
"allowJs": false, |
|||
"skipLibCheck": true, |
|||
"esModuleInterop": false, |
|||
"allowSyntheticDefaultImports": true, |
|||
"strict": true, |
|||
"forceConsistentCasingInFileNames": true, |
|||
"module": "ESNext", |
|||
"moduleResolution": "Node", |
|||
"resolveJsonModule": true, |
|||
"isolatedModules": true, |
|||
"noEmit": true, |
|||
"jsx": "react-jsx" |
|||
}, |
|||
"include": ["src"], |
|||
"references": [{ "path": "./tsconfig.node.json" }] |
|||
} |
@ -0,0 +1,9 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"composite": true, |
|||
"module": "ESNext", |
|||
"moduleResolution": "Node", |
|||
"allowSyntheticDefaultImports": true |
|||
}, |
|||
"include": ["vite.config.ts"] |
|||
} |
@ -0,0 +1,24 @@ |
|||
import {defineConfig, loadEnv} from 'vite' |
|||
import react from '@vitejs/plugin-react' |
|||
|
|||
// https://vitejs.dev/config/
|
|||
export default ({mode}) => { |
|||
//@ts-ignore
|
|||
process.env = {...process.env, ...loadEnv(mode, process.cwd())} |
|||
|
|||
return defineConfig({ |
|||
plugins: [react()], |
|||
build: { |
|||
//@ts-ignore
|
|||
outDir: process.env.VITE_MODE === "webapp" ? "./dist-webapp" : "./dist-chrome", |
|||
}, |
|||
server: { |
|||
proxy: { |
|||
"/api": { |
|||
target: 'http://localhost:8080', |
|||
changeOrigin: true, |
|||
}, |
|||
} |
|||
}, |
|||
}); |
|||
} |
@ -0,0 +1,977 @@ |
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. |
|||
# yarn lockfile v1 |
|||
|
|||
|
|||
"@ampproject/remapping@^2.1.0": |
|||
version "2.2.0" |
|||
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" |
|||
integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== |
|||
dependencies: |
|||
"@jridgewell/gen-mapping" "^0.1.0" |
|||
"@jridgewell/trace-mapping" "^0.3.9" |
|||
|
|||
"@babel/code-frame@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" |
|||
integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== |
|||
dependencies: |
|||
"@babel/highlight" "^7.18.6" |
|||
|
|||
"@babel/compat-data@^7.18.8": |
|||
version "7.18.8" |
|||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" |
|||
integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== |
|||
|
|||
"@babel/core@^7.18.10": |
|||
version "7.18.10" |
|||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8" |
|||
integrity sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw== |
|||
dependencies: |
|||
"@ampproject/remapping" "^2.1.0" |
|||
"@babel/code-frame" "^7.18.6" |
|||
"@babel/generator" "^7.18.10" |
|||
"@babel/helper-compilation-targets" "^7.18.9" |
|||
"@babel/helper-module-transforms" "^7.18.9" |
|||
"@babel/helpers" "^7.18.9" |
|||
"@babel/parser" "^7.18.10" |
|||
"@babel/template" "^7.18.10" |
|||
"@babel/traverse" "^7.18.10" |
|||
"@babel/types" "^7.18.10" |
|||
convert-source-map "^1.7.0" |
|||
debug "^4.1.0" |
|||
gensync "^1.0.0-beta.2" |
|||
json5 "^2.2.1" |
|||
semver "^6.3.0" |
|||
|
|||
"@babel/generator@^7.18.10": |
|||
version "7.18.12" |
|||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.12.tgz#fa58daa303757bd6f5e4bbca91b342040463d9f4" |
|||
integrity sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg== |
|||
dependencies: |
|||
"@babel/types" "^7.18.10" |
|||
"@jridgewell/gen-mapping" "^0.3.2" |
|||
jsesc "^2.5.1" |
|||
|
|||
"@babel/helper-annotate-as-pure@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" |
|||
integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== |
|||
dependencies: |
|||
"@babel/types" "^7.18.6" |
|||
|
|||
"@babel/helper-compilation-targets@^7.18.9": |
|||
version "7.18.9" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf" |
|||
integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg== |
|||
dependencies: |
|||
"@babel/compat-data" "^7.18.8" |
|||
"@babel/helper-validator-option" "^7.18.6" |
|||
browserslist "^4.20.2" |
|||
semver "^6.3.0" |
|||
|
|||
"@babel/helper-environment-visitor@^7.18.9": |
|||
version "7.18.9" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" |
|||
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== |
|||
|
|||
"@babel/helper-function-name@^7.18.9": |
|||
version "7.18.9" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0" |
|||
integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A== |
|||
dependencies: |
|||
"@babel/template" "^7.18.6" |
|||
"@babel/types" "^7.18.9" |
|||
|
|||
"@babel/helper-hoist-variables@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" |
|||
integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== |
|||
dependencies: |
|||
"@babel/types" "^7.18.6" |
|||
|
|||
"@babel/helper-module-imports@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" |
|||
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== |
|||
dependencies: |
|||
"@babel/types" "^7.18.6" |
|||
|
|||
"@babel/helper-module-transforms@^7.18.9": |
|||
version "7.18.9" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712" |
|||
integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g== |
|||
dependencies: |
|||
"@babel/helper-environment-visitor" "^7.18.9" |
|||
"@babel/helper-module-imports" "^7.18.6" |
|||
"@babel/helper-simple-access" "^7.18.6" |
|||
"@babel/helper-split-export-declaration" "^7.18.6" |
|||
"@babel/helper-validator-identifier" "^7.18.6" |
|||
"@babel/template" "^7.18.6" |
|||
"@babel/traverse" "^7.18.9" |
|||
"@babel/types" "^7.18.9" |
|||
|
|||
"@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9": |
|||
version "7.18.9" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" |
|||
integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== |
|||
|
|||
"@babel/helper-simple-access@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" |
|||
integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== |
|||
dependencies: |
|||
"@babel/types" "^7.18.6" |
|||
|
|||
"@babel/helper-split-export-declaration@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" |
|||
integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== |
|||
dependencies: |
|||
"@babel/types" "^7.18.6" |
|||
|
|||
"@babel/helper-string-parser@^7.18.10": |
|||
version "7.18.10" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" |
|||
integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== |
|||
|
|||
"@babel/helper-validator-identifier@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" |
|||
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== |
|||
|
|||
"@babel/helper-validator-option@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" |
|||
integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== |
|||
|
|||
"@babel/helpers@^7.18.9": |
|||
version "7.18.9" |
|||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9" |
|||
integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ== |
|||
dependencies: |
|||
"@babel/template" "^7.18.6" |
|||
"@babel/traverse" "^7.18.9" |
|||
"@babel/types" "^7.18.9" |
|||
|
|||
"@babel/highlight@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" |
|||
integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== |
|||
dependencies: |
|||
"@babel/helper-validator-identifier" "^7.18.6" |
|||
chalk "^2.0.0" |
|||
js-tokens "^4.0.0" |
|||
|
|||
"@babel/parser@^7.18.10", "@babel/parser@^7.18.11": |
|||
version "7.18.11" |
|||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" |
|||
integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== |
|||
|
|||
"@babel/plugin-syntax-jsx@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" |
|||
integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== |
|||
dependencies: |
|||
"@babel/helper-plugin-utils" "^7.18.6" |
|||
|
|||
"@babel/plugin-transform-react-jsx-development@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz#dbe5c972811e49c7405b630e4d0d2e1380c0ddc5" |
|||
integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA== |
|||
dependencies: |
|||
"@babel/plugin-transform-react-jsx" "^7.18.6" |
|||
|
|||
"@babel/plugin-transform-react-jsx-self@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz#3849401bab7ae8ffa1e3e5687c94a753fc75bda7" |
|||
integrity sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig== |
|||
dependencies: |
|||
"@babel/helper-plugin-utils" "^7.18.6" |
|||
|
|||
"@babel/plugin-transform-react-jsx-source@^7.18.6": |
|||
version "7.18.6" |
|||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.18.6.tgz#06e9ae8a14d2bc19ce6e3c447d842032a50598fc" |
|||
integrity sha512-utZmlASneDfdaMh0m/WausbjUjEdGrQJz0vFK93d7wD3xf5wBtX219+q6IlCNZeguIcxS2f/CvLZrlLSvSHQXw== |
|||
dependencies: |
|||
"@babel/helper-plugin-utils" "^7.18.6" |
|||
|
|||
"@babel/plugin-transform-react-jsx@^7.18.10", "@babel/plugin-transform-react-jsx@^7.18.6": |
|||
version "7.18.10" |
|||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.10.tgz#ea47b2c4197102c196cbd10db9b3bb20daa820f1" |
|||
integrity sha512-gCy7Iikrpu3IZjYZolFE4M1Sm+nrh1/6za2Ewj77Z+XirT4TsbJcvOFOyF+fRPwU6AKKK136CZxx6L8AbSFG6A== |
|||
dependencies: |
|||
"@babel/helper-annotate-as-pure" "^7.18.6" |
|||
"@babel/helper-module-imports" "^7.18.6" |
|||
"@babel/helper-plugin-utils" "^7.18.9" |
|||
"@babel/plugin-syntax-jsx" "^7.18.6" |
|||
"@babel/types" "^7.18.10" |
|||
|
|||
"@babel/runtime@^7.7.6": |
|||
version "7.18.9" |
|||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" |
|||
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== |
|||
dependencies: |
|||
regenerator-runtime "^0.13.4" |
|||
|
|||
"@babel/template@^7.18.10", "@babel/template@^7.18.6": |
|||
version "7.18.10" |
|||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" |
|||
integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== |
|||
dependencies: |
|||
"@babel/code-frame" "^7.18.6" |
|||
"@babel/parser" "^7.18.10" |
|||
"@babel/types" "^7.18.10" |
|||
|
|||
"@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9": |
|||
version "7.18.11" |
|||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.11.tgz#3d51f2afbd83ecf9912bcbb5c4d94e3d2ddaa16f" |
|||
integrity sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ== |
|||
dependencies: |
|||
"@babel/code-frame" "^7.18.6" |
|||
"@babel/generator" "^7.18.10" |
|||
"@babel/helper-environment-visitor" "^7.18.9" |
|||
"@babel/helper-function-name" "^7.18.9" |
|||
"@babel/helper-hoist-variables" "^7.18.6" |
|||
"@babel/helper-split-export-declaration" "^7.18.6" |
|||
"@babel/parser" "^7.18.11" |
|||
"@babel/types" "^7.18.10" |
|||
debug "^4.1.0" |
|||
globals "^11.1.0" |
|||
|
|||
"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9": |
|||
version "7.18.10" |
|||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.10.tgz#4908e81b6b339ca7c6b7a555a5fc29446f26dde6" |
|||
integrity sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ== |
|||
dependencies: |
|||
"@babel/helper-string-parser" "^7.18.10" |
|||
"@babel/helper-validator-identifier" "^7.18.6" |
|||
to-fast-properties "^2.0.0" |
|||
|
|||
"@esbuild/linux-loong64@0.14.54": |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" |
|||
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== |
|||
|
|||
"@fortawesome/fontawesome-common-types@6.1.2": |
|||
version "6.1.2" |
|||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz#c1095b1bbabf19f37f9ff0719db38d92a410bcfe" |
|||
integrity sha512-wBaAPGz1Awxg05e0PBRkDRuTsy4B3dpBm+zreTTyd9TH4uUM27cAL4xWyWR0rLJCrRwzVsQ4hF3FvM6rqydKPA== |
|||
|
|||
"@fortawesome/fontawesome-svg-core@^6.1.1": |
|||
version "6.1.2" |
|||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.2.tgz#11e2e8583a7dea75d734e4d0e53d91c63fae7511" |
|||
integrity sha512-853G/Htp0BOdXnPoeCPTjFrVwyrJHpe8MhjB/DYE9XjwhnNDfuBCd3aKc2YUYbEfHEcBws4UAA0kA9dymZKGjA== |
|||
dependencies: |
|||
"@fortawesome/fontawesome-common-types" "6.1.2" |
|||
|
|||
"@fortawesome/free-solid-svg-icons@^6.1.1": |
|||
version "6.1.2" |
|||
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.2.tgz#491d668b8a6603698d0ce1ac620f66fd22b74c84" |
|||
integrity sha512-lTgZz+cMpzjkHmCwOG3E1ilUZrnINYdqMmrkv30EC3XbRsGlbIOL8H9LaNp5SV4g0pNJDfQ4EdTWWaMvdwyLiQ== |
|||
dependencies: |
|||
"@fortawesome/fontawesome-common-types" "6.1.2" |
|||
|
|||
"@fortawesome/react-fontawesome@^0.2.0": |
|||
version "0.2.0" |
|||
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4" |
|||
integrity sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw== |
|||
dependencies: |
|||
prop-types "^15.8.1" |
|||
|
|||
"@jridgewell/gen-mapping@^0.1.0": |
|||
version "0.1.1" |
|||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" |
|||
integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== |
|||
dependencies: |
|||
"@jridgewell/set-array" "^1.0.0" |
|||
"@jridgewell/sourcemap-codec" "^1.4.10" |
|||
|
|||
"@jridgewell/gen-mapping@^0.3.2": |
|||
version "0.3.2" |
|||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" |
|||
integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== |
|||
dependencies: |
|||
"@jridgewell/set-array" "^1.0.1" |
|||
"@jridgewell/sourcemap-codec" "^1.4.10" |
|||
"@jridgewell/trace-mapping" "^0.3.9" |
|||
|
|||
"@jridgewell/resolve-uri@^3.0.3": |
|||
version "3.1.0" |
|||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" |
|||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== |
|||
|
|||
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": |
|||
version "1.1.2" |
|||
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" |
|||
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== |
|||
|
|||
"@jridgewell/sourcemap-codec@^1.4.10": |
|||
version "1.4.14" |
|||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" |
|||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== |
|||
|
|||
"@jridgewell/trace-mapping@^0.3.9": |
|||
version "0.3.15" |
|||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" |
|||
integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== |
|||
dependencies: |
|||
"@jridgewell/resolve-uri" "^3.0.3" |
|||
"@jridgewell/sourcemap-codec" "^1.4.10" |
|||
|
|||
"@types/prop-types@*": |
|||
version "15.7.5" |
|||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" |
|||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== |
|||
|
|||
"@types/react-dom@^18.0.6": |
|||
version "18.0.6" |
|||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1" |
|||
integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA== |
|||
dependencies: |
|||
"@types/react" "*" |
|||
|
|||
"@types/react@*", "@types/react@^18.0.17": |
|||
version "18.0.17" |
|||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.17.tgz#4583d9c322d67efe4b39a935d223edcc7050ccf4" |
|||
integrity sha512-38ETy4tL+rn4uQQi7mB81G7V1g0u2ryquNmsVIOKUAEIDK+3CUjZ6rSRpdvS99dNBnkLFL83qfmtLacGOTIhwQ== |
|||
dependencies: |
|||
"@types/prop-types" "*" |
|||
"@types/scheduler" "*" |
|||
csstype "^3.0.2" |
|||
|
|||
"@types/scheduler@*": |
|||
version "0.16.2" |
|||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" |
|||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== |
|||
|
|||
"@vitejs/plugin-react@^2.0.1": |
|||
version "2.0.1" |
|||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-2.0.1.tgz#3197c01d8e4a4eb9fed829c7888c467a43aadd4e" |
|||
integrity sha512-uINzNHmjrbunlFtyVkST6lY1ewSfz/XwLufG0PIqvLGnpk2nOIOa/1CACTDNcKi1/RwaCzJLmsXwm1NsUVV/NA== |
|||
dependencies: |
|||
"@babel/core" "^7.18.10" |
|||
"@babel/plugin-transform-react-jsx" "^7.18.10" |
|||
"@babel/plugin-transform-react-jsx-development" "^7.18.6" |
|||
"@babel/plugin-transform-react-jsx-self" "^7.18.6" |
|||
"@babel/plugin-transform-react-jsx-source" "^7.18.6" |
|||
magic-string "^0.26.2" |
|||
react-refresh "^0.14.0" |
|||
|
|||
ansi-styles@^3.2.1: |
|||
version "3.2.1" |
|||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" |
|||
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== |
|||
dependencies: |
|||
color-convert "^1.9.0" |
|||
|
|||
anymatch@~3.1.2: |
|||
version "3.1.2" |
|||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" |
|||
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== |
|||
dependencies: |
|||
normalize-path "^3.0.0" |
|||
picomatch "^2.0.4" |
|||
|
|||
asynckit@^0.4.0: |
|||
version "0.4.0" |
|||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" |
|||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== |
|||
|
|||
axios@^0.27.2: |
|||
version "0.27.2" |
|||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" |
|||
integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== |
|||
dependencies: |
|||
follow-redirects "^1.14.9" |
|||
form-data "^4.0.0" |
|||
|
|||
binary-extensions@^2.0.0: |
|||
version "2.2.0" |
|||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" |
|||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== |
|||
|
|||
braces@~3.0.2: |
|||
version "3.0.2" |
|||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" |
|||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== |
|||
dependencies: |
|||
fill-range "^7.0.1" |
|||
|
|||
browserslist@^4.20.2: |
|||
version "4.21.3" |
|||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a" |
|||
integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ== |
|||
dependencies: |
|||
caniuse-lite "^1.0.30001370" |
|||
electron-to-chromium "^1.4.202" |
|||
node-releases "^2.0.6" |
|||
update-browserslist-db "^1.0.5" |
|||
|
|||
caniuse-lite@^1.0.30001370: |
|||
version "1.0.30001375" |
|||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz#8e73bc3d1a4c800beb39f3163bf0190d7e5d7672" |
|||
integrity sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw== |
|||
|
|||
chalk@^2.0.0: |
|||
version "2.4.2" |
|||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" |
|||
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== |
|||
dependencies: |
|||
ansi-styles "^3.2.1" |
|||
escape-string-regexp "^1.0.5" |
|||
supports-color "^5.3.0" |
|||
|
|||
"chokidar@>=3.0.0 <4.0.0": |
|||
version "3.5.3" |
|||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" |
|||
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== |
|||
dependencies: |
|||
anymatch "~3.1.2" |
|||
braces "~3.0.2" |
|||
glob-parent "~5.1.2" |
|||
is-binary-path "~2.1.0" |
|||
is-glob "~4.0.1" |
|||
normalize-path "~3.0.0" |
|||
readdirp "~3.6.0" |
|||
optionalDependencies: |
|||
fsevents "~2.3.2" |
|||
|
|||
color-convert@^1.9.0: |
|||
version "1.9.3" |
|||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" |
|||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== |
|||
dependencies: |
|||
color-name "1.1.3" |
|||
|
|||
color-name@1.1.3: |
|||
version "1.1.3" |
|||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" |
|||
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== |
|||
|
|||
combined-stream@^1.0.8: |
|||
version "1.0.8" |
|||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" |
|||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== |
|||
dependencies: |
|||
delayed-stream "~1.0.0" |
|||
|
|||
convert-source-map@^1.7.0: |
|||
version "1.8.0" |
|||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" |
|||
integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== |
|||
dependencies: |
|||
safe-buffer "~5.1.1" |
|||
|
|||
csstype@^3.0.2: |
|||
version "3.1.0" |
|||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2" |
|||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA== |
|||
|
|||
debug@^4.1.0: |
|||
version "4.3.4" |
|||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" |
|||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== |
|||
dependencies: |
|||
ms "2.1.2" |
|||
|
|||
delayed-stream@~1.0.0: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" |
|||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== |
|||
|
|||
electron-to-chromium@^1.4.202: |
|||
version "1.4.218" |
|||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.218.tgz#d6b817b5454499a92c85888b42dc2ad075e4493a" |
|||
integrity sha512-INDylKH//YIf2w67D+IjkfVnGVrZ/D94DAU/FPPm6T4jEPbEDQvo9r2wTj0ncFdtJH8+V8BggZTaN8Rzk5wkgw== |
|||
|
|||
esbuild-android-64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" |
|||
integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== |
|||
|
|||
esbuild-android-arm64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" |
|||
integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== |
|||
|
|||
esbuild-darwin-64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" |
|||
integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== |
|||
|
|||
esbuild-darwin-arm64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" |
|||
integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== |
|||
|
|||
esbuild-freebsd-64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" |
|||
integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== |
|||
|
|||
esbuild-freebsd-arm64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" |
|||
integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== |
|||
|
|||
esbuild-linux-32@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" |
|||
integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== |
|||
|
|||
esbuild-linux-64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" |
|||
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== |
|||
|
|||
esbuild-linux-arm64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" |
|||
integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== |
|||
|
|||
esbuild-linux-arm@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" |
|||
integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== |
|||
|
|||
esbuild-linux-mips64le@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" |
|||
integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== |
|||
|
|||
esbuild-linux-ppc64le@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" |
|||
integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== |
|||
|
|||
esbuild-linux-riscv64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" |
|||
integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== |
|||
|
|||
esbuild-linux-s390x@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" |
|||
integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== |
|||
|
|||
esbuild-netbsd-64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" |
|||
integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== |
|||
|
|||
esbuild-openbsd-64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" |
|||
integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== |
|||
|
|||
esbuild-sunos-64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" |
|||
integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== |
|||
|
|||
esbuild-windows-32@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" |
|||
integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== |
|||
|
|||
esbuild-windows-64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" |
|||
integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== |
|||
|
|||
esbuild-windows-arm64@0.14.54: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" |
|||
integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== |
|||
|
|||
esbuild@^0.14.47: |
|||
version "0.14.54" |
|||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" |
|||
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== |
|||
optionalDependencies: |
|||
"@esbuild/linux-loong64" "0.14.54" |
|||
esbuild-android-64 "0.14.54" |
|||
esbuild-android-arm64 "0.14.54" |
|||
esbuild-darwin-64 "0.14.54" |
|||
esbuild-darwin-arm64 "0.14.54" |
|||
esbuild-freebsd-64 "0.14.54" |
|||
esbuild-freebsd-arm64 "0.14.54" |
|||
esbuild-linux-32 "0.14.54" |
|||
esbuild-linux-64 "0.14.54" |
|||
esbuild-linux-arm "0.14.54" |
|||
esbuild-linux-arm64 "0.14.54" |
|||
esbuild-linux-mips64le "0.14.54" |
|||
esbuild-linux-ppc64le "0.14.54" |
|||
esbuild-linux-riscv64 "0.14.54" |
|||
esbuild-linux-s390x "0.14.54" |
|||
esbuild-netbsd-64 "0.14.54" |
|||
esbuild-openbsd-64 "0.14.54" |
|||
esbuild-sunos-64 "0.14.54" |
|||
esbuild-windows-32 "0.14.54" |
|||
esbuild-windows-64 "0.14.54" |
|||
esbuild-windows-arm64 "0.14.54" |
|||
|
|||
escalade@^3.1.1: |
|||
version "3.1.1" |
|||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" |
|||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== |
|||
|
|||
escape-string-regexp@^1.0.5: |
|||
version "1.0.5" |
|||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" |
|||
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== |
|||
|
|||
fill-range@^7.0.1: |
|||
version "7.0.1" |
|||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" |
|||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== |
|||
dependencies: |
|||
to-regex-range "^5.0.1" |
|||
|
|||
follow-redirects@^1.14.9: |
|||
version "1.15.1" |
|||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" |
|||
integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== |
|||
|
|||
form-data@^4.0.0: |
|||
version "4.0.0" |
|||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" |
|||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== |
|||
dependencies: |
|||
asynckit "^0.4.0" |
|||
combined-stream "^1.0.8" |
|||
mime-types "^2.1.12" |
|||
|
|||
fsevents@~2.3.2: |
|||
version "2.3.2" |
|||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" |
|||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== |
|||
|
|||
function-bind@^1.1.1: |
|||
version "1.1.1" |
|||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" |
|||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== |
|||
|
|||
gensync@^1.0.0-beta.2: |
|||
version "1.0.0-beta.2" |
|||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" |
|||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== |
|||
|
|||
glob-parent@~5.1.2: |
|||
version "5.1.2" |
|||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" |
|||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== |
|||
dependencies: |
|||
is-glob "^4.0.1" |
|||
|
|||
globals@^11.1.0: |
|||
version "11.12.0" |
|||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" |
|||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== |
|||
|
|||
has-flag@^3.0.0: |
|||
version "3.0.0" |
|||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" |
|||
integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== |
|||
|
|||
has@^1.0.3: |
|||
version "1.0.3" |
|||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" |
|||
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== |
|||
dependencies: |
|||
function-bind "^1.1.1" |
|||
|
|||
history@^5.2.0: |
|||
version "5.3.0" |
|||
resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" |
|||
integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== |
|||
dependencies: |
|||
"@babel/runtime" "^7.7.6" |
|||
|
|||
immutable@^4.0.0: |
|||
version "4.1.0" |
|||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" |
|||
integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ== |
|||
|
|||
is-binary-path@~2.1.0: |
|||
version "2.1.0" |
|||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" |
|||
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== |
|||
dependencies: |
|||
binary-extensions "^2.0.0" |
|||
|
|||
is-core-module@^2.9.0: |
|||
version "2.10.0" |
|||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" |
|||
integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== |
|||
dependencies: |
|||
has "^1.0.3" |
|||
|
|||
is-extglob@^2.1.1: |
|||
version "2.1.1" |
|||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" |
|||
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== |
|||
|
|||
is-glob@^4.0.1, is-glob@~4.0.1: |
|||
version "4.0.3" |
|||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" |
|||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== |
|||
dependencies: |
|||
is-extglob "^2.1.1" |
|||
|
|||
is-number@^7.0.0: |
|||
version "7.0.0" |
|||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" |
|||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== |
|||
|
|||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: |
|||
version "4.0.0" |
|||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" |
|||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== |
|||
|
|||
jsesc@^2.5.1: |
|||
version "2.5.2" |
|||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" |
|||
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== |
|||
|
|||
json5@^2.2.1: |
|||
version "2.2.1" |
|||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" |
|||
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== |
|||
|
|||
loose-envify@^1.1.0, loose-envify@^1.4.0: |
|||
version "1.4.0" |
|||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" |
|||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== |
|||
dependencies: |
|||
js-tokens "^3.0.0 || ^4.0.0" |
|||
|
|||
magic-string@^0.26.2: |
|||
version "0.26.2" |
|||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.2.tgz#5331700e4158cd6befda738bb6b0c7b93c0d4432" |
|||
integrity sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A== |
|||
dependencies: |
|||
sourcemap-codec "^1.4.8" |
|||
|
|||
mime-db@1.52.0: |
|||
version "1.52.0" |
|||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" |
|||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== |
|||
|
|||
mime-types@^2.1.12: |
|||
version "2.1.35" |
|||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" |
|||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== |
|||
dependencies: |
|||
mime-db "1.52.0" |
|||
|
|||
ms@2.1.2: |
|||
version "2.1.2" |
|||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" |
|||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== |
|||
|
|||
nanoid@^3.3.4: |
|||
version "3.3.4" |
|||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" |
|||
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== |
|||
|
|||
node-releases@^2.0.6: |
|||
version "2.0.6" |
|||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" |
|||
integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== |
|||
|
|||
normalize-path@^3.0.0, normalize-path@~3.0.0: |
|||
version "3.0.0" |
|||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" |
|||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== |
|||
|
|||
object-assign@^4.1.1: |
|||
version "4.1.1" |
|||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" |
|||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== |
|||
|
|||
path-parse@^1.0.7: |
|||
version "1.0.7" |
|||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" |
|||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== |
|||
|
|||
picocolors@^1.0.0: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" |
|||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== |
|||
|
|||
picomatch@^2.0.4, picomatch@^2.2.1: |
|||
version "2.3.1" |
|||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" |
|||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== |
|||
|
|||
postcss@^8.4.16: |
|||
version "8.4.16" |
|||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" |
|||
integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== |
|||
dependencies: |
|||
nanoid "^3.3.4" |
|||
picocolors "^1.0.0" |
|||
source-map-js "^1.0.2" |
|||
|
|||
prop-types@^15.8.1: |
|||
version "15.8.1" |
|||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" |
|||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== |
|||
dependencies: |
|||
loose-envify "^1.4.0" |
|||
object-assign "^4.1.1" |
|||
react-is "^16.13.1" |
|||
|
|||
react-dom@^18.0.0: |
|||
version "18.2.0" |
|||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" |
|||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== |
|||
dependencies: |
|||
loose-envify "^1.1.0" |
|||
scheduler "^0.23.0" |
|||
|
|||
react-is@^16.13.1: |
|||
version "16.13.1" |
|||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" |
|||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== |
|||
|
|||
react-refresh@^0.14.0: |
|||
version "0.14.0" |
|||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" |
|||
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== |
|||
|
|||
react-router-dom@^6.3.0: |
|||
version "6.3.0" |
|||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" |
|||
integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== |
|||
dependencies: |
|||
history "^5.2.0" |
|||
react-router "6.3.0" |
|||
|
|||
react-router@6.3.0, react-router@^6.3.0: |
|||
version "6.3.0" |
|||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" |
|||
integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== |
|||
dependencies: |
|||
history "^5.2.0" |
|||
|
|||
react@^18.0.0: |
|||
version "18.2.0" |
|||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" |
|||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== |
|||
dependencies: |
|||
loose-envify "^1.1.0" |
|||
|
|||
readdirp@~3.6.0: |
|||
version "3.6.0" |
|||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" |
|||
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== |
|||
dependencies: |
|||
picomatch "^2.2.1" |
|||
|
|||
regenerator-runtime@^0.13.4: |
|||
version "0.13.9" |
|||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" |
|||
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== |
|||
|
|||
resolve@^1.22.1: |
|||
version "1.22.1" |
|||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" |
|||
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== |
|||
dependencies: |
|||
is-core-module "^2.9.0" |
|||
path-parse "^1.0.7" |
|||
supports-preserve-symlinks-flag "^1.0.0" |
|||
|
|||
"rollup@>=2.75.6 <2.77.0 || ~2.77.0": |
|||
version "2.77.3" |
|||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12" |
|||
integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g== |
|||
optionalDependencies: |
|||
fsevents "~2.3.2" |
|||
|
|||
safe-buffer@~5.1.1: |
|||
version "5.1.2" |
|||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" |
|||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== |
|||
|
|||
sass@^1.53.0: |
|||
version "1.54.4" |
|||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.4.tgz#803ff2fef5525f1dd01670c3915b4b68b6cba72d" |
|||
integrity sha512-3tmF16yvnBwtlPrNBHw/H907j8MlOX8aTBnlNX1yrKx24RKcJGPyLhFUwkoKBKesR3unP93/2z14Ll8NicwQUA== |
|||
dependencies: |
|||
chokidar ">=3.0.0 <4.0.0" |
|||
immutable "^4.0.0" |
|||
source-map-js ">=0.6.2 <2.0.0" |
|||
|
|||
scheduler@^0.23.0: |
|||
version "0.23.0" |
|||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" |
|||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== |
|||
dependencies: |
|||
loose-envify "^1.1.0" |
|||
|
|||
semver@^6.3.0: |
|||
version "6.3.0" |
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" |
|||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== |
|||
|
|||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" |
|||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== |
|||
|
|||
sourcemap-codec@^1.4.8: |
|||
version "1.4.8" |
|||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" |
|||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== |
|||
|
|||
supports-color@^5.3.0: |
|||
version "5.5.0" |
|||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" |
|||
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== |
|||
dependencies: |
|||
has-flag "^3.0.0" |
|||
|
|||
supports-preserve-symlinks-flag@^1.0.0: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" |
|||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== |
|||
|
|||
to-fast-properties@^2.0.0: |
|||
version "2.0.0" |
|||
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" |
|||
integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== |
|||
|
|||
to-regex-range@^5.0.1: |
|||
version "5.0.1" |
|||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" |
|||
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== |
|||
dependencies: |
|||
is-number "^7.0.0" |
|||
|
|||
typescript@^4.6.4: |
|||
version "4.7.4" |
|||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" |
|||
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== |
|||
|
|||
update-browserslist-db@^1.0.5: |
|||
version "1.0.5" |
|||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38" |
|||
integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q== |
|||
dependencies: |
|||
escalade "^3.1.1" |
|||
picocolors "^1.0.0" |
|||
|
|||
vite@^3.0.7: |
|||
version "3.0.7" |
|||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.7.tgz#f1e379857e9c5d652126f8b20d371e1365eb700f" |
|||
integrity sha512-dILhvKba1mbP1wCezVQx/qhEK7/+jVn9ciadEcyKMMhZpsuAi/eWZfJRMkmYlkSFG7Qq9NvJbgFq4XOBxugJsA== |
|||
dependencies: |
|||
esbuild "^0.14.47" |
|||
postcss "^8.4.16" |
|||
resolve "^1.22.1" |
|||
rollup ">=2.75.6 <2.77.0 || ~2.77.0" |
|||
optionalDependencies: |
|||
fsevents "~2.3.2" |
@ -0,0 +1,28 @@ |
|||
package net.aiterp.git.ykonsole2.infrastructure.drivers |
|||
|
|||
import kotlinx.coroutines.delay |
|||
import net.aiterp.git.ykonsole2.domain.runtime.* |
|||
import net.aiterp.git.ykonsole2.infrastructure.drivers.abstracts.ActiveDriver |
|||
import kotlin.time.Duration.Companion.seconds |
|||
|
|||
object Skipper : ActiveDriver() { |
|||
private var enabled: Boolean = false |
|||
|
|||
override suspend fun onCommand(command: Command, output: FlowBus<Event>) { |
|||
if (command is ConnectCommand) { |
|||
enabled = true |
|||
} |
|||
|
|||
if (command is DisconnectCommand) { |
|||
enabled = false |
|||
} |
|||
|
|||
if (command is SkipCommand && enabled) { |
|||
output.emit(Skipped) |
|||
} |
|||
} |
|||
|
|||
override suspend fun onTick(output: FlowBus<Event>) { |
|||
delay(10.seconds) |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue