2021-04-05 01:10:35 -06:00
|
|
|
import { h, VNode } from 'snabbdom';
|
2021-10-19 08:26:04 -06:00
|
|
|
import { Prop, prop } from 'common';
|
2021-10-18 11:20:52 -06:00
|
|
|
import { bind, dataIcon, onInsert } from 'common/snabbdom';
|
2021-10-19 08:26:04 -06:00
|
|
|
import { storedProp, storedJsonProp, StoredJsonProp, StoredProp } from 'common/storage';
|
|
|
|
import { ExplorerDb, ExplorerSpeed, ExplorerMode } from './interfaces';
|
2021-10-18 11:20:52 -06:00
|
|
|
import { snabModal } from 'common/modal';
|
2021-10-19 08:43:04 -06:00
|
|
|
import AnalyseCtrl from '../ctrl';
|
2021-10-19 11:03:52 -06:00
|
|
|
import { perf } from 'game/perf';
|
|
|
|
import { iconTag } from '../util';
|
|
|
|
import { ucfirst } from './explorerUtil';
|
2021-10-20 03:46:28 -06:00
|
|
|
import { Color } from 'chessground/types';
|
|
|
|
import { opposite } from 'chessground/util';
|
2021-10-21 02:07:08 -06:00
|
|
|
import { Redraw } from '../interfaces';
|
2017-06-21 04:38:08 -06:00
|
|
|
|
2021-10-19 11:03:52 -06:00
|
|
|
const allSpeeds: ExplorerSpeed[] = ['ultraBullet', 'bullet', 'blitz', 'rapid', 'classical', 'correspondence'];
|
2021-10-19 07:45:02 -06:00
|
|
|
const allModes: ExplorerMode[] = ['casual', 'rated'];
|
2019-02-24 17:16:53 -07:00
|
|
|
const allRatings = [1600, 1800, 2000, 2200, 2500];
|
2021-10-31 07:32:27 -06:00
|
|
|
const minYear = 1952;
|
2017-11-30 09:10:36 -07:00
|
|
|
|
2021-10-19 09:51:12 -06:00
|
|
|
type Month = string;
|
|
|
|
|
2021-10-19 08:26:04 -06:00
|
|
|
export interface ExplorerConfigData {
|
|
|
|
open: Prop<boolean>;
|
2021-10-19 08:50:05 -06:00
|
|
|
db: StoredProp<ExplorerDb>;
|
|
|
|
rating: StoredJsonProp<number[]>;
|
|
|
|
speed: StoredJsonProp<ExplorerSpeed[]>;
|
|
|
|
mode: StoredJsonProp<ExplorerMode[]>;
|
2021-10-19 09:51:12 -06:00
|
|
|
since: StoredProp<Month>;
|
|
|
|
until: StoredProp<Month>;
|
2021-10-19 08:26:04 -06:00
|
|
|
playerName: {
|
|
|
|
open: Prop<boolean>;
|
|
|
|
value: StoredProp<string>;
|
|
|
|
previous: StoredJsonProp<string[]>;
|
|
|
|
};
|
2021-10-20 03:46:28 -06:00
|
|
|
color: Prop<Color>;
|
2021-10-19 08:26:04 -06:00
|
|
|
}
|
2017-06-21 04:38:08 -06:00
|
|
|
|
2021-10-19 08:26:04 -06:00
|
|
|
export class ExplorerConfigCtrl {
|
|
|
|
data: ExplorerConfigData;
|
2021-10-20 07:30:20 -06:00
|
|
|
allDbs: ExplorerDb[] = ['lichess', 'player'];
|
2021-10-20 08:17:51 -06:00
|
|
|
myName?: string;
|
2017-06-21 04:38:08 -06:00
|
|
|
|
2021-10-19 08:43:04 -06:00
|
|
|
constructor(readonly root: AnalyseCtrl, readonly variant: VariantKey, readonly onClose: () => void) {
|
2021-10-20 08:17:51 -06:00
|
|
|
this.myName = document.body.dataset['user'];
|
2021-10-20 07:30:20 -06:00
|
|
|
if (variant === 'standard') this.allDbs.unshift('masters');
|
2021-10-19 08:26:04 -06:00
|
|
|
this.data = {
|
2021-10-21 02:17:48 -06:00
|
|
|
open: prop(false),
|
2021-10-20 07:30:20 -06:00
|
|
|
db: storedProp('explorer.db.' + variant, this.allDbs[0]),
|
2021-10-19 08:50:05 -06:00
|
|
|
rating: storedJsonProp('explorer.rating', () => allRatings),
|
|
|
|
speed: storedJsonProp<ExplorerSpeed[]>('explorer.speed', () => allSpeeds),
|
|
|
|
mode: storedJsonProp<ExplorerMode[]>('explorer.mode', () => allModes),
|
2021-10-19 10:25:07 -06:00
|
|
|
since: storedProp('explorer.since', '2010-01'),
|
2021-10-19 09:51:12 -06:00
|
|
|
until: storedProp('explorer.until', ''),
|
2021-10-19 08:26:04 -06:00
|
|
|
playerName: {
|
|
|
|
open: prop(false),
|
|
|
|
value: storedProp<string>('explorer.player.name', document.body.dataset['user'] || ''),
|
|
|
|
previous: storedJsonProp<string[]>('explorer.player.name.previous', () => []),
|
|
|
|
},
|
2021-10-20 03:46:28 -06:00
|
|
|
color: prop('white'),
|
2021-10-19 08:26:04 -06:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-10-20 07:58:23 -06:00
|
|
|
selectPlayer = (name?: string) => {
|
|
|
|
name = name == 'me' ? this.myName : name;
|
|
|
|
if (!name) return;
|
|
|
|
if (name != this.myName) {
|
|
|
|
const previous = this.data.playerName.previous().filter(n => n !== name);
|
|
|
|
previous.unshift(name);
|
|
|
|
this.data.playerName.previous(previous.slice(0, 20));
|
|
|
|
}
|
|
|
|
this.data.db('player');
|
|
|
|
this.data.playerName.value(name);
|
|
|
|
this.data.playerName.open(false);
|
|
|
|
};
|
|
|
|
|
2021-10-19 08:59:04 -06:00
|
|
|
toggleMany =
|
|
|
|
<T>(c: StoredJsonProp<T[]>) =>
|
|
|
|
(value: T) => {
|
|
|
|
if (!c().includes(value)) c(c().concat([value]));
|
|
|
|
else if (c().length > 1) c(c().filter(v => v !== value));
|
|
|
|
};
|
2017-06-21 04:38:08 -06:00
|
|
|
|
2021-10-20 09:41:28 -06:00
|
|
|
toggleColor = () => this.data.color(opposite(this.data.color()));
|
|
|
|
|
2021-10-19 08:26:04 -06:00
|
|
|
toggleOpen = () => {
|
|
|
|
this.data.open(!this.data.open());
|
2021-10-20 03:52:35 -06:00
|
|
|
if (!this.data.open()) {
|
|
|
|
if (this.data.db() == 'player' && !this.data.playerName.value()) this.data.db('lichess');
|
|
|
|
this.onClose();
|
|
|
|
}
|
2017-06-21 04:38:08 -06:00
|
|
|
};
|
2021-10-21 02:07:08 -06:00
|
|
|
|
2021-10-19 08:26:04 -06:00
|
|
|
fullHouse = () =>
|
2021-10-31 07:32:27 -06:00
|
|
|
this.data.since() <= `${minYear}-01` &&
|
|
|
|
(!this.data.until() || new Date().toISOString().slice(0, 7) <= this.data.until()) &&
|
|
|
|
(this.data.db() === 'masters' || this.data.speed().length == allSpeeds.length) &&
|
|
|
|
(this.data.db() !== 'lichess' || this.data.rating().length == allRatings.length) &&
|
|
|
|
(this.data.db() !== 'player' || this.data.mode().length == allModes.length);
|
2017-06-21 04:38:08 -06:00
|
|
|
}
|
|
|
|
|
2017-08-15 18:21:23 -06:00
|
|
|
export function view(ctrl: ExplorerConfigCtrl): VNode[] {
|
2017-06-21 04:38:08 -06:00
|
|
|
return [
|
2021-10-19 08:50:05 -06:00
|
|
|
ctrl.data.db() === 'masters' ? masterDb(ctrl) : ctrl.data.db() === 'lichess' ? lichessDb(ctrl) : playerDb(ctrl),
|
2021-02-06 06:26:05 -07:00
|
|
|
h(
|
|
|
|
'section.save',
|
|
|
|
h(
|
|
|
|
'button.button.button-green.text',
|
|
|
|
{
|
2021-06-14 01:13:27 -06:00
|
|
|
attrs: dataIcon(''),
|
2021-02-06 06:26:05 -07:00
|
|
|
hook: bind('click', ctrl.toggleOpen),
|
|
|
|
},
|
2021-10-19 08:43:04 -06:00
|
|
|
ctrl.root.trans.noarg('allSet')
|
2021-02-06 06:26:05 -07:00
|
|
|
)
|
|
|
|
),
|
2017-06-21 04:38:08 -06:00
|
|
|
];
|
|
|
|
}
|
2021-10-18 11:20:52 -06:00
|
|
|
|
2021-10-21 01:40:37 -06:00
|
|
|
const selectText = 'Select a Lichess player';
|
|
|
|
|
2021-10-18 11:20:52 -06:00
|
|
|
const playerDb = (ctrl: ExplorerConfigCtrl) => {
|
|
|
|
const name = ctrl.data.playerName.value();
|
|
|
|
return h('div.player-db', [
|
|
|
|
ctrl.data.playerName.open() ? playerModal(ctrl) : undefined,
|
|
|
|
h('section.name', [
|
2021-10-31 10:08:22 -06:00
|
|
|
h('label', 'Player'),
|
|
|
|
h('div', [
|
2021-10-21 01:40:37 -06:00
|
|
|
h(
|
2021-10-31 10:08:22 -06:00
|
|
|
'div.choices',
|
|
|
|
h(
|
|
|
|
`button.player-name${name ? '.active' : ''}`,
|
|
|
|
{
|
|
|
|
hook: bind('click', () => ctrl.data.playerName.open(true), ctrl.root.redraw),
|
|
|
|
attrs: name ? { title: selectText } : undefined,
|
|
|
|
},
|
|
|
|
name || selectText
|
|
|
|
)
|
|
|
|
),
|
|
|
|
' as ',
|
|
|
|
h(
|
|
|
|
'button.button-link.text.color',
|
2021-10-21 01:40:37 -06:00
|
|
|
{
|
2021-10-31 10:08:22 -06:00
|
|
|
attrs: dataIcon(''),
|
|
|
|
hook: bind('click', ctrl.toggleColor, ctrl.root.redraw),
|
2021-10-21 01:40:37 -06:00
|
|
|
},
|
2021-10-31 10:08:22 -06:00
|
|
|
ctrl.data.color()
|
|
|
|
),
|
|
|
|
]),
|
2021-10-18 11:20:52 -06:00
|
|
|
]),
|
2021-10-31 04:10:22 -06:00
|
|
|
speedSection(ctrl),
|
2021-10-31 10:08:22 -06:00
|
|
|
modeSection(ctrl),
|
|
|
|
monthSection(ctrl),
|
2021-10-18 11:20:52 -06:00
|
|
|
]);
|
|
|
|
};
|
|
|
|
|
2021-10-19 08:59:04 -06:00
|
|
|
const masterDb = (ctrl: ExplorerConfigCtrl) =>
|
2021-10-31 07:32:27 -06:00
|
|
|
h('div', [
|
2021-10-31 10:08:22 -06:00
|
|
|
h('section.date', [
|
|
|
|
h('label', ['Since', yearInput(ctrl.data.since, () => '', ctrl.root.redraw)]),
|
|
|
|
h('label', ['Until', yearInput(ctrl.data.until, ctrl.data.since, ctrl.root.redraw)]),
|
2021-10-31 07:32:27 -06:00
|
|
|
]),
|
2021-10-19 08:59:04 -06:00
|
|
|
]);
|
|
|
|
|
|
|
|
const radioButton =
|
2021-10-19 11:03:52 -06:00
|
|
|
<T>(ctrl: ExplorerConfigCtrl, storage: StoredJsonProp<T[]>, render?: (t: T) => VNode) =>
|
2021-10-19 08:59:04 -06:00
|
|
|
(v: T) =>
|
|
|
|
h(
|
|
|
|
'button',
|
|
|
|
{
|
2021-10-19 11:03:52 -06:00
|
|
|
attrs: { 'aria-pressed': `${storage().includes(v)}`, title: render ? ucfirst('' + v) : '' },
|
2021-10-19 08:59:04 -06:00
|
|
|
hook: bind('click', _ => ctrl.toggleMany(storage)(v), ctrl.root.redraw),
|
|
|
|
},
|
2021-10-19 11:03:52 -06:00
|
|
|
render ? render(v) : '' + v
|
2021-10-19 08:59:04 -06:00
|
|
|
);
|
|
|
|
|
|
|
|
const lichessDb = (ctrl: ExplorerConfigCtrl) =>
|
|
|
|
h('div', [
|
2021-10-31 04:10:22 -06:00
|
|
|
speedSection(ctrl),
|
2021-10-31 10:08:22 -06:00
|
|
|
h('section.rating', [
|
|
|
|
h('label', ctrl.root.trans.noarg('averageElo')),
|
|
|
|
h('div.choices', allRatings.map(radioButton(ctrl, ctrl.data.rating))),
|
2021-10-21 00:46:33 -06:00
|
|
|
]),
|
2021-10-31 10:08:22 -06:00
|
|
|
monthSection(ctrl),
|
2021-10-19 08:59:04 -06:00
|
|
|
]);
|
|
|
|
|
2021-10-31 04:10:22 -06:00
|
|
|
const speedSection = (ctrl: ExplorerConfigCtrl) =>
|
2021-10-19 08:59:04 -06:00
|
|
|
h('section.speed', [
|
|
|
|
h('label', ctrl.root.trans.noarg('timeControl')),
|
2021-10-31 04:10:22 -06:00
|
|
|
h('div.choices', allSpeeds.map(radioButton(ctrl, ctrl.data.speed, s => iconTag(perf.icons[s])))),
|
2021-10-19 08:59:04 -06:00
|
|
|
]);
|
|
|
|
|
|
|
|
const modeSection = (ctrl: ExplorerConfigCtrl) =>
|
2021-10-31 10:08:22 -06:00
|
|
|
h('section.mode', [
|
|
|
|
h('label', ctrl.root.trans.noarg('mode')),
|
|
|
|
h('div.choices', allModes.map(radioButton(ctrl, ctrl.data.mode))),
|
|
|
|
]);
|
2021-10-19 08:59:04 -06:00
|
|
|
|
2021-10-31 07:32:27 -06:00
|
|
|
const monthInput = (prop: StoredProp<Month>, after: () => Month, redraw: Redraw) => {
|
2021-10-21 05:12:08 -06:00
|
|
|
const validateRange = (input: HTMLInputElement) =>
|
2021-10-31 07:32:27 -06:00
|
|
|
input.setCustomValidity(!input.value || after() <= input.value ? '' : 'Invalid date range');
|
|
|
|
const max = new Date().toISOString().slice(0, 7);
|
2021-10-21 05:12:08 -06:00
|
|
|
return h('input', {
|
2021-10-31 10:13:59 -06:00
|
|
|
key: after() ? 'until-month' : 'since-month',
|
2021-10-19 09:51:12 -06:00
|
|
|
attrs: {
|
|
|
|
type: 'month',
|
2021-10-31 07:32:27 -06:00
|
|
|
pattern: '^(19|20)[0-9]{2}-(0[1-9]|1[012])$',
|
|
|
|
min: `${minYear}-01`,
|
|
|
|
max,
|
|
|
|
value: prop() > max ? max : prop(),
|
2021-10-19 09:51:12 -06:00
|
|
|
},
|
2021-10-21 05:12:08 -06:00
|
|
|
hook: {
|
|
|
|
insert: vnode => {
|
|
|
|
const input = vnode.elm as HTMLInputElement;
|
|
|
|
validateRange(input);
|
|
|
|
input.addEventListener('change', e => {
|
|
|
|
const input = e.target as HTMLInputElement;
|
|
|
|
input.setCustomValidity('');
|
|
|
|
if (input.checkValidity()) {
|
|
|
|
validateRange(input);
|
|
|
|
prop(input.value);
|
|
|
|
redraw();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
update: (_, vnode) => validateRange(vnode.elm as HTMLInputElement),
|
|
|
|
},
|
2021-10-19 09:51:12 -06:00
|
|
|
});
|
2021-10-21 05:12:08 -06:00
|
|
|
};
|
2021-10-19 09:51:12 -06:00
|
|
|
|
2021-10-31 07:32:27 -06:00
|
|
|
const yearInput = (prop: StoredProp<Month>, after: () => Month, redraw: Redraw) => {
|
|
|
|
const validateRange = (input: HTMLInputElement) =>
|
|
|
|
input.setCustomValidity(!input.value || after().split('-')[0] <= input.value ? '' : 'Invalid date range');
|
|
|
|
return h('input', {
|
|
|
|
attrs: {
|
2021-10-31 10:13:59 -06:00
|
|
|
key: after() ? 'until-year' : 'since-year',
|
2021-10-31 07:32:27 -06:00
|
|
|
type: 'number',
|
|
|
|
min: minYear,
|
|
|
|
max: new Date().toISOString().slice(0, 4),
|
|
|
|
value: prop().split('-')[0],
|
|
|
|
},
|
|
|
|
hook: {
|
|
|
|
insert: vnode => {
|
|
|
|
const input = vnode.elm as HTMLInputElement;
|
|
|
|
validateRange(input);
|
|
|
|
input.addEventListener('change', e => {
|
|
|
|
const input = e.target as HTMLInputElement;
|
|
|
|
input.setCustomValidity('');
|
|
|
|
if (input.checkValidity()) {
|
|
|
|
validateRange(input);
|
|
|
|
prop(`${input.value}-${after() ? '12' : '01'}`);
|
|
|
|
redraw();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
update: (_, vnode) => validateRange(vnode.elm as HTMLInputElement),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-10-19 09:51:12 -06:00
|
|
|
const monthSection = (ctrl: ExplorerConfigCtrl) =>
|
2021-10-31 07:32:27 -06:00
|
|
|
h('section.date', [
|
2021-10-21 05:12:08 -06:00
|
|
|
h('label', ['Since', monthInput(ctrl.data.since, () => '', ctrl.root.redraw)]),
|
|
|
|
h('label', ['Until', monthInput(ctrl.data.until, ctrl.data.since, ctrl.root.redraw)]),
|
2021-10-19 09:51:12 -06:00
|
|
|
]);
|
|
|
|
|
2021-10-18 11:20:52 -06:00
|
|
|
const playerModal = (ctrl: ExplorerConfigCtrl) => {
|
2021-10-19 03:22:32 -06:00
|
|
|
const onSelect = (name: string) => {
|
2021-10-20 07:58:23 -06:00
|
|
|
ctrl.selectPlayer(name);
|
2021-10-19 08:43:04 -06:00
|
|
|
ctrl.root.redraw();
|
2021-10-19 03:22:32 -06:00
|
|
|
};
|
2021-10-18 11:20:52 -06:00
|
|
|
return snabModal({
|
|
|
|
class: 'explorer__config__player__choice',
|
|
|
|
onClose() {
|
|
|
|
ctrl.data.playerName.open(false);
|
2021-10-19 08:43:04 -06:00
|
|
|
ctrl.root.redraw();
|
2021-10-18 11:20:52 -06:00
|
|
|
},
|
|
|
|
content: [
|
|
|
|
h('h2', 'Personal opening explorer'),
|
|
|
|
h('div.input-wrapper', [
|
|
|
|
h('input', {
|
2021-10-19 08:43:04 -06:00
|
|
|
attrs: { placeholder: ctrl.root.trans.noarg('searchByUsername') },
|
2021-10-18 11:20:52 -06:00
|
|
|
hook: onInsert<HTMLInputElement>(input =>
|
|
|
|
lichess.userComplete().then(uac => {
|
|
|
|
uac({
|
|
|
|
input,
|
|
|
|
tag: 'span',
|
2021-10-19 03:22:32 -06:00
|
|
|
onSelect: v => onSelect(v.name),
|
2021-10-18 11:20:52 -06:00
|
|
|
});
|
|
|
|
input.focus();
|
|
|
|
})
|
|
|
|
),
|
|
|
|
}),
|
|
|
|
]),
|
2021-10-19 03:22:32 -06:00
|
|
|
h(
|
|
|
|
'div.previous',
|
2021-10-20 07:58:23 -06:00
|
|
|
[...(ctrl.myName ? [ctrl.myName] : []), ...ctrl.data.playerName.previous()].map(name =>
|
2021-10-19 03:22:32 -06:00
|
|
|
h(
|
2021-10-20 07:58:23 -06:00
|
|
|
`button.button${name == ctrl.myName ? '.button-green' : ''}`,
|
2021-10-19 03:22:32 -06:00
|
|
|
{
|
|
|
|
hook: bind('click', () => onSelect(name)),
|
|
|
|
},
|
|
|
|
name
|
|
|
|
)
|
|
|
|
)
|
|
|
|
),
|
2021-10-18 11:20:52 -06:00
|
|
|
],
|
|
|
|
});
|
|
|
|
};
|