diff --git a/tsconfig.base.json b/tsconfig.base.json index 4eaccaa..a573ca3 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,6 +20,7 @@ "@jikan0/fsm": ["fsm/src/index.ts"], "@jikan0/react": ["react/src/index.ts"], "@jikan0/test-utils": ["test-utils/src/index.ts"], + "@jikan0/ui": ["ui/src/index.ts"], "@jikan0/utils": ["utils/src/index.ts"], "jikan0": ["facade/src/index.ts"] } diff --git a/ui/.eslintrc.json b/ui/.eslintrc.json new file mode 100644 index 0000000..4c52ca2 --- /dev/null +++ b/ui/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "extends": ["../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": "error" + } + } + ] +} diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..28b5125 --- /dev/null +++ b/ui/README.md @@ -0,0 +1 @@ +UI reducers diff --git a/ui/jest.config.ts b/ui/jest.config.ts new file mode 100644 index 0000000..1b900b0 --- /dev/null +++ b/ui/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'ui', + preset: '../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../coverage/ui', +}; diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..82f3695 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,10 @@ +{ + "name": "@jikan0/ui", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts" +} diff --git a/ui/project.json b/ui/project.json new file mode 100644 index 0000000..914a528 --- /dev/null +++ b/ui/project.json @@ -0,0 +1,30 @@ +{ + "name": "ui", + "$schema": "../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "ui/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/ui", + "main": "ui/src/index.ts", + "tsConfig": "ui/tsconfig.lib.json", + "assets": ["ui/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "ui/jest.config.ts" + } + } + }, + "tags": [] +} diff --git a/ui/src/index.ts b/ui/src/index.ts new file mode 100644 index 0000000..48da4fd --- /dev/null +++ b/ui/src/index.ts @@ -0,0 +1 @@ +export * from './lib/ui'; diff --git a/ui/src/lib/ui.spec.ts b/ui/src/lib/ui.spec.ts new file mode 100644 index 0000000..ab92a85 --- /dev/null +++ b/ui/src/lib/ui.spec.ts @@ -0,0 +1,5 @@ +describe('ui', () => { + it('should work', () => { + // TODO + }); +}); diff --git a/ui/src/lib/ui.ts b/ui/src/lib/ui.ts new file mode 100644 index 0000000..9e7b9f2 --- /dev/null +++ b/ui/src/lib/ui.ts @@ -0,0 +1,211 @@ +import { Program, State as FsmState, reset, push } from '@jikan0/fsm'; +import { pipe } from '@jikan0/utils'; + +// TODO program library +// TODO program settings +// TODO fp eslint + +export type StartClickedEvent = { + _tag: 'StartClicked'; +} + +export type StopClickedEvent = { + _tag: 'StopClicked'; +} + +export type PauseClickedEvent = { + _tag: 'PauseClicked'; +} + +export type ContinueClickedEvent = { + _tag: 'ContinueClicked'; +} + +export type TimePassedEvent = { + _tag: 'TimePassed'; + timeMs: BigInt; +} + +export const DEFAULT_MODE = 'simple' as const satisfies Mode; +export const DEFAULT_EXERCISE_TIME_MS = BigInt(30000); +export const DEFAULT_REST_TIME_MS = BigInt(10000); +export const DEFAULT_ROUNDS = BigInt(10); + +export type ModeSelected = { + _tag: 'ModeSelected'; +} + +export type Event = StartClickedEvent | StopClickedEvent | PauseClickedEvent | ContinueClickedEvent; + +export type Action = Event; + +type RunningState = 'running' | 'paused' | 'stopped'; +type Button = { + active: true, + onClick: E, +} | { + active: false, +} +type ActiveButton = Button & { + active: true, +} +type InactiveButton = Button & { + active: false, +} + + +type ViewActiveValue = { + startButton: Button, + stopButton: Button, + pauseButton: Button, + continueButton: Button, +} + +type ViewQueryValue = { + running: RunningState, + program: Program, // can be empty + fsmState: FsmState, // can be empty +} + +type State = ViewQueryValue & { + mode: ModeSelectorState +}; + +const SIMPLE_MODE = 'simple' as const; + +type SimpleMode = typeof SIMPLE_MODE; + +const MODES = [SIMPLE_MODE] as const; + +type Mode = typeof MODES[number]; + +export type ModeSelectorSettingsValue = { + mode: Mode, +} & ({ + mode: SimpleMode, + exerciseTimeMs: BigInt, // TODO positive... + restTimeMs: BigInt, // TODO positive... + rounds: BigInt, // TODO positive... +}) + +export type ModeSelectorSettings = Readonly<{ + [k in Mode]: Readonly> +}> + +export type ModeSelectorState = Readonly<{ + selected: Mode, + settings: ModeSelectorSettings, +}>; + +export const modeSelectorState0: ModeSelectorState = Object.freeze({ + selected: DEFAULT_MODE, + settings: Object.freeze({ + simple: Object.freeze({ + exerciseTimeMs: DEFAULT_EXERCISE_TIME_MS, + restTimeMs: DEFAULT_REST_TIME_MS, + rounds: DEFAULT_ROUNDS, + }) + }) +}); + +const PREPARATION_STEP = 'preparation' as const; +const EXERCISE_STEP = 'exercise' as const; +const REST_STEP = 'rest' as const; + +const SIMPLE_PROGRAM_STEPS = [ + PREPARATION_STEP, + EXERCISE_STEP, + REST_STEP, +] as const; + +type SimpleProgramStep = typeof SIMPLE_PROGRAM_STEPS[number]; + +type ProgramPerMode = { + [k in Mode]: Program +} & ({ + simple: Program +}); + +const simpleModeSelectorToProgram = (settings: Omit): ProgramPerMode[SimpleMode] => + Object.freeze([...Array(settings.rounds).keys()].flatMap(i => [ + ...(i === 0 ? [{kind: PREPARATION_STEP, duration: 3000/*TODO make configurable*/}] : []), + { kind: EXERCISE_STEP, duration: Number(settings.exerciseTimeMs) }, + ...(Number(settings.rounds) === i + 1 ? [] : [{ kind: REST_STEP, duration: Number(settings.restTimeMs) }]), + ])) + + +const selectorToProgram = (selector: ModeSelectorState): ProgramPerMode[typeof selector.selected] => { + const settings = selector.settings[selector.selected]; + switch (selector.selected) { + case SIMPLE_MODE: { + return simpleModeSelectorToProgram(settings) + } + } +} + +export type ModeSelectorSettingsViewValue = ModeSelectorSettingsValue; + +export type ViewValue = ViewActiveValue & ViewQueryValue & ({ + running: 'running', + startButton: InactiveButton, + stopButton: InactiveButton, + pauseButton: ActiveButton, + continueButton: InactiveButton, +} | { + running: 'paused', + startButton: InactiveButton, + stopButton: ActiveButton, + pauseButton: InactiveButton, + continueButton: ActiveButton, +} | { + running: 'stopped', + startButton: ActiveButton, + stopButton: InactiveButton, + pauseButton: InactiveButton, + continueButton: InactiveButton, + modeSelector: ModeSelectorSettingsViewValue + // todo program queries +}); +type View_ = (state: S) => R; +export type View = View_; + +export const reduce = (state: State, action: Action): State => { + switch (action._tag) { + case 'StartClicked': { + if (state.running === 'running' || state.running === 'paused') return state; + const program = selectorToProgram(state.mode); + return { + ...state, + running: 'running', + program, + fsmState: pipe(state.fsmState, reset, push(program)) + }; + } + case 'StopClicked': { + if (state.running === 'stopped' || state.running === 'running'/*pause before stopping*/) return state; + const program = selectorToProgram(state.mode); + return { + ...state, + running: 'stopped', + program, + fsmState: pipe(state.fsmState, reset), + }; + } + case 'PauseClicked': { + if (state.running === 'stopped' || state.running === 'paused') return state; + return { + ...state, + running: 'paused', + }; + } + case 'ContinueClicked': { + if (state.running === 'stopped' || state.running === 'running') return state; + return { + ...state, + running: 'running', + }; + } + } +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..451a604 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/ui/tsconfig.lib.json b/ui/tsconfig.lib.json new file mode 100644 index 0000000..6f3c503 --- /dev/null +++ b/ui/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/ui/tsconfig.spec.json b/ui/tsconfig.spec.json new file mode 100644 index 0000000..663c878 --- /dev/null +++ b/ui/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/utils/src/index.ts b/utils/src/index.ts index 3a4bf34..23973c8 100644 --- a/utils/src/index.ts +++ b/utils/src/index.ts @@ -1 +1,2 @@ export * from './lib/utils'; +export * from './lib/pipe'; diff --git a/utils/src/lib/pipe.ts b/utils/src/lib/pipe.ts new file mode 100644 index 0000000..18ed1d5 --- /dev/null +++ b/utils/src/lib/pipe.ts @@ -0,0 +1,275 @@ +// fp-ts (mit) +export function pipe(a: A): A +export function pipe(a: A, ab: (a: A) => B): B +export function pipe(a: A, ab: (a: A) => B, bc: (b: B) => C): C +export function pipe(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D +export function pipe(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E): E +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F +): F +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G +): G +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H +): H +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I +): I +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J +): J +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K +): K +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L +): L +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M +): M +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N +): N +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O +): O + +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P +): P + +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q +): Q + +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R +): R + +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S +): S + +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F, + fg: (f: F) => G, + gh: (g: G) => H, + hi: (h: H) => I, + ij: (i: I) => J, + jk: (j: J) => K, + kl: (k: K) => L, + lm: (l: L) => M, + mn: (m: M) => N, + no: (n: N) => O, + op: (o: O) => P, + pq: (p: P) => Q, + qr: (q: Q) => R, + rs: (r: R) => S, + st: (s: S) => T +): T +export function pipe( + a: unknown, + ab?: Function, + bc?: Function, + cd?: Function, + de?: Function, + ef?: Function, + fg?: Function, + gh?: Function, + hi?: Function +): unknown { + switch (arguments.length) { + case 1: + return a + case 2: + return ab!(a) + case 3: + return bc!(ab!(a)) + case 4: + return cd!(bc!(ab!(a))) + case 5: + return de!(cd!(bc!(ab!(a)))) + case 6: + return ef!(de!(cd!(bc!(ab!(a))))) + case 7: + return fg!(ef!(de!(cd!(bc!(ab!(a)))))) + case 8: + return gh!(fg!(ef!(de!(cd!(bc!(ab!(a))))))) + case 9: + return hi!(gh!(fg!(ef!(de!(cd!(bc!(ab!(a)))))))) + default: { + let ret = arguments[0] + for (let i = 1; i < arguments.length; i++) { + ret = arguments[i](ret) + } + return ret + } + } +}