Chain/Events calendar (#3505)
* Scaffolding * next/prevMonth * WIP * Day layout * Something shows * Scheduler itesm * Fix clacls, basics are there * styled localized * Constant retrievals * Extraction cleanups * extraMargin on article.error * It works. * Fix council ids * merge yarn
This commit is contained in:
parent
c6cb85de79
commit
b69dcc3fb0
20
packages/apps-routing/src/calendar.ts
Normal file
20
packages/apps-routing/src/calendar.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2017-2020 @polkadot/apps-routing authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { Route } from './types';
|
||||
|
||||
import Component from '@polkadot/app-calendar';
|
||||
|
||||
export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
|
||||
return {
|
||||
Component,
|
||||
display: {
|
||||
needsApi: []
|
||||
},
|
||||
group: 'network',
|
||||
icon: 'calendar-alt',
|
||||
name: 'calendar',
|
||||
text: t<string>('nav.calendar', 'Event calendar', { ns: 'apps-routing' })
|
||||
};
|
||||
}
|
||||
@ -6,6 +6,7 @@ import { Routes } from './types';
|
||||
|
||||
import accounts from './accounts';
|
||||
import addresses from './addresses';
|
||||
import calendar from './calendar';
|
||||
import claims from './claims';
|
||||
import contracts from './contracts';
|
||||
import council from './council';
|
||||
@ -45,6 +46,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
|
||||
techcomm(t),
|
||||
parachains(t),
|
||||
society(t),
|
||||
calendar(t),
|
||||
contracts(t),
|
||||
storage(t),
|
||||
extrinsics(t),
|
||||
|
||||
3
packages/apps/public/locales/en/app-calendar.json
Normal file
3
packages/apps/public/locales/en/app-calendar.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"Upcoming events": "Upcoming events"
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
[
|
||||
"app-accounts.json",
|
||||
"app-addresses.json",
|
||||
"app-calendar.json",
|
||||
"app-claims.json",
|
||||
"app-contracts.json",
|
||||
"app-council.json",
|
||||
|
||||
0
packages/page-calendar/.skip-build
Normal file
0
packages/page-calendar/.skip-build
Normal file
0
packages/page-calendar/.skip-npm
Normal file
0
packages/page-calendar/.skip-npm
Normal file
201
packages/page-calendar/LICENSE
Normal file
201
packages/page-calendar/LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
1
packages/page-calendar/README.md
Normal file
1
packages/page-calendar/README.md
Normal file
@ -0,0 +1 @@
|
||||
# @polkadot/app-calendar
|
||||
17
packages/page-calendar/package.json
Normal file
17
packages/page-calendar/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@polkadot/app-calendar",
|
||||
"private": true,
|
||||
"version": "0.56.0-beta.0",
|
||||
"description": "A on-chain events calendar",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"author": "Jaco Greeff <jacogr@gmail.com>",
|
||||
"maintainers": [
|
||||
"Jaco Greeff <jacogr@gmail.com>"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"@polkadot/react-components": "0.56.0-beta.0"
|
||||
}
|
||||
}
|
||||
102
packages/page-calendar/src/Day.tsx
Normal file
102
packages/page-calendar/src/Day.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { EntryInfo } from './types';
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Button } from '@polkadot/react-components';
|
||||
|
||||
import DayHour from './DayHour';
|
||||
import { MONTHS } from './constants';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
date: Date;
|
||||
now: Date;
|
||||
scheduled: EntryInfo[];
|
||||
}
|
||||
|
||||
const HOURS = ((): number[] => {
|
||||
const hours: number[] = [];
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
hours.push(i);
|
||||
}
|
||||
|
||||
return hours;
|
||||
})();
|
||||
|
||||
const NOOP = () => undefined;
|
||||
|
||||
function Day ({ className, date, now, scheduled }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const monthRef = useRef(MONTHS.map((m) => t(m)));
|
||||
|
||||
const [isToday, nowOffset] = useMemo(
|
||||
() => [
|
||||
date.getDate() === now.getDate() && date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear(),
|
||||
now.getHours()
|
||||
],
|
||||
[date, now]
|
||||
);
|
||||
|
||||
const offset = isToday
|
||||
? nowOffset
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='dayHeader'>
|
||||
<Button
|
||||
icon='chevron-left'
|
||||
isDisabled
|
||||
onClick={NOOP}
|
||||
/>
|
||||
<div>{date.getDate()} {monthRef.current[date.getMonth()]} {date.getFullYear()}</div>
|
||||
<Button
|
||||
icon='chevron-right'
|
||||
isDisabled
|
||||
onClick={NOOP}
|
||||
/>
|
||||
</div>
|
||||
<div className='hoursContainer'>
|
||||
{HOURS.map((hour, index): React.ReactNode =>
|
||||
<DayHour
|
||||
date={date}
|
||||
hour={(hour + offset) % 24}
|
||||
index={index}
|
||||
key={(hour + offset) % 24}
|
||||
minutes={(!isToday || index) ? 0 : now.getMinutes()}
|
||||
offset={offset}
|
||||
scheduled={scheduled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(Day)`
|
||||
flex: 1;
|
||||
margin-right: 1rem;
|
||||
|
||||
.dayHeader {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: 1.25rem;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem 0;
|
||||
|
||||
.ui--Button {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hoursContainer {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
`);
|
||||
98
packages/page-calendar/src/DayHour.tsx
Normal file
98
packages/page-calendar/src/DayHour.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { EntryInfo } from './types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import DayItem from './DayItem';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
date: Date;
|
||||
hour: number;
|
||||
index: number;
|
||||
minutes: number;
|
||||
offset: number;
|
||||
scheduled: EntryInfo[];
|
||||
}
|
||||
|
||||
const MN_TO_MS = 60 * 1000;
|
||||
const HR_TO_MS = 60 * MN_TO_MS;
|
||||
|
||||
function DayHour ({ className = '', date, hour, index, minutes, offset, scheduled }: Props): React.ReactElement<Props> | null {
|
||||
const filtered = useMemo(
|
||||
(): EntryInfo[] => {
|
||||
const start = date.getTime() + ((offset + index) * HR_TO_MS);
|
||||
const end = start + HR_TO_MS;
|
||||
const explicit = start + (minutes * MN_TO_MS);
|
||||
|
||||
return scheduled
|
||||
.filter(({ dateTime }) => dateTime >= explicit && dateTime < end)
|
||||
.sort((a, b) => (a.dateTime - b.dateTime) || a.type.localeCompare(b.type));
|
||||
},
|
||||
[date, index, minutes, offset, scheduled]
|
||||
);
|
||||
|
||||
const hourStr = `${` ${hour}`.slice(-2)} ${hour >= 12 ? 'pm' : 'am'}`;
|
||||
|
||||
return (
|
||||
<div className={`${className}${filtered.length ? ' hasItems' : ''}`}>
|
||||
<div className={`hourLabel${filtered.length ? ' highlight--color' : ''}`}>{hourStr}</div>
|
||||
<div className='hourContainer'>
|
||||
{filtered.map((item, index): React.ReactNode => (
|
||||
<DayItem
|
||||
item={item}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(DayHour)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: #faf8f6;
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: white;
|
||||
}
|
||||
|
||||
&.isPast {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.hourContainer {
|
||||
flex: 1;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.hourLabel {
|
||||
flex: 0;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 100;
|
||||
line-height: 1;
|
||||
min-width: 5.5rem;
|
||||
opacity: 0.5;
|
||||
padding: 0.25rem 1rem;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.hasItems .hourLabel {
|
||||
font-size: 1.1rem;
|
||||
font-weight: normal;
|
||||
opacity: 1;
|
||||
padding: 0.7rem 1rem;
|
||||
}
|
||||
`);
|
||||
126
packages/page-calendar/src/DayItem.tsx
Normal file
126
packages/page-calendar/src/DayItem.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { EntryInfo } from './types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { formatNumber, isString } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
item: EntryInfo;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function assertUnreachable (x: never): never {
|
||||
throw new Error('We cannot get here');
|
||||
}
|
||||
|
||||
function DayItem ({ className, item: { date, info, type } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const desc = useMemo(
|
||||
(): React.ReactNode => {
|
||||
const id: string | null = info && (
|
||||
isString(info)
|
||||
? info
|
||||
: formatNumber(info)
|
||||
);
|
||||
const typeLink = ['councilElection'].includes(type)
|
||||
? <div className='itemLink'><a href='#/council'>{t<string>('via Council')}</a></div>
|
||||
: ['councilMotion'].includes(type)
|
||||
? <div className='itemLink'><a href='#/council/motions'>{t<string>('via Council/Motions')}</a></div>
|
||||
: ['democracyDispatch', 'scheduler'].includes(type)
|
||||
? <div className='itemLink'><a href='#/democracy/dispatch'>{t<string>('via Democracy/Dispatch')}</a></div>
|
||||
: ['democracyLaunch', 'referendumDispatch', 'referendumVote'].includes(type)
|
||||
? <div className='itemLink'><a href='#/democracy'>{t<string>('via Democracy')}</a></div>
|
||||
: ['societyChallenge', 'societyRotate'].includes(type)
|
||||
? <div className='itemLink'><a href='#/society'>{t<string>('via Society')}</a></div>
|
||||
: ['stakingEra', 'stakingSession'].includes(type)
|
||||
? <div className='itemLink'><a href='#/staking'>{t<string>('via Staking')}</a></div>
|
||||
: ['stakingSlash'].includes(type)
|
||||
? <div className='itemLink'><a href='#/staking/slashes'>{t<string>('via Staking/Slashed')}</a></div>
|
||||
: ['treasurySpend'].includes(type)
|
||||
? <div className='itemLink'><a href='#/treasury'>{t<string>('via Treasury')}</a></div>
|
||||
: undefined;
|
||||
|
||||
switch (type) {
|
||||
case 'councilElection':
|
||||
return <><div className='itemDesc'>{t<string>('Election of new council candidates')}</div>{typeLink}</>;
|
||||
|
||||
case 'councilMotion':
|
||||
return <><div className='itemDesc'>{t<string>('Voting ends on council motion {{id}}', { replace: { id } })}</div>{typeLink}</>;
|
||||
|
||||
case 'democracyDispatch':
|
||||
return <><div className='itemDesc'>{t<string>('Enactment of the result of referendum {{id}}', { replace: { id } })}</div>{typeLink}</>;
|
||||
|
||||
case 'democracyLaunch':
|
||||
return <><div className='itemDesc'>{t<string>('Start of the next referendum voting period')}</div>{typeLink}</>;
|
||||
|
||||
case 'referendumDispatch':
|
||||
return <><div className='itemDesc'>{t<string>('Potential dispatch of referendum {{id}} (if passed)', { replace: { id } })}</div>{typeLink}</>;
|
||||
|
||||
case 'referendumVote':
|
||||
return <><div className='itemDesc'>{t<string>('Voting ends for referendum {{id}}', { replace: { id } })}</div>{typeLink}</>;
|
||||
|
||||
case 'scheduler':
|
||||
return <><div className='itemDesc'>{
|
||||
id
|
||||
? t<string>('Execute named scheduled task {{id}}', { replace: { id } })
|
||||
: t<string>('Execute anonymous scheduled task')
|
||||
}</div>{typeLink}</>;
|
||||
|
||||
case 'stakingEra':
|
||||
return <><div className='itemDesc'>{t<string>('Start of a new staking era {{id}}', { replace: { id } })}</div>{typeLink}</>;
|
||||
|
||||
case 'stakingSession':
|
||||
return <><div className='itemDesc'>{t<string>('Start of a new staking session {{id}}', { replace: { id } })}</div>{typeLink}</>;
|
||||
|
||||
case 'stakingSlash':
|
||||
return <><div className='itemDesc'>{t<string>('Application of slashes from era {{id}}', { replace: { id } })}</div>{typeLink}</>;
|
||||
|
||||
case 'treasurySpend':
|
||||
return <><div className='itemDesc'>{t<string>('Start of next spending period')}</div>{typeLink}</>;
|
||||
|
||||
case 'societyChallenge':
|
||||
return <><div className='itemDesc'>{t<string>('Start of next membership challenge period')}</div>{typeLink}</>;
|
||||
|
||||
case 'societyRotate':
|
||||
return <><div className='itemDesc'>{t<string>('Acceptance of new members and bids')}</div>{typeLink}</>;
|
||||
|
||||
default:
|
||||
return assertUnreachable(type);
|
||||
}
|
||||
},
|
||||
[info, t, type]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='itemTime'>{date.toLocaleTimeString().split(':').slice(0, 2).join(':')}</div>
|
||||
{desc}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(DayItem)`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin: 0.5rem 0.75rem;
|
||||
|
||||
> div+div {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.itemTime {
|
||||
background: #999;
|
||||
padding: 0 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
color: #eee;
|
||||
}
|
||||
`);
|
||||
201
packages/page-calendar/src/Month.tsx
Normal file
201
packages/page-calendar/src/Month.tsx
Normal file
@ -0,0 +1,201 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { EntryInfo } from './types';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Button } from '@polkadot/react-components';
|
||||
|
||||
import { DAYS, MONTHS } from './constants';
|
||||
import { useTranslation } from './translate';
|
||||
import MonthDay from './MonthDay';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
now: Date;
|
||||
onChange: (date: Date) => void;
|
||||
scheduled: EntryInfo[];
|
||||
}
|
||||
|
||||
interface DateState {
|
||||
dateMonth: Date;
|
||||
dateSelected: Date;
|
||||
days: number[];
|
||||
startClass: string;
|
||||
}
|
||||
|
||||
function newZeroDate (input: Date): Date {
|
||||
const date = new Date(input);
|
||||
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
function nextMonth (date: Date, firstDay = 1): Date {
|
||||
const currMonth = date.getMonth();
|
||||
|
||||
return currMonth === 11
|
||||
? new Date(date.getFullYear() + 1, 0, firstDay)
|
||||
: new Date(date.getFullYear(), currMonth + 1, firstDay);
|
||||
}
|
||||
|
||||
function prevMonth (date: Date): Date {
|
||||
const currMonth = date.getMonth();
|
||||
|
||||
return currMonth === 0
|
||||
? new Date(date.getFullYear() - 1, 11, 1)
|
||||
: new Date(date.getFullYear(), currMonth - 1, 1);
|
||||
}
|
||||
|
||||
function getDateState (_dateMonth: Date, _dateSelected: Date): DateState {
|
||||
const dateMonth = newZeroDate(_dateMonth);
|
||||
const dateSelected = newZeroDate(_dateSelected);
|
||||
|
||||
dateMonth.setDate(1);
|
||||
|
||||
const numDays = nextMonth(dateMonth, 0).getDate();
|
||||
const days: number[] = [];
|
||||
|
||||
for (let i = 1; i <= numDays; i++) {
|
||||
days.push(i);
|
||||
}
|
||||
|
||||
return {
|
||||
dateMonth,
|
||||
dateSelected,
|
||||
days,
|
||||
startClass: `start${DAYS[dateMonth.getDay()]}`
|
||||
};
|
||||
}
|
||||
|
||||
function Month ({ className, now, onChange, scheduled }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [{ dateMonth, dateSelected, days, startClass }, setDate] = useState(getDateState(now, now));
|
||||
|
||||
const dayOfWeekRef = useRef(DAYS.map((d) => t(d)));
|
||||
const monthRef = useRef(MONTHS.map((m) => t(m)));
|
||||
|
||||
const _nextMonth = useCallback(
|
||||
() => setDate(({ dateMonth, dateSelected }) => getDateState(nextMonth(dateMonth), dateSelected)),
|
||||
[]
|
||||
);
|
||||
|
||||
const _prevMonth = useCallback(
|
||||
() => setDate(({ dateMonth, dateSelected }) => getDateState(prevMonth(dateMonth), dateSelected)),
|
||||
[]
|
||||
);
|
||||
|
||||
const _setDay = useCallback(
|
||||
(day: number) => setDate(({ dateMonth }): DateState => {
|
||||
const date = new Date(dateMonth);
|
||||
|
||||
date.setDate(day);
|
||||
|
||||
return getDateState(date, date);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
onChange(dateSelected);
|
||||
}, [dateSelected, onChange]);
|
||||
|
||||
const [isCurrYear, isCurrMonth, isNowYear, isNowMonth, isOlderMonth] = useMemo(
|
||||
() => [
|
||||
dateMonth.getFullYear() === dateSelected.getFullYear(),
|
||||
dateMonth.getMonth() === dateSelected.getMonth(),
|
||||
now.getFullYear() === dateMonth.getFullYear(),
|
||||
now.getMonth() === dateMonth.getMonth(),
|
||||
now.getMonth() > dateMonth.getMonth()
|
||||
],
|
||||
[dateMonth, dateSelected, now]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={`calendar ${startClass}`}>
|
||||
<div className='monthIndicator'>
|
||||
<Button
|
||||
icon='chevron-left'
|
||||
isDisabled={isNowYear && (isOlderMonth || isNowMonth)}
|
||||
onClick={_prevMonth}
|
||||
/>
|
||||
<div>{monthRef.current[dateMonth.getMonth()]} {dateMonth.getFullYear()}</div>
|
||||
<Button
|
||||
icon='chevron-right'
|
||||
onClick={_nextMonth}
|
||||
/>
|
||||
</div>
|
||||
<div className='dayOfWeek'>
|
||||
{dayOfWeekRef.current.map((day): React.ReactNode => (
|
||||
<div key={day}>{t(day)}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='dateGrid'>
|
||||
{days.map((day): React.ReactNode => (
|
||||
<MonthDay
|
||||
dateMonth={dateMonth}
|
||||
day={day}
|
||||
isCurrent={isCurrYear && isCurrMonth && day === dateSelected.getDate()}
|
||||
isDisabled={isNowYear && (isOlderMonth || (isNowMonth && now.getDate() > day))}
|
||||
key={day}
|
||||
scheduled={scheduled}
|
||||
setDay={_setDay}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(Month)`
|
||||
flex: 0;
|
||||
max-width: max-content;
|
||||
padding: 1rem 1.5rem;
|
||||
|
||||
.calendar {
|
||||
.dateGrid,
|
||||
.dayOfWeek {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
}
|
||||
|
||||
.dateGrid {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
&.startSun .dateGrid .day:first-child { grid-column: 1 }
|
||||
&.startMon .dateGrid .day:first-child { grid-column: 2 }
|
||||
&.startTue .dateGrid .day:first-child { grid-column: 3 }
|
||||
&.startWed .dateGrid .day:first-child { grid-column: 4 }
|
||||
&.startThu .dateGrid .day:first-child { grid-column: 5 }
|
||||
&.startFri .dateGrid .day:first-child { grid-column: 6 }
|
||||
&.startSat .dateGrid .day:first-child { grid-column: 7 }
|
||||
|
||||
.dayOfWeek {
|
||||
margin-top: 1.25em;
|
||||
|
||||
> * {
|
||||
font-size: 0.7em;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.monthIndicator {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: 1.25rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
`);
|
||||
94
packages/page-calendar/src/MonthDay.tsx
Normal file
94
packages/page-calendar/src/MonthDay.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { EntryInfo } from './types';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
dateMonth: Date;
|
||||
day: number;
|
||||
isCurrent: boolean;
|
||||
isDisabled: boolean;
|
||||
setDay: (day: number) => void;
|
||||
scheduled: EntryInfo[];
|
||||
}
|
||||
|
||||
const DAY_TO_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function MonthDay ({ className = '', dateMonth, day, isCurrent, isDisabled, scheduled, setDay }: Props): React.ReactElement<Props> {
|
||||
const hasEvents = useMemo(
|
||||
(): boolean => {
|
||||
const start = dateMonth.getTime() + ((day - 1) * DAY_TO_MS);
|
||||
const end = start + DAY_TO_MS;
|
||||
|
||||
return scheduled.some(({ dateTime }) => dateTime >= start && dateTime < end);
|
||||
},
|
||||
[dateMonth, day, scheduled]
|
||||
);
|
||||
|
||||
const _onClick = useCallback(
|
||||
(): void => {
|
||||
!isDisabled && setDay(day);
|
||||
},
|
||||
[day, isDisabled, setDay]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`day${isDisabled ? ' isDisabled' : (isCurrent ? ' highlight--bg-light highlight--color isSelected' : '')} ${className}`}
|
||||
onClick={_onClick}
|
||||
>
|
||||
{day}
|
||||
{hasEvents && <div className='eventIndicator highlight--border' />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(MonthDay)`
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
line-height: 1;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
|
||||
&:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&:not(.isDisabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:not(.isSelected):hover {
|
||||
background: #f7f5f3;
|
||||
}
|
||||
|
||||
.eventIndicator {
|
||||
border: 0.25rem solid transparent;
|
||||
border-radius: 50%;
|
||||
height: 0.25rem;
|
||||
position: absolute;
|
||||
right: 0.625rem;
|
||||
top: 0.625rem;
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
&.isDisabled {
|
||||
opacity: 0.375;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.eventIndicator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`);
|
||||
6
packages/page-calendar/src/constants.ts
Normal file
6
packages/page-calendar/src/constants.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
export const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
export const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
77
packages/page-calendar/src/index.tsx
Normal file
77
packages/page-calendar/src/index.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Tabs } from '@polkadot/react-components';
|
||||
|
||||
import Day from './Day';
|
||||
import Month from './Month';
|
||||
import { useTranslation } from './translate';
|
||||
import useScheduled from './useScheduled';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NOW_INC = 30 * 1000;
|
||||
|
||||
function CalendarApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const scheduled = useScheduled();
|
||||
const [now, setNow] = useState(new Date());
|
||||
const [selected, setSelected] = useState(new Date());
|
||||
|
||||
const itemsRef = useRef([{
|
||||
isRoot: true,
|
||||
name: 'view',
|
||||
text: t<string>('Upcoming events')
|
||||
}]);
|
||||
|
||||
useEffect((): () => void => {
|
||||
const intervalId = setInterval(() => setNow(new Date()), NOW_INC);
|
||||
|
||||
return (): void => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
<header>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
items={itemsRef.current}
|
||||
/>
|
||||
</header>
|
||||
<div className='calendarFlex'>
|
||||
<Day
|
||||
date={selected}
|
||||
now={now}
|
||||
scheduled={scheduled}
|
||||
/>
|
||||
<Month
|
||||
now={now}
|
||||
onChange={setSelected}
|
||||
scheduled={scheduled}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(CalendarApp)`
|
||||
.calendarFlex {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
> div {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
`);
|
||||
9
packages/page-calendar/src/translate.ts
Normal file
9
packages/page-calendar/src/translate.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { useTranslation as useTranslationBase, UseTranslationResponse } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): UseTranslationResponse {
|
||||
return useTranslationBase('app-calendar');
|
||||
}
|
||||
17
packages/page-calendar/src/types.ts
Normal file
17
packages/page-calendar/src/types.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import BN from 'bn.js';
|
||||
|
||||
export type EntryType = 'councilElection' | 'councilMotion' | 'democracyDispatch' | 'democracyLaunch' | 'referendumDispatch' | 'referendumVote' | 'scheduler' | 'societyChallenge' | 'societyRotate'| 'stakingEra' | 'stakingSession' | 'stakingSlash' | 'treasurySpend';
|
||||
|
||||
export interface EntryInfo {
|
||||
blockNumber: BN;
|
||||
blocks: BN;
|
||||
date: Date;
|
||||
dateTime: number;
|
||||
info: string | BN | null;
|
||||
isPending?: boolean;
|
||||
type: EntryType;
|
||||
}
|
||||
241
packages/page-calendar/src/useScheduled.ts
Normal file
241
packages/page-calendar/src/useScheduled.ts
Normal file
@ -0,0 +1,241 @@
|
||||
// Copyright 2017-2020 @polkadot/app-calendar authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { DeriveCollectiveProposal, DeriveDispatch, DeriveReferendumExt, DeriveSessionProgress } from '@polkadot/api-derive/types';
|
||||
import { BlockNumber, EraIndex, Scheduled, UnappliedSlash } from '@polkadot/types/interfaces';
|
||||
import { EntryInfo, EntryType } from './types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useApi, useBlockTime, useCall } from '@polkadot/react-hooks';
|
||||
import { Option } from '@polkadot/types';
|
||||
import { BN_ONE } from '@polkadot/util';
|
||||
|
||||
interface DateExt {
|
||||
date: Date;
|
||||
dateTime: number;
|
||||
}
|
||||
|
||||
type SlashEntry = [{ args: [EraIndex] }, UnappliedSlash[]];
|
||||
|
||||
type ScheduleEntry = [{ args: [BlockNumber] }, Option<Scheduled>[]];
|
||||
|
||||
function newDate (remaining: BN, blockTime: number): DateExt {
|
||||
const date = new Date(Date.now() + remaining.muln(blockTime).toNumber());
|
||||
|
||||
return { date, dateTime: date.getTime() };
|
||||
}
|
||||
|
||||
function createConstDurations (bestNumber: BlockNumber, blockTime: number, items: [EntryType, BlockNumber?][]): [EntryType, EntryInfo[]][] {
|
||||
return items.map(([type, duration]): [EntryType, EntryInfo[]] => {
|
||||
if (!duration) {
|
||||
return [type, []];
|
||||
}
|
||||
|
||||
const blocks = duration.sub(bestNumber.mod(duration));
|
||||
|
||||
return [type, [{
|
||||
...newDate(blocks, blockTime),
|
||||
blockNumber: bestNumber.add(blocks),
|
||||
blocks,
|
||||
info: null,
|
||||
type
|
||||
}]];
|
||||
});
|
||||
}
|
||||
|
||||
function createCouncilMotions (bestNumber: BlockNumber, blockTime: number, motions: DeriveCollectiveProposal[]): [EntryType, EntryInfo[]][] {
|
||||
return motions
|
||||
.map(({ hash, votes }): [EntryType, EntryInfo[]] | null => {
|
||||
if (!votes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hashStr = hash.toHex();
|
||||
const blocks = votes.end.sub(bestNumber);
|
||||
|
||||
return ['councilMotion', [{
|
||||
...newDate(blocks, blockTime),
|
||||
blockNumber: votes.end,
|
||||
blocks,
|
||||
info: `${hashStr.substr(0, 6)}…${hashStr.substr(-4)}`,
|
||||
type: 'councilMotion'
|
||||
}]];
|
||||
})
|
||||
.filter((item): item is [EntryType, EntryInfo[]] => !!item);
|
||||
}
|
||||
|
||||
function createDispatches (bestNumber: BlockNumber, blockTime: number, dispatches: DeriveDispatch[]): [EntryType, EntryInfo[]][] {
|
||||
return dispatches.map(({ at, index }): [EntryType, EntryInfo[]] => {
|
||||
const blocks = at.sub(bestNumber);
|
||||
|
||||
return ['democracyDispatch', [{
|
||||
...newDate(blocks, blockTime),
|
||||
blockNumber: at,
|
||||
blocks,
|
||||
info: index,
|
||||
type: 'democracyDispatch'
|
||||
}]];
|
||||
});
|
||||
}
|
||||
|
||||
function createReferendums (bestNumber: BlockNumber, blockTime: number, referendums: DeriveReferendumExt[]): [EntryType, EntryInfo[]][] {
|
||||
return referendums.reduce((result: [EntryType, EntryInfo[]][], { index, status }): [EntryType, EntryInfo[]][] => {
|
||||
const enactBlocks = status.end.add(status.delay).sub(bestNumber);
|
||||
const voteBlocks = status.end.sub(bestNumber).subn(1);
|
||||
|
||||
result.push(['referendumVote', [{
|
||||
...newDate(voteBlocks, blockTime),
|
||||
blockNumber: bestNumber.add(voteBlocks),
|
||||
blocks: voteBlocks,
|
||||
info: index,
|
||||
type: 'referendumVote'
|
||||
}]]);
|
||||
result.push(['referendumDispatch', [{
|
||||
...newDate(enactBlocks, blockTime),
|
||||
blockNumber: bestNumber.add(enactBlocks),
|
||||
blocks: enactBlocks,
|
||||
info: index,
|
||||
isPending: true,
|
||||
type: 'referendumDispatch'
|
||||
}]]);
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function createStakingInfo (bestNumber: BlockNumber, blockTime: number, sessionInfo: DeriveSessionProgress, unapplied: SlashEntry[], slashDeferDuration?: BlockNumber): [EntryType, EntryInfo[]][] {
|
||||
const remainingEra = sessionInfo.eraLength.sub(sessionInfo.eraProgress);
|
||||
const remainingSes = sessionInfo.sessionLength.sub(sessionInfo.sessionProgress);
|
||||
const slashDuration = slashDeferDuration?.mul(sessionInfo.eraLength);
|
||||
const slashEras = slashDuration
|
||||
? unapplied
|
||||
.filter(([, values]) => values.length)
|
||||
.map(([key]): EntryInfo => {
|
||||
const eraIndex = key.args[0];
|
||||
const blockProgress = sessionInfo.activeEra.sub(eraIndex).subn(1).mul(sessionInfo.eraLength).add(sessionInfo.eraProgress);
|
||||
const blocks = slashDuration.sub(blockProgress);
|
||||
|
||||
return {
|
||||
...newDate(blocks, blockTime),
|
||||
blockNumber: bestNumber.add(blocks),
|
||||
blocks,
|
||||
info: eraIndex,
|
||||
type: 'stakingSlash'
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
return [
|
||||
['stakingEra', [{
|
||||
...newDate(remainingEra, blockTime),
|
||||
blockNumber: bestNumber.add(remainingEra),
|
||||
blocks: remainingEra,
|
||||
info: sessionInfo.activeEra.add(BN_ONE),
|
||||
type: 'stakingEra'
|
||||
}]],
|
||||
['stakingSession', [{
|
||||
...newDate(remainingSes, blockTime),
|
||||
blockNumber: bestNumber.add(remainingSes),
|
||||
blocks: remainingSes,
|
||||
info: sessionInfo.currentIndex.add(BN_ONE),
|
||||
type: 'stakingSession'
|
||||
}]],
|
||||
['stakingSlash', slashEras]
|
||||
];
|
||||
}
|
||||
|
||||
function createScheduled (bestNumber: BlockNumber, blockTime: number, scheduled: ScheduleEntry[]): [EntryType, EntryInfo[]][] {
|
||||
return [['scheduler', scheduled
|
||||
.filter(([, vecSchedOpt]) => vecSchedOpt.some((schedOpt) => schedOpt.isSome))
|
||||
.reduce((items: EntryInfo[], [key, vecSchedOpt]): EntryInfo[] => {
|
||||
const blockNumber = key.args[0];
|
||||
|
||||
return vecSchedOpt
|
||||
.filter((schedOpt) => schedOpt.isSome)
|
||||
.map((schedOpt) => schedOpt.unwrap())
|
||||
.reduce((items: EntryInfo[], { maybeId }) => {
|
||||
const idOrNull = maybeId.unwrapOr(null);
|
||||
const blocks = blockNumber.sub(bestNumber);
|
||||
|
||||
items.push({
|
||||
...newDate(blocks, blockTime),
|
||||
blockNumber,
|
||||
blocks,
|
||||
info: idOrNull
|
||||
? idOrNull.isAscii
|
||||
? idOrNull.toUtf8()
|
||||
: idOrNull.toHex()
|
||||
: null,
|
||||
type: 'scheduler'
|
||||
});
|
||||
|
||||
return items;
|
||||
}, items);
|
||||
}, [])]];
|
||||
}
|
||||
|
||||
function addFiltered (state: EntryInfo[], types: [EntryType, EntryInfo[]][]): EntryInfo[] {
|
||||
return types.reduce((state: EntryInfo[], [typeFilter, items]): EntryInfo[] =>
|
||||
state.filter(({ type }) => type !== typeFilter).concat(...items), state
|
||||
);
|
||||
}
|
||||
|
||||
// TODO council votes, tips closing
|
||||
export default function useScheduled (): EntryInfo[] {
|
||||
const { api } = useApi();
|
||||
const [blockTime] = useBlockTime();
|
||||
const bestNumber = useCall<BlockNumber>(api.derive.chain.bestNumber);
|
||||
const councilMotions = useCall<DeriveCollectiveProposal[]>(api.derive.council?.proposals);
|
||||
const dispatches = useCall<DeriveDispatch[]>(api.derive.democracy?.dispatchQueue);
|
||||
const referendums = useCall<DeriveReferendumExt[]>(api.derive.democracy?.referendums);
|
||||
const scheduled = useCall<ScheduleEntry[]>(api.query.scheduler?.agenda.entries as any);
|
||||
const sessionInfo = useCall<DeriveSessionProgress>(api.query.staking && api.derive.session?.progress);
|
||||
const slashes = useCall<SlashEntry[]>(api.query.staking?.unappliedSlashes.entries as any);
|
||||
const [state, setState] = useState<EntryInfo[]>([]);
|
||||
|
||||
useEffect((): void => {
|
||||
bestNumber && dispatches && setState((state) =>
|
||||
addFiltered(state, createDispatches(bestNumber, blockTime, dispatches))
|
||||
);
|
||||
}, [bestNumber, blockTime, dispatches]);
|
||||
|
||||
useEffect((): void => {
|
||||
bestNumber && councilMotions && setState((state) =>
|
||||
addFiltered(state, createCouncilMotions(bestNumber, blockTime, councilMotions))
|
||||
);
|
||||
}, [bestNumber, blockTime, councilMotions]);
|
||||
|
||||
useEffect((): void => {
|
||||
bestNumber && referendums && setState((state) =>
|
||||
addFiltered(state, createReferendums(bestNumber, blockTime, referendums))
|
||||
);
|
||||
}, [bestNumber, blockTime, referendums]);
|
||||
|
||||
useEffect((): void => {
|
||||
bestNumber && scheduled && setState((state) =>
|
||||
addFiltered(state, createScheduled(bestNumber, blockTime, scheduled))
|
||||
);
|
||||
}, [bestNumber, blockTime, scheduled]);
|
||||
|
||||
useEffect((): void => {
|
||||
bestNumber && sessionInfo?.sessionLength.gt(BN_ONE) && slashes && setState((state) =>
|
||||
addFiltered(state, createStakingInfo(bestNumber, blockTime, sessionInfo, slashes, api.consts.staking.slashDeferDuration))
|
||||
);
|
||||
}, [api, bestNumber, blockTime, sessionInfo, slashes]);
|
||||
|
||||
useEffect((): void => {
|
||||
bestNumber && setState((state) =>
|
||||
addFiltered(state, createConstDurations(bestNumber, blockTime, [
|
||||
['councilElection', (api.consts.elections || api.consts.electionsPhragmen)?.termDuration],
|
||||
['democracyLaunch', api.consts.democracy?.launchPeriod],
|
||||
['societyChallenge', api.consts.society?.challengePeriod],
|
||||
['societyRotate', api.consts.society?.rotationPeriod],
|
||||
['treasurySpend', api.consts.treasury?.spendPeriod]
|
||||
]))
|
||||
);
|
||||
}, [api, bestNumber, blockTime]);
|
||||
|
||||
return state;
|
||||
}
|
||||
@ -52,7 +52,7 @@ class ErrorBoundary extends React.Component<Props> {
|
||||
|
||||
return error
|
||||
? (
|
||||
<article className='error'>
|
||||
<article className='error extraMargin'>
|
||||
{t<string>('Uncaught error. Something went wrong with the query and rendering of this component. {{message}}', {
|
||||
replace: { message: error.message }
|
||||
})}
|
||||
|
||||
@ -72,6 +72,10 @@ export default createGlobalStyle<Props>`
|
||||
background: ${(props: Props) => `linear-gradient(90deg, ${props.uiHighlight || defaultHighlight}, transparent)`};
|
||||
}
|
||||
|
||||
.highlight--hover-bg:hover {
|
||||
background: ${getHighlight} !important;
|
||||
}
|
||||
|
||||
.highlight--hover-color:hover {
|
||||
color: ${getHighlight} !important;
|
||||
}
|
||||
@ -219,6 +223,10 @@ export default createGlobalStyle<Props>`
|
||||
}
|
||||
}
|
||||
|
||||
&.extraMargin {
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
&.nomargin {
|
||||
margin: 0.5rem auto;
|
||||
max-width: 75rem;
|
||||
|
||||
@ -38,7 +38,7 @@ export default function useAvailableSlashes (): [BN, UnappliedSlash[]][] {
|
||||
mountedRef.current && setSlashes(
|
||||
values
|
||||
.map((value, index): [BN, UnappliedSlash[]] => [from.addn(index), value])
|
||||
.filter(([, slashes]): boolean => slashes.length !== 0)
|
||||
.filter(([, slashes]) => slashes.length)
|
||||
);
|
||||
});
|
||||
})().catch(console.error);
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
"@polkadot/app-accounts/*": [ "packages/page-accounts/src/*" ],
|
||||
"@polkadot/app-addresses": [ "packages/page-addresses/src" ],
|
||||
"@polkadot/app-addresses/*": [ "packages/page-addresses/src/*" ],
|
||||
"@polkadot/app-calendar": [ "packages/page-calendar/src" ],
|
||||
"@polkadot/app-calendar/*": [ "packages/page-calendar/src/*" ],
|
||||
"@polkadot/app-claims": [ "packages/page-claims/src" ],
|
||||
"@polkadot/app-claims/*": [ "packages/page-claims/src/*" ],
|
||||
"@polkadot/app-contracts": [ "packages/page-contracts/src" ],
|
||||
|
||||
@ -3137,6 +3137,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/app-calendar@workspace:packages/page-calendar":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/app-calendar@workspace:packages/page-calendar"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.11.2
|
||||
"@polkadot/react-components": 0.56.0-beta.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@polkadot/app-claims@workspace:packages/page-claims":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@polkadot/app-claims@workspace:packages/page-claims"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user