213 lines
4.9 KiB
TypeScript
213 lines
4.9 KiB
TypeScript
import { Ctrl, Challenge, ChallengeData, ChallengeDirection, ChallengeUser, TimeControl } from './interfaces';
|
|
import { h, VNode } from 'snabbdom';
|
|
|
|
export function loaded(ctrl: Ctrl): VNode {
|
|
return ctrl.redirecting()
|
|
? h('div#challenge-app.dropdown', h('div.initiating', spinner()))
|
|
: h('div#challenge-app.links.dropdown.rendered', renderContent(ctrl));
|
|
}
|
|
|
|
export function loading(): VNode {
|
|
return h('div#challenge-app.links.dropdown.rendered', [h('div.empty.loading', '-'), create()]);
|
|
}
|
|
|
|
function renderContent(ctrl: Ctrl): VNode[] {
|
|
const d = ctrl.data();
|
|
const nb = d.in.length + d.out.length;
|
|
return nb ? [allChallenges(ctrl, d, nb)] : [empty(), create()];
|
|
}
|
|
|
|
function userPowertips(vnode: VNode) {
|
|
lichess.powertip.manualUserIn(vnode.elm);
|
|
}
|
|
|
|
function allChallenges(ctrl: Ctrl, d: ChallengeData, nb: number): VNode {
|
|
return h(
|
|
'div.challenges',
|
|
{
|
|
class: { many: nb > 3 },
|
|
hook: {
|
|
insert: userPowertips,
|
|
postpatch: userPowertips,
|
|
},
|
|
},
|
|
d.in.map(challenge(ctrl, 'in')).concat(d.out.map(challenge(ctrl, 'out')))
|
|
);
|
|
}
|
|
|
|
function challenge(ctrl: Ctrl, dir: ChallengeDirection) {
|
|
return (c: Challenge) => {
|
|
return h(
|
|
'div.challenge.' + dir + '.c-' + c.id,
|
|
{
|
|
class: {
|
|
declined: !!c.declined,
|
|
},
|
|
},
|
|
[
|
|
h('div.content', [
|
|
h('span.head', renderUser(dir === 'in' ? c.challenger : c.destUser)),
|
|
h(
|
|
'span.desc',
|
|
[ctrl.trans()(c.rated ? 'rated' : 'casual'), timeControl(c.timeControl), c.variant.name].join(' • ')
|
|
),
|
|
]),
|
|
h('i', {
|
|
attrs: { 'data-icon': c.perf.icon },
|
|
}),
|
|
h('div.buttons', (dir === 'in' ? inButtons : outButtons)(ctrl, c)),
|
|
]
|
|
);
|
|
};
|
|
}
|
|
|
|
function inButtons(ctrl: Ctrl, c: Challenge): VNode[] {
|
|
const trans = ctrl.trans();
|
|
return [
|
|
h(
|
|
'form',
|
|
{
|
|
attrs: {
|
|
method: 'post',
|
|
action: `/challenge/${c.id}/accept`,
|
|
},
|
|
},
|
|
[
|
|
h('button.button.accept', {
|
|
attrs: {
|
|
type: 'submit',
|
|
'data-icon': 'E',
|
|
title: trans('accept'),
|
|
},
|
|
hook: onClick(ctrl.onRedirect),
|
|
}),
|
|
]
|
|
),
|
|
h('button.button.decline', {
|
|
attrs: {
|
|
type: 'submit',
|
|
'data-icon': 'L',
|
|
title: trans('decline'),
|
|
},
|
|
hook: onClick(() => ctrl.decline(c.id, 'generic')),
|
|
}),
|
|
h(
|
|
'select.decline-reason',
|
|
{
|
|
hook: {
|
|
insert: (vnode: VNode) => {
|
|
const select = vnode.elm as HTMLSelectElement;
|
|
select.addEventListener('change', () => ctrl.decline(c.id, select.value));
|
|
},
|
|
},
|
|
},
|
|
Object.entries(ctrl.reasons()).map(([key, name]) =>
|
|
h('option', { attrs: { value: key } }, key == 'generic' ? '' : name)
|
|
)
|
|
),
|
|
];
|
|
}
|
|
|
|
function outButtons(ctrl: Ctrl, c: Challenge) {
|
|
const trans = ctrl.trans();
|
|
return [
|
|
h('div.owner', [
|
|
h('span.waiting', ctrl.trans()('waiting')),
|
|
h('a.view', {
|
|
attrs: {
|
|
'data-icon': 'v',
|
|
href: '/' + c.id,
|
|
title: trans('viewInFullSize'),
|
|
},
|
|
}),
|
|
]),
|
|
h('button.button.decline', {
|
|
attrs: {
|
|
'data-icon': 'L',
|
|
title: trans('cancel'),
|
|
},
|
|
hook: onClick(() => ctrl.cancel(c.id)),
|
|
}),
|
|
];
|
|
}
|
|
|
|
function timeControl(c: TimeControl): string {
|
|
switch (c.type) {
|
|
case 'unlimited':
|
|
return 'Unlimited';
|
|
case 'correspondence':
|
|
return c.daysPerTurn + ' days';
|
|
case 'clock':
|
|
return c.show || '-';
|
|
}
|
|
}
|
|
|
|
function renderUser(u?: ChallengeUser): VNode {
|
|
if (!u) return h('span', 'Open challenge');
|
|
const rating = u.rating + (u.provisional ? '?' : '');
|
|
return h(
|
|
'a.ulpt.user-link',
|
|
{
|
|
attrs: { href: `/@/${u.name}` },
|
|
class: { online: !!u.online },
|
|
},
|
|
[
|
|
h('i.line' + (u.patron ? '.patron' : '')),
|
|
h('name', [
|
|
u.title && h('span.utitle', u.title == 'BOT' ? { attrs: { 'data-bot': true } } : {}, u.title + ' '),
|
|
u.name + ' (' + rating + ') ',
|
|
]),
|
|
h(
|
|
'signal',
|
|
u.lag === undefined
|
|
? []
|
|
: [1, 2, 3, 4].map(i =>
|
|
h('i', {
|
|
class: { off: u.lag! < i },
|
|
})
|
|
)
|
|
),
|
|
]
|
|
);
|
|
}
|
|
|
|
function create(): VNode {
|
|
return h('a.create', {
|
|
attrs: {
|
|
href: '/?any#friend',
|
|
'data-icon': 'O',
|
|
title: 'Challenge someone',
|
|
},
|
|
});
|
|
}
|
|
|
|
function empty(): VNode {
|
|
return h(
|
|
'div.empty.text',
|
|
{
|
|
attrs: {
|
|
'data-icon': '',
|
|
},
|
|
},
|
|
'No challenges.'
|
|
);
|
|
}
|
|
|
|
function onClick(f: (e: Event) => void) {
|
|
return {
|
|
insert: (vnode: VNode) => {
|
|
(vnode.elm as HTMLElement).addEventListener('click', f);
|
|
},
|
|
};
|
|
}
|
|
|
|
function spinner() {
|
|
return h('div.spinner', [
|
|
h('svg', { attrs: { viewBox: '0 0 40 40' } }, [
|
|
h('circle', {
|
|
attrs: { cx: 20, cy: 20, r: 18, fill: 'none' },
|
|
}),
|
|
]),
|
|
]);
|
|
}
|