mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-07-12 19:15:40 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { EntryInfoTyped } from './types.js';
|
||||
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
|
||||
import { Button, styled } from '@pezkuwi/react-components';
|
||||
|
||||
import { MONTHS } from './constants.js';
|
||||
import DayHour from './DayHour.js';
|
||||
import DayTime from './DayTime.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
date: Date;
|
||||
hasNextDay: boolean;
|
||||
now: Date;
|
||||
scheduled: EntryInfoTyped[];
|
||||
setNextDay: () => void;
|
||||
setPrevDay: () => void;
|
||||
setView: (v: boolean) => void;
|
||||
}
|
||||
|
||||
const HOURS = ((): number[] => {
|
||||
const hours: number[] = [];
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
hours.push(i);
|
||||
}
|
||||
|
||||
return hours;
|
||||
})();
|
||||
|
||||
function Day ({ className, date, hasNextDay, now, scheduled, setNextDay, setPrevDay, setView }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const monthRef = useRef(MONTHS.map((m) => t(m)));
|
||||
|
||||
const [isToday, nowHours, nowMinutes] = useMemo(
|
||||
() => [
|
||||
date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear() && date.getDate() === now.getDate(),
|
||||
now.getHours(),
|
||||
now.getMinutes()
|
||||
],
|
||||
[date, now]
|
||||
);
|
||||
|
||||
const _setView = useCallback(
|
||||
(): void => setView(true),
|
||||
[setView]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<h1>
|
||||
<div>
|
||||
<Button
|
||||
className='all-events-button'
|
||||
icon={'list'}
|
||||
onClick={_setView}
|
||||
/>
|
||||
{date.getDate()} {monthRef.current[date.getMonth()]} {date.getFullYear()} {isToday && <DayTime />}
|
||||
</div>
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='chevron-left'
|
||||
isDisabled={isToday}
|
||||
onClick={setPrevDay}
|
||||
/>
|
||||
<Button
|
||||
icon='chevron-right'
|
||||
isDisabled={!hasNextDay}
|
||||
onClick={setNextDay}
|
||||
/>
|
||||
</Button.Group>
|
||||
</h1>
|
||||
<div className='hoursContainer highlight--bg-faint'>
|
||||
{HOURS.map((hour, index): React.ReactNode =>
|
||||
<DayHour
|
||||
date={date}
|
||||
hour={hour}
|
||||
index={index}
|
||||
key={hour}
|
||||
minutes={(isToday && nowHours === index) ? nowMinutes : 0}
|
||||
scheduled={scheduled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
flex: 1;
|
||||
|
||||
.dayHeader {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: 1.25rem;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem 0;
|
||||
}
|
||||
|
||||
.hoursContainer {
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Day);
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { EntryInfoTyped } from './types.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-components';
|
||||
|
||||
import DayItem from './DayItem.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
date: Date;
|
||||
hour: number;
|
||||
index: number;
|
||||
minutes: number;
|
||||
scheduled: EntryInfoTyped[];
|
||||
}
|
||||
|
||||
const MN_TO_MS = 60 * 1000;
|
||||
const HR_TO_MS = 60 * MN_TO_MS;
|
||||
|
||||
function filterEntries (date: Date, minutes: number, index: number, scheduled: EntryInfoTyped[]): EntryInfoTyped[] {
|
||||
const start = date.getTime() + (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));
|
||||
}
|
||||
|
||||
function DayHour ({ className = '', date, hour, index, minutes, scheduled }: Props): React.ReactElement<Props> | null {
|
||||
const filtered = useMemo(
|
||||
() => filterEntries(date, minutes, index, scheduled),
|
||||
[date, index, minutes, scheduled]
|
||||
);
|
||||
|
||||
const hourStr = `${` ${hour}`.slice(-2)} ${hour >= 12 ? 'pm' : 'am'}`;
|
||||
|
||||
return (
|
||||
<StyledDiv 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>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: var(--bg-table);
|
||||
}
|
||||
|
||||
&.isPast {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.hourContainer {
|
||||
flex: 1;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.hourLabel {
|
||||
flex: 0;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 1;
|
||||
min-width: 5.5rem;
|
||||
opacity: var(--opacity-light);
|
||||
padding: 0.5rem 1rem;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.hasItems .hourLabel {
|
||||
font-size: 1.1rem;
|
||||
font-weight: var(--font-weight-normal);
|
||||
opacity: 1;
|
||||
padding: 0.7rem 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(DayHour);
|
||||
@@ -0,0 +1,250 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { EntryInfoTyped } from './types.js';
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, styled } from '@pezkuwi/react-components';
|
||||
import { formatNumber, isString } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from './translate.js';
|
||||
import { dateCalendarFormat } from './util.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
showAllEvents?: boolean;
|
||||
item: EntryInfoTyped;
|
||||
}
|
||||
|
||||
const FORMAT_OPTIONS: Intl.DateTimeFormatOptions = {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
weekday: 'long',
|
||||
year: 'numeric'
|
||||
};
|
||||
|
||||
function assertUnreachable (type: string): never {
|
||||
throw new Error(`We should not get here, unhandled ${type}`);
|
||||
}
|
||||
|
||||
function exportCalendar (date: Date, description: string): void {
|
||||
const startDate = dateCalendarFormat(date);
|
||||
// For now just add 1 hour for each event
|
||||
const endDate = dateCalendarFormat(new Date(new Date(date).setHours(new Date(date).getHours() + 1)));
|
||||
const calData =
|
||||
'BEGIN:VCALENDAR\n' +
|
||||
'CALSCALE:GREGORIAN\n' +
|
||||
'METHOD:PUBLISH\n' +
|
||||
'PRODID:-//Test Cal//EN\n' +
|
||||
'VERSION:2.0\n' +
|
||||
'BEGIN:VEVENT\n' +
|
||||
'UID:test-1\n' +
|
||||
'DTSTART;VALUE=DATE:' + startDate + '\n' +
|
||||
'DTEND;VALUE=DATE:' + endDate + '\n' +
|
||||
'SUMMARY:' + description + '\n' +
|
||||
'DESCRIPTION:' + description + '\n' +
|
||||
'END:VEVENT\n' +
|
||||
'END:VCALENDAR';
|
||||
const fileNameIcs = encodeURI(description) + '.ics';
|
||||
const data = new File([calData], fileNameIcs, { type: 'text/plain' });
|
||||
const anchor = window.document.createElement('a');
|
||||
|
||||
anchor.href = window.URL.createObjectURL(data);
|
||||
anchor.download = fileNameIcs;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
window.URL.revokeObjectURL(anchor.href);
|
||||
}
|
||||
|
||||
function createLink (to: string, desc: string): React.ReactNode {
|
||||
return <div className='itemLink'><a href={`#/${to}`}>{desc}</a></div>;
|
||||
}
|
||||
|
||||
function DayItem ({ className, item: { blockNumber, date, info, type }, showAllEvents }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [description, setDescription] = useState<string>('');
|
||||
|
||||
const _exportCal = useCallback(
|
||||
() => exportCalendar(date, description),
|
||||
[description, date]
|
||||
);
|
||||
|
||||
const desc = useMemo(
|
||||
(): React.ReactNode => {
|
||||
const id: string | null = info && (
|
||||
isString(info)
|
||||
? info
|
||||
: formatNumber(info)
|
||||
);
|
||||
const typeLink = ['councilElection'].includes(type)
|
||||
? createLink('council', t('via Council'))
|
||||
: ['councilMotion'].includes(type)
|
||||
? createLink('council/motions', t('via Council/Motions'))
|
||||
: ['democracyDispatch', 'scheduler'].includes(type)
|
||||
? createLink('democracy/dispatch', t('via Democracy/Dispatch'))
|
||||
: ['democracyLaunch', 'referendumDispatch', 'referendumVote'].includes(type)
|
||||
? createLink('/democracy', t('via Democracy'))
|
||||
: ['societyChallenge', 'societyRotate'].includes(type)
|
||||
? createLink('society', t('via Society'))
|
||||
: ['stakingEpoch', 'stakingEra'].includes(type)
|
||||
? createLink('staking', t('via Staking'))
|
||||
: ['stakingSlash'].includes(type)
|
||||
? createLink('staking/slashes', t('via Staking/Slashed'))
|
||||
: ['treasurySpend'].includes(type)
|
||||
? createLink('treasury', t('via Treasury'))
|
||||
: ['teyrchainLease'].includes(type)
|
||||
? createLink('teyrchains', t('via Teyrchains'))
|
||||
: ['teyrchainAuction'].includes(type)
|
||||
? createLink('teyrchains/auction', t('via Teyrchains/Auction'))
|
||||
: undefined;
|
||||
let s = '';
|
||||
|
||||
switch (type) {
|
||||
case 'councilElection':
|
||||
s = t('Election of new council candidates');
|
||||
break;
|
||||
|
||||
case 'councilMotion':
|
||||
s = t('Voting ends on council motion {{id}}', { replace: { id } });
|
||||
break;
|
||||
|
||||
case 'democracyDispatch':
|
||||
s = t('Enactment of the result of referendum {{id}}', { replace: { id } });
|
||||
break;
|
||||
|
||||
case 'democracyLaunch':
|
||||
s = t('Start of the next referendum voting period');
|
||||
break;
|
||||
|
||||
case 'teyrchainAuction':
|
||||
s = t('End of the current teyrchain auction {{id}}', { replace: { id } });
|
||||
break;
|
||||
|
||||
case 'teyrchainLease':
|
||||
s = t('Start of the next teyrchain lease period {{id}}', { replace: { id } });
|
||||
break;
|
||||
|
||||
case 'referendumDispatch':
|
||||
s = t('Potential dispatch of referendum {{id}} (if passed)', { replace: { id } });
|
||||
break;
|
||||
|
||||
case 'referendumVote':
|
||||
s = t('Voting ends for referendum {{id}}', { replace: { id } });
|
||||
break;
|
||||
|
||||
case 'scheduler':
|
||||
s = id
|
||||
? t('Execute named scheduled task {{id}}', { replace: { id } })
|
||||
: t('Execute anonymous scheduled task');
|
||||
break;
|
||||
|
||||
case 'stakingEpoch':
|
||||
s = t('Start of a new staking session {{id}}', { replace: { id } });
|
||||
break;
|
||||
|
||||
case 'stakingEra':
|
||||
s = t('Start of a new staking era {{id}}', { replace: { id } });
|
||||
break;
|
||||
|
||||
case 'stakingSlash':
|
||||
s = t('Application of slashes from era {{id}}', { replace: { id } });
|
||||
break;
|
||||
|
||||
case 'treasurySpend':
|
||||
s = t('Start of next spending period');
|
||||
break;
|
||||
|
||||
case 'societyChallenge':
|
||||
s = t('Start of next membership challenge period');
|
||||
break;
|
||||
|
||||
case 'societyRotate':
|
||||
s = t('Acceptance of new members and bids');
|
||||
break;
|
||||
|
||||
default:
|
||||
return assertUnreachable(type);
|
||||
}
|
||||
|
||||
setDescription(s);
|
||||
|
||||
return (<><div className='itemDesc'>{s}</div>{typeLink}</>);
|
||||
},
|
||||
[info, t, type]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
{showAllEvents &&
|
||||
<div className='itemDate'>{date.toLocaleString(undefined, FORMAT_OPTIONS)}</div>
|
||||
}
|
||||
<div className='itemTime'>{date.toLocaleTimeString().split(':').slice(0, 2).join(':')}</div>
|
||||
<div className='itemBlock'>#{formatNumber(blockNumber)}</div>
|
||||
{desc}
|
||||
{date && (
|
||||
<Button
|
||||
className={showAllEvents ? 'exportCal exportCal-allEvents' : 'exportCal'}
|
||||
icon='calendar-plus'
|
||||
onClick={_exportCal}
|
||||
/>
|
||||
)}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin: 0.5rem 0.75rem;
|
||||
|
||||
> div+div {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
> div.itemTime+div.itemBlock {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.exportCal {
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 1.5rem;
|
||||
|
||||
.ui--Icon {
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
.exportCal-allEvents {
|
||||
right: 3.5rem;
|
||||
}
|
||||
|
||||
.itemBlock {
|
||||
background: #aaa;
|
||||
color: #eee;
|
||||
font-size: var(--font-size-small);
|
||||
align-self: center;
|
||||
padding: 0.075rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.itemDate {
|
||||
padding: 0 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
width: 17rem;
|
||||
}
|
||||
|
||||
.itemTime {
|
||||
background: #999;
|
||||
color: #eee;
|
||||
padding: 0 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(DayItem);
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function DayTime (): React.ReactElement {
|
||||
const [now, setNow] = useState(new Date());
|
||||
|
||||
useEffect((): () => void => {
|
||||
const intervalId = setInterval(() => setNow(new Date()), 1000);
|
||||
|
||||
return (): void => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <>{now.toLocaleTimeString().split(':').slice(0, 2).join(':')}</>;
|
||||
}
|
||||
|
||||
export default React.memo(DayTime);
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DateState, EntryInfo } from './types.js';
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
|
||||
import { Button, styled } from '@pezkuwi/react-components';
|
||||
|
||||
import { DAYS, MONTHS } from './constants.js';
|
||||
import MonthDay from './MonthDay.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
hasNextMonth: boolean;
|
||||
lastDay: number;
|
||||
now: Date;
|
||||
scheduled: EntryInfo[];
|
||||
setDay: (day: number) => void;
|
||||
setNextMonth: () => void;
|
||||
setPrevMonth: () => void;
|
||||
state: DateState;
|
||||
}
|
||||
|
||||
function Month ({ className, hasNextMonth, lastDay, now, scheduled, setDay, setNextMonth, setPrevMonth, state: { dateMonth, dateSelected, days, startClass } }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dayOfWeekRef = useRef(DAYS.map((d) => t(d)));
|
||||
const monthRef = useRef(MONTHS.map((m) => t(m)));
|
||||
|
||||
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 (
|
||||
<StyledDiv className={className}>
|
||||
<h1>
|
||||
<div>{monthRef.current[dateMonth.getMonth()]} {dateMonth.getFullYear()}</div>
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='chevron-left'
|
||||
isDisabled={isNowYear && (isOlderMonth || isNowMonth)}
|
||||
onClick={setPrevMonth}
|
||||
/>
|
||||
<Button
|
||||
icon='chevron-right'
|
||||
isDisabled={!hasNextMonth}
|
||||
onClick={setNextMonth}
|
||||
/>
|
||||
</Button.Group>
|
||||
</h1>
|
||||
<div className={`calendar ${startClass}`}>
|
||||
<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))) || (!hasNextMonth && day > lastDay)}
|
||||
key={day}
|
||||
scheduled={scheduled}
|
||||
setDay={setDay}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
flex: 0;
|
||||
max-width: max-content;
|
||||
|
||||
.calendar {
|
||||
padding: 1rem 1.5rem;
|
||||
|
||||
.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 {
|
||||
> * {
|
||||
font-size: var(--font-size-tiny);
|
||||
font-weight: var(--font-weight-normal);
|
||||
letter-spacing: 0.1em;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.monthIndicator {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: 1.25rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Month);
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { EntryInfo } from './types.js';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-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 (
|
||||
<StyledDiv
|
||||
className={`${className} day ${isDisabled ? 'isDisabled' : (isCurrent ? 'highlight--bg-light highlight--color isSelected' : '')}`}
|
||||
onClick={_onClick}
|
||||
>
|
||||
{day}
|
||||
{hasEvents && <div className='eventIndicator highlight--border' />}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(MonthDay);
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { EntryInfoTyped } from './types.js';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { Button, styled } from '@pezkuwi/react-components';
|
||||
|
||||
import DayItem from './DayItem.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
scheduled: EntryInfoTyped[];
|
||||
setView: (v: boolean) => void;
|
||||
}
|
||||
|
||||
function UpcomingEvents ({ className, scheduled, setView }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const sched = useMemo(
|
||||
() => scheduled.sort((a, b) => a.dateTime - b.dateTime),
|
||||
[scheduled]
|
||||
);
|
||||
|
||||
const _setView = useCallback(
|
||||
(): void => setView(false),
|
||||
[setView]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<h1>
|
||||
<div>
|
||||
<Button
|
||||
className='all-events-button'
|
||||
icon='calendar'
|
||||
onClick={_setView}
|
||||
/>
|
||||
{t('Upcoming Events')}
|
||||
</div>
|
||||
</h1>
|
||||
<ul className='allEventsWrapper'>
|
||||
{sched.map((item, index): React.ReactNode => {
|
||||
return (
|
||||
<DayItem
|
||||
className='all-events-rows'
|
||||
item={item}
|
||||
key={index}
|
||||
showAllEvents
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
flex: 0;
|
||||
max-width: max-content;
|
||||
|
||||
.all-events-rows {
|
||||
padding: 10px 0;
|
||||
font-size: 13px;
|
||||
&:nth-child(odd) {
|
||||
background: var(--bg-table);
|
||||
}
|
||||
}
|
||||
|
||||
.allEventsWrapper {
|
||||
padding-inline-start: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(UpcomingEvents);
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
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'];
|
||||
@@ -0,0 +1,212 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DateState } from './types.js';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { styled, Tabs } from '@pezkuwi/react-components';
|
||||
|
||||
import Day from './Day.js';
|
||||
import Month from './Month.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
import UpcomingEvents from './UpcomingEvents.js';
|
||||
import useScheduled from './useScheduled.js';
|
||||
import { getDateState, nextMonth, prevMonth } from './util.js';
|
||||
|
||||
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 [dateState, setDateState] = useState(() => getDateState(now, now));
|
||||
const [allEventsView, setAllEventsView] = useState(false);
|
||||
|
||||
const itemsRef = useRef([{
|
||||
isRoot: true,
|
||||
name: 'view',
|
||||
text: t('Upcoming events')
|
||||
}]);
|
||||
|
||||
useEffect((): () => void => {
|
||||
const intervalId = setInterval(() => setNow(new Date()), NOW_INC);
|
||||
|
||||
return (): void => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [hasNextMonth, hasNextDay, lastDay] = useMemo(
|
||||
() => {
|
||||
const nextDay = new Date(dateState.dateSelected);
|
||||
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
|
||||
const nextDayTime = nextDay.getTime();
|
||||
const nextMonthTime = dateState.dateMonthNext.getTime();
|
||||
const hasNextMonth = scheduled.some(({ dateTime }) => dateTime >= nextMonthTime);
|
||||
const hasNextDay = hasNextMonth || scheduled.some(({ dateTime }) => dateTime >= nextDayTime);
|
||||
const lastDay = hasNextMonth
|
||||
? 42 // more than days in month
|
||||
: scheduled.reduce((lastDay: number, { date }): number => {
|
||||
return date.getFullYear() === dateState.dateMonth.getFullYear() && date.getMonth() === dateState.dateMonth.getMonth()
|
||||
? Math.max(lastDay, date.getDate())
|
||||
: lastDay;
|
||||
}, 0);
|
||||
|
||||
return [hasNextMonth, hasNextDay, lastDay];
|
||||
},
|
||||
[dateState, scheduled]
|
||||
);
|
||||
|
||||
const _nextMonth = useCallback(
|
||||
() => setDateState(({ dateMonth, dateSelected }) => getDateState(nextMonth(dateMonth), dateSelected)),
|
||||
[]
|
||||
);
|
||||
|
||||
const _prevMonth = useCallback(
|
||||
() => setDateState(({ dateMonth, dateSelected }) => getDateState(prevMonth(dateMonth), dateSelected)),
|
||||
[]
|
||||
);
|
||||
|
||||
const _nextDay = useCallback(
|
||||
() => setDateState(({ dateSelected }): DateState => {
|
||||
const date = new Date(dateSelected);
|
||||
|
||||
date.setDate(date.getDate() + 1);
|
||||
|
||||
return getDateState(date, date);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const _prevDay = useCallback(
|
||||
() => setDateState(({ dateSelected }): DateState => {
|
||||
const date = new Date(dateSelected);
|
||||
|
||||
date.setDate(date.getDate() - 1);
|
||||
|
||||
return getDateState(date, date);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const _setDay = useCallback(
|
||||
(day: number) => setDateState(({ dateMonth }): DateState => {
|
||||
const date = new Date(dateMonth);
|
||||
|
||||
date.setDate(day);
|
||||
|
||||
return getDateState(date, date);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const _setAllEventsView = useCallback(
|
||||
(v: boolean) => setAllEventsView(v),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledMain className={className}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
items={itemsRef.current}
|
||||
/>
|
||||
<div className='calendarFlex'>
|
||||
<Month
|
||||
hasNextMonth={hasNextMonth}
|
||||
lastDay={lastDay}
|
||||
now={now}
|
||||
scheduled={scheduled}
|
||||
setDay={_setDay}
|
||||
setNextMonth={_nextMonth}
|
||||
setPrevMonth={_prevMonth}
|
||||
state={dateState}
|
||||
/>
|
||||
<div className='wrapper-style'>
|
||||
{allEventsView
|
||||
? (
|
||||
<UpcomingEvents
|
||||
className='upcoming-events'
|
||||
scheduled={scheduled}
|
||||
setView={_setAllEventsView}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Day
|
||||
date={dateState.dateSelected}
|
||||
hasNextDay={hasNextDay}
|
||||
now={now}
|
||||
scheduled={scheduled}
|
||||
setNextDay={_nextDay}
|
||||
setPrevDay={_prevDay}
|
||||
setView={_setAllEventsView}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</StyledMain>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledMain = styled.main`
|
||||
.calendarFlex {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.wrapper-style {
|
||||
flex: 1;
|
||||
|
||||
.upcoming-events {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
background-color: var(--bg-table);
|
||||
border: 1px solid var(--border-table);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&+div {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.ui--Button-Group {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
align-items: center;
|
||||
border-bottom: 0.25rem solid var(--bg-page);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0.5rem 0.5rem 1rem;
|
||||
|
||||
> div:first-child {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.all-events-button {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.ui--Button {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(CalendarApp);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useTranslation as useTranslationBase } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): { t: (key: string, options?: { replace: Record<string, unknown> }) => string } {
|
||||
return useTranslationBase('app-calendar');
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
export type EntryType = 'councilElection' | 'councilMotion' | 'democracyDispatch' | 'democracyLaunch' | 'teyrchainAuction' | 'teyrchainLease' | 'referendumDispatch' | 'referendumVote' | 'scheduler' | 'societyChallenge' | 'societyRotate'| 'stakingEpoch' | 'stakingEra' | 'stakingSlash' | 'treasurySpend';
|
||||
|
||||
export interface EntryInfo {
|
||||
blockNumber: BN;
|
||||
blocks: BN;
|
||||
date: Date;
|
||||
dateTime: number;
|
||||
info: string | BN | null;
|
||||
isPending?: boolean;
|
||||
}
|
||||
|
||||
export interface EntryInfoTyped extends EntryInfo {
|
||||
type: EntryType;
|
||||
}
|
||||
|
||||
export interface DateState {
|
||||
dateMonth: Date;
|
||||
dateMonthNext: Date;
|
||||
dateSelected: Date;
|
||||
days: number[];
|
||||
startClass: string;
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal, DeriveDispatch, DeriveReferendumExt, DeriveSessionProgress } from '@pezkuwi/api-derive/types';
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { BlockNumber, EraIndex, LeasePeriodOf, Scheduled, UnappliedSlash } from '@pezkuwi/types/interfaces';
|
||||
import type { ITuple } from '@pezkuwi/types/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { EntryInfo, EntryInfoTyped, EntryType } from './types.js';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useLeaseRangeMax } from '@pezkuwi/app-teyrchains/useLeaseRanges';
|
||||
import { createNamedHook, useApi, useBestNumber, useBlockInterval, useCall } from '@pezkuwi/react-hooks';
|
||||
import { BN_ONE, BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
interface DateExt {
|
||||
date: Date;
|
||||
dateTime: number;
|
||||
}
|
||||
|
||||
type SlashEntry = [{ args: [EraIndex] }, UnappliedSlash[]];
|
||||
|
||||
type ScheduleEntry = [{ args: [BlockNumber] }, Option<Scheduled>[]];
|
||||
|
||||
function newDate (blocks: BN, blockTime: BN): DateExt {
|
||||
const date = new Date(Date.now() + blocks.mul(blockTime).toNumber());
|
||||
|
||||
return { date, dateTime: date.getTime() };
|
||||
}
|
||||
|
||||
function createConstDurations (bestNumber: BlockNumber, blockTime: BN, items: [EntryType, BlockNumber?, BN?, BN?][]): [EntryType, EntryInfo[]][] {
|
||||
return items.map(([type, duration, additional = BN_ZERO, offset = BN_ZERO]): [EntryType, EntryInfo[]] => {
|
||||
if (!duration) {
|
||||
return [type, []];
|
||||
}
|
||||
|
||||
const startNumber = bestNumber.sub(offset);
|
||||
const blocks = duration.sub(startNumber.mod(duration));
|
||||
|
||||
return [type, [{
|
||||
...newDate(blocks, blockTime),
|
||||
blockNumber: startNumber.add(blocks),
|
||||
blocks,
|
||||
info: startNumber.div(duration).iadd(additional)
|
||||
}]];
|
||||
});
|
||||
}
|
||||
|
||||
function createCouncilMotions (bestNumber: BlockNumber, blockTime: BN, motions: DeriveCollectiveProposal[]): [EntryType, EntryInfo[]][] {
|
||||
return [['councilMotion', motions
|
||||
.map(({ hash, votes }): EntryInfo | null => {
|
||||
if (!votes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hashStr = hash.toHex();
|
||||
const blocks = votes.end.sub(bestNumber);
|
||||
|
||||
return {
|
||||
...newDate(blocks, blockTime),
|
||||
blockNumber: votes.end,
|
||||
blocks,
|
||||
info: `${hashStr.slice(0, 6)}…${hashStr.slice(-4)}`
|
||||
};
|
||||
})
|
||||
.filter((item): item is EntryInfo => !!item)
|
||||
]];
|
||||
}
|
||||
|
||||
function createDispatches (bestNumber: BlockNumber, blockTime: BN, 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
|
||||
}]];
|
||||
});
|
||||
}
|
||||
|
||||
function createReferendums (bestNumber: BlockNumber, blockTime: BN, referendums: DeriveReferendumExt[]): [EntryType, EntryInfo[]][] {
|
||||
return referendums.reduce((result: [EntryType, EntryInfo[]][], { index, status }): [EntryType, EntryInfo[]][] => {
|
||||
const enactBlocks = status.end.add(status.delay).isub(bestNumber);
|
||||
const voteBlocks = status.end.sub(bestNumber).isub(BN_ONE);
|
||||
|
||||
result.push(['referendumVote', [{
|
||||
...newDate(voteBlocks, blockTime),
|
||||
blockNumber: bestNumber.add(voteBlocks),
|
||||
blocks: voteBlocks,
|
||||
info: index
|
||||
}]]);
|
||||
result.push(['referendumDispatch', [{
|
||||
...newDate(enactBlocks, blockTime),
|
||||
blockNumber: bestNumber.add(enactBlocks),
|
||||
blocks: enactBlocks,
|
||||
info: index,
|
||||
isPending: true
|
||||
}]]);
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function createStakingInfo (bestNumber: BlockNumber, blockTime: BN, sessionInfo: DeriveSessionProgress, unapplied: SlashEntry[], slashDeferDuration?: BlockNumber): [EntryType, EntryInfo[]][] {
|
||||
const blocksEra = sessionInfo.eraLength.sub(sessionInfo.eraProgress);
|
||||
const blocksSes = 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).isub(BN_ONE).imul(sessionInfo.eraLength).iadd(sessionInfo.eraProgress);
|
||||
const blocks = slashDuration.sub(blockProgress);
|
||||
|
||||
return {
|
||||
...newDate(blocks, blockTime),
|
||||
blockNumber: bestNumber.add(blocks),
|
||||
blocks,
|
||||
info: eraIndex
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
return [
|
||||
['stakingEpoch', [{
|
||||
...newDate(blocksSes, blockTime),
|
||||
blockNumber: bestNumber.add(blocksSes),
|
||||
blocks: blocksSes,
|
||||
info: sessionInfo.currentIndex.add(BN_ONE)
|
||||
}]],
|
||||
['stakingEra', [{
|
||||
...newDate(blocksEra, blockTime),
|
||||
blockNumber: bestNumber.add(blocksEra),
|
||||
blocks: blocksEra,
|
||||
info: sessionInfo.activeEra.add(BN_ONE)
|
||||
}]],
|
||||
['stakingSlash', slashEras]
|
||||
];
|
||||
}
|
||||
|
||||
function createScheduled (bestNumber: BlockNumber, blockTime: BN, 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
|
||||
});
|
||||
|
||||
return items;
|
||||
}, items);
|
||||
}, [])]];
|
||||
}
|
||||
|
||||
function createAuctionInfo (bestNumber: BlockNumber, blockTime: BN, rangeMax: BN, [leasePeriod, endBlock]: [LeasePeriodOf, BlockNumber]): [EntryType, EntryInfo[]][] {
|
||||
const blocks = endBlock.sub(bestNumber);
|
||||
|
||||
return [
|
||||
['teyrchainAuction', [{
|
||||
...newDate(blocks, blockTime),
|
||||
blockNumber: endBlock,
|
||||
blocks,
|
||||
info: `${leasePeriod.toString()} - ${leasePeriod.add(rangeMax).toString()}`
|
||||
}]]
|
||||
];
|
||||
}
|
||||
|
||||
function addFiltered (state: EntryInfoTyped[], types: [EntryType, EntryInfo[]][]): EntryInfoTyped[] {
|
||||
return types.reduce((state: EntryInfoTyped[], [typeFilter, items]): EntryInfoTyped[] => {
|
||||
return state
|
||||
.filter(({ type }) => type !== typeFilter)
|
||||
.concat(...items.map((item): EntryInfoTyped => {
|
||||
(item as EntryInfoTyped).type = typeFilter;
|
||||
|
||||
return item as EntryInfoTyped;
|
||||
}));
|
||||
}, state);
|
||||
}
|
||||
|
||||
// TODO council votes, tips closing
|
||||
function useScheduledImpl (): EntryInfoTyped[] {
|
||||
const { api } = useApi();
|
||||
const blockTime = useBlockInterval();
|
||||
const bestNumber = useBestNumber();
|
||||
const leaseRangeMax = useLeaseRangeMax();
|
||||
const auctionInfo = useCall<Option<ITuple<[LeasePeriodOf, BlockNumber]>>>(api.query.auctions?.auctionInfo);
|
||||
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);
|
||||
const sessionInfo = useCall<DeriveSessionProgress>(api.derive.session?.progress);
|
||||
const slashes = useCall<SlashEntry[]>(api.query.staking?.unappliedSlashes.entries);
|
||||
const [state, setState] = useState<EntryInfoTyped[]>([]);
|
||||
|
||||
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) && setState((state) =>
|
||||
addFiltered(state, createStakingInfo(bestNumber, blockTime, sessionInfo, slashes || [], api.consts.staking?.slashDeferDuration))
|
||||
);
|
||||
}, [api, bestNumber, blockTime, sessionInfo, slashes]);
|
||||
|
||||
useEffect((): void => {
|
||||
bestNumber && auctionInfo?.isSome && setState((state) =>
|
||||
addFiltered(state, createAuctionInfo(bestNumber, blockTime, leaseRangeMax, auctionInfo.unwrap()))
|
||||
);
|
||||
}, [auctionInfo, bestNumber, blockTime, leaseRangeMax]);
|
||||
|
||||
useEffect((): void => {
|
||||
bestNumber && setState((state) =>
|
||||
addFiltered(state, createConstDurations(bestNumber, blockTime, [
|
||||
['councilElection', (api.consts.elections || api.consts.phragmenElection || api.consts.electionsPhragmen)?.termDuration],
|
||||
['democracyLaunch', api.consts.democracy?.launchPeriod],
|
||||
['teyrchainLease', api.consts.slots?.leasePeriod as BlockNumber, BN_ONE, api.consts.slots?.leaseOffset as BlockNumber],
|
||||
['societyChallenge', api.consts.society?.challengePeriod],
|
||||
['societyRotate', api.consts.society?.rotationPeriod as BlockNumber],
|
||||
['treasurySpend', api.consts.treasury?.spendPeriod]
|
||||
]))
|
||||
);
|
||||
}, [api, bestNumber, blockTime]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default createNamedHook('useScheduled', useScheduledImpl);
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-calendar authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DateState } from './types.js';
|
||||
|
||||
import { DAYS } from './constants.js';
|
||||
|
||||
export function newZeroDate (input: Date): Date {
|
||||
const date = new Date(input);
|
||||
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
export 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);
|
||||
}
|
||||
|
||||
export 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);
|
||||
}
|
||||
|
||||
export function getDateState (_dateMonth: Date, _dateSelected: Date): DateState {
|
||||
const dateMonth = newZeroDate(_dateMonth);
|
||||
|
||||
dateMonth.setDate(1);
|
||||
|
||||
const dateMonthNext = nextMonth(dateMonth);
|
||||
const dateSelected = newZeroDate(_dateSelected);
|
||||
const numDays = nextMonth(dateMonth, 0).getDate();
|
||||
const days: number[] = [];
|
||||
|
||||
for (let i = 1; i <= numDays; i++) {
|
||||
days.push(i);
|
||||
}
|
||||
|
||||
return {
|
||||
dateMonth,
|
||||
dateMonthNext,
|
||||
dateSelected,
|
||||
days,
|
||||
startClass: `start${DAYS[dateMonth.getDay()]}`
|
||||
};
|
||||
}
|
||||
|
||||
export function dateCalendarFormat (date: Date): string {
|
||||
return new Date(date)
|
||||
.toISOString()
|
||||
.split('.')[0]
|
||||
.replace(/-/g, '')
|
||||
.replace(/:/g, '') + 'Z';
|
||||
}
|
||||
Reference in New Issue
Block a user