Skip to content

Commit

Permalink
ui data model WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
dearlordylord committed Jan 13, 2024
1 parent 2a9b797 commit 51bafb6
Show file tree
Hide file tree
Showing 14 changed files with 617 additions and 0 deletions.
1 change: 1 addition & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Expand Down
25 changes: 25 additions & 0 deletions ui/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
1 change: 1 addition & 0 deletions ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UI reducers
11 changes: 11 additions & 0 deletions ui/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'ui',
preset: '../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../coverage/ui',
};
10 changes: 10 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
30 changes: 30 additions & 0 deletions ui/project.json
Original file line number Diff line number Diff line change
@@ -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": []
}
1 change: 1 addition & 0 deletions ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/ui';
5 changes: 5 additions & 0 deletions ui/src/lib/ui.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
describe('ui', () => {
it('should work', () => {
// TODO
});
});
211 changes: 211 additions & 0 deletions ui/src/lib/ui.ts
Original file line number Diff line number Diff line change
@@ -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<E> = {
active: true,
onClick: E,
} | {
active: false,
}
type ActiveButton<E> = Button<E> & {
active: true,
}
type InactiveButton<E = never> = Button<E> & {
active: false,
}


type ViewActiveValue = {
startButton: Button<StartClickedEvent>,
stopButton: Button<StopClickedEvent>,
pauseButton: Button<PauseClickedEvent>,
continueButton: Button<ContinueClickedEvent>,
}

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<Omit<ModeSelectorSettingsValue & {
mode: k
}, 'mode'>>
}>

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<string>
} & ({
simple: Program<SimpleProgramStep>
});

const simpleModeSelectorToProgram = (settings: Omit<ModeSelectorSettingsValue & {mode: SimpleMode}, 'mode'>): 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<PauseClickedEvent>,
continueButton: InactiveButton,
} | {
running: 'paused',
startButton: InactiveButton,
stopButton: ActiveButton<StopClickedEvent>,
pauseButton: InactiveButton,
continueButton: ActiveButton<ContinueClickedEvent>,
} | {
running: 'stopped',
startButton: ActiveButton<StartClickedEvent>,
stopButton: InactiveButton,
pauseButton: InactiveButton,
continueButton: InactiveButton,
modeSelector: ModeSelectorSettingsViewValue
// todo program queries
});
type View_<S, R> = (state: S) => R;
export type View = View_<State, ViewValue>;

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',
};
}
}
}
22 changes: 22 additions & 0 deletions ui/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
10 changes: 10 additions & 0 deletions ui/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -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"]
}
14 changes: 14 additions & 0 deletions ui/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
1 change: 1 addition & 0 deletions utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './lib/utils';
export * from './lib/pipe';
Loading

0 comments on commit 51bafb6

Please sign in to comment.