mirror of
https://github.com/quantum5/qcal.git
synced 2025-04-24 17:51:57 -04:00
Factor out common calendar app logic
This commit is contained in:
parent
21f1110323
commit
89b3c6c4e1
60
common/src/ui/BaseApp.tsx
Normal file
60
common/src/ui/BaseApp.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {dateJDN} from '../gregorian';
|
||||||
|
|
||||||
|
type AppState<Selector> = {
|
||||||
|
selector: Selector;
|
||||||
|
todayJDN: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default abstract class BaseApp<Selector> extends React.Component<{}, AppState<Selector>> {
|
||||||
|
state: AppState<Selector>;
|
||||||
|
|
||||||
|
protected constructor(props: {}) {
|
||||||
|
super(props);
|
||||||
|
const todayJDN = dateJDN(new Date());
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
selector: this.parsePath() || this.defaultSelector(todayJDN),
|
||||||
|
todayJDN,
|
||||||
|
};
|
||||||
|
this.updateStateFromURL = this.updateStateFromURL.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract parsePath(): Selector | null;
|
||||||
|
|
||||||
|
abstract generatePath(selector: Selector): string;
|
||||||
|
|
||||||
|
abstract defaultSelector(todayJDN: number): Selector;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('popstate', this.updateStateFromURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('popstate', this.updateStateFromURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateStateFromURL(event: PopStateEvent) {
|
||||||
|
this.setState(event.state as Selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateURL() {
|
||||||
|
const path = this.generatePath(this.state.selector);
|
||||||
|
if (path !== window.location.pathname) {
|
||||||
|
window.history.pushState(this.state.selector, '', path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(state: any, callback?: () => void) {
|
||||||
|
super.setState(state, () => {
|
||||||
|
this.updateURL();
|
||||||
|
callback?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDateChange = (todayJDN: number) => {
|
||||||
|
this.setState({todayJDN});
|
||||||
|
};
|
||||||
|
|
||||||
|
abstract render(): JSX.Element;
|
||||||
|
}
|
22
common/src/ui/MonthBasedApp.tsx
Normal file
22
common/src/ui/MonthBasedApp.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import BaseApp from './BaseApp';
|
||||||
|
|
||||||
|
|
||||||
|
type Selector<Year, Month> = {
|
||||||
|
year: Year;
|
||||||
|
month: Month;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default abstract class MonthBasedApp<Year, Month> extends BaseApp<Selector<Year, Month>> {
|
||||||
|
abstract parseYearMonth(year: string, month: string): Selector<Year, Month> | null;
|
||||||
|
|
||||||
|
override parsePath(): Selector<Year, Month> | null {
|
||||||
|
const match = /\/(-?\d+)\/(\d+)/.exec(window.location.pathname);
|
||||||
|
if (!match)
|
||||||
|
return null;
|
||||||
|
return this.parseYearMonth(match[1], match[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
override generatePath({year, month}: Selector<Year, Month>) {
|
||||||
|
return `/${year}/${month}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,99 +1,42 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Calendar} from './Calendar';
|
import {Calendar} from './Calendar';
|
||||||
import {FrenchMonth, frEndJD, frStartJD, frSupportedYear, jdnFrench} from '@common/french';
|
import {FrenchMonth, frEndJD, frStartJD, frSupportedYear, jdnFrench} from '@common/french';
|
||||||
import {dateJDN} from '@common/gregorian';
|
import {JulianMonth} from '@common/gregorian';
|
||||||
import {TimeOfDay} from './TimeOfDay';
|
import {TimeOfDay} from './TimeOfDay';
|
||||||
import {GregorianJumper} from '@common/dateJump';
|
import {GregorianJumper} from '@common/dateJump';
|
||||||
|
import MonthBasedApp from '@common/ui/MonthBasedApp';
|
||||||
|
|
||||||
type YearMonth = {
|
export default class App extends MonthBasedApp<number, FrenchMonth> {
|
||||||
year: number;
|
override parseYearMonth(year: string, month: string) {
|
||||||
month: FrenchMonth;
|
if (!frSupportedYear(+year) || +month < 1 || +month > 13)
|
||||||
}
|
|
||||||
|
|
||||||
function parseURL(): YearMonth | null {
|
|
||||||
const match = /\/(-?\d+)\/(\d+)/.exec(window.location.pathname);
|
|
||||||
if (!match)
|
|
||||||
return null;
|
return null;
|
||||||
|
return {year: +year, month: +month as JulianMonth};
|
||||||
|
}
|
||||||
|
|
||||||
const month = +match[2];
|
override defaultSelector(todayJDN: number) {
|
||||||
const year = +match[1];
|
|
||||||
if (!frSupportedYear(year) || month < 1 || month > 13)
|
|
||||||
return null;
|
|
||||||
return {year: year, month: month as FrenchMonth};
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppState = YearMonth & {
|
|
||||||
todayJDN: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
class App extends React.Component<{}, AppState> {
|
|
||||||
state: AppState;
|
|
||||||
|
|
||||||
constructor(props: {}) {
|
|
||||||
super(props);
|
|
||||||
const todayJDN = dateJDN(new Date());
|
|
||||||
const {year, month} = jdnFrench(todayJDN);
|
const {year, month} = jdnFrench(todayJDN);
|
||||||
|
return {year, month};
|
||||||
this.state = {
|
|
||||||
...(parseURL() || {year, month}),
|
|
||||||
todayJDN,
|
|
||||||
};
|
|
||||||
this.updateStateFromURL = this.updateStateFromURL.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
window.addEventListener('popstate', this.updateStateFromURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.removeEventListener('popstate', this.updateStateFromURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateStateFromURL(event: PopStateEvent) {
|
|
||||||
this.setState(event.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateURL() {
|
|
||||||
const {year, month} = this.state;
|
|
||||||
const path = `/${year}/${month}`;
|
|
||||||
if (path !== window.location.pathname) {
|
|
||||||
window.history.pushState({year, month}, '', path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(state: any, callback?: () => void) {
|
|
||||||
super.setState(state, () => {
|
|
||||||
this.updateURL();
|
|
||||||
callback?.();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onDateChange = (todayJDN: number) => {
|
|
||||||
this.setState({todayJDN});
|
|
||||||
};
|
|
||||||
|
|
||||||
goToJDN = (jdn: number) => {
|
goToJDN = (jdn: number) => {
|
||||||
const {year, month} = jdnFrench(Math.min(Math.max(frStartJD, jdn), frEndJD));
|
const {year, month} = jdnFrench(Math.min(Math.max(frStartJD, jdn), frEndJD));
|
||||||
this.setState({year, month});
|
this.setState({selector: {year, month}});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {selector: {year, month}, todayJDN} = this.state;
|
||||||
return <>
|
return <>
|
||||||
<Calendar
|
<Calendar
|
||||||
year={this.state.year} month={this.state.month} todayJDN={this.state.todayJDN}
|
year={year} month={month} todayJDN={todayJDN}
|
||||||
onSwitch={(year, month) => {
|
onSwitch={(year, month) => this.setState({selector: {year, month}})}/>
|
||||||
this.setState({year, month});
|
|
||||||
}}/>
|
|
||||||
|
|
||||||
<TimeOfDay onDateChange={this.onDateChange}/>
|
<TimeOfDay onDateChange={this.onDateChange}/>
|
||||||
|
|
||||||
<div className="navigate">
|
<div className="navigate">
|
||||||
<h4>Go to a date</h4>
|
<h4>Go to a date</h4>
|
||||||
<GregorianJumper minJDN={frStartJD} maxJDN={frEndJD} todayJDN={this.state.todayJDN}
|
<GregorianJumper minJDN={frStartJD} maxJDN={frEndJD} todayJDN={todayJDN}
|
||||||
onJump={this.goToJDN}/>
|
onJump={this.goToJDN}/>
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
|
@ -1,103 +1,45 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Calendar} from './Calendar';
|
import {Calendar} from './Calendar';
|
||||||
import {dateJDN, gregorianJDN, JulianMonth} from '@common/gregorian';
|
import {gregorianJDN, JulianMonth} from '@common/gregorian';
|
||||||
import {DayChanger} from '@common/ui/DayChanger';
|
import {DayChanger} from '@common/ui/DayChanger';
|
||||||
import {jdnJulian} from '@common/julian';
|
import {jdnJulian} from '@common/julian';
|
||||||
import {GregorianJumper} from '@common/dateJump';
|
import {GregorianJumper} from '@common/dateJump';
|
||||||
|
import MonthBasedApp from '@common/ui/MonthBasedApp';
|
||||||
|
|
||||||
// Not real limitations other than JS number precision.
|
// Not real limitations other than JS number precision.
|
||||||
const START_JDN = gregorianJDN(-10_000_000_000_000, 1, 1);
|
const START_JDN = gregorianJDN(-10_000_000_000_000, 1, 1);
|
||||||
const END_JDN = gregorianJDN(10_000_000_000_000, 12, 31);
|
const END_JDN = gregorianJDN(10_000_000_000_000, 12, 31);
|
||||||
|
|
||||||
type YearMonth = {
|
export default class App extends MonthBasedApp<number, JulianMonth> {
|
||||||
year: number;
|
override parseYearMonth(year: string, month: string) {
|
||||||
month: JulianMonth;
|
if (+month < 1 || +month > 12)
|
||||||
}
|
|
||||||
|
|
||||||
function parseURL(): YearMonth | null {
|
|
||||||
const match = /\/(-?\d+)\/(\d+)/.exec(window.location.pathname);
|
|
||||||
if (!match)
|
|
||||||
return null;
|
return null;
|
||||||
|
return {year: +year, month: +month as JulianMonth};
|
||||||
|
}
|
||||||
|
|
||||||
const month = +match[2];
|
override defaultSelector(todayJDN: number) {
|
||||||
const year = +match[1];
|
|
||||||
if (month < 1 || month > 23)
|
|
||||||
return null;
|
|
||||||
return {year: year, month: month as JulianMonth};
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppState = YearMonth & {
|
|
||||||
todayJDN: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
class App extends React.Component<{}, AppState> {
|
|
||||||
state: AppState;
|
|
||||||
|
|
||||||
constructor(props: {}) {
|
|
||||||
super(props);
|
|
||||||
const todayJDN = dateJDN(new Date());
|
|
||||||
const [year, month] = jdnJulian(todayJDN);
|
const [year, month] = jdnJulian(todayJDN);
|
||||||
|
return {year, month};
|
||||||
this.state = {
|
|
||||||
...(parseURL() || {year, month}),
|
|
||||||
todayJDN,
|
|
||||||
};
|
|
||||||
this.updateStateFromURL = this.updateStateFromURL.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
window.addEventListener('popstate', this.updateStateFromURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
window.removeEventListener('popstate', this.updateStateFromURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateStateFromURL(event: PopStateEvent) {
|
|
||||||
this.setState(event.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateURL() {
|
|
||||||
const {year, month} = this.state;
|
|
||||||
const path = `/${year}/${month}`;
|
|
||||||
if (path !== window.location.pathname) {
|
|
||||||
window.history.pushState({year, month}, '', path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(state: any, callback?: () => void) {
|
|
||||||
super.setState(state, () => {
|
|
||||||
this.updateURL();
|
|
||||||
callback?.();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onDateChange = (todayJDN: number) => {
|
|
||||||
this.setState({todayJDN});
|
|
||||||
};
|
|
||||||
|
|
||||||
goToJDN = (jdn: number) => {
|
goToJDN = (jdn: number) => {
|
||||||
const [year, month] = jdnJulian(jdn);
|
const [year, month] = jdnJulian(jdn);
|
||||||
this.setState({year, month});
|
this.setState({selector: {year, month}});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {selector: {year, month}, todayJDN} = this.state;
|
||||||
return <>
|
return <>
|
||||||
<Calendar
|
<Calendar
|
||||||
year={this.state.year} month={this.state.month} todayJDN={this.state.todayJDN}
|
year={year} month={month} todayJDN={todayJDN}
|
||||||
onSwitch={(year, month) => {
|
onSwitch={(year, month) => this.setState({selector: {year, month}})}/>
|
||||||
this.setState({year, month});
|
|
||||||
}}/>
|
|
||||||
|
|
||||||
<DayChanger onDateChange={this.onDateChange}/>
|
<DayChanger onDateChange={this.onDateChange}/>
|
||||||
|
|
||||||
<div className="navigate">
|
<div className="navigate">
|
||||||
<h4>Go to a date</h4>
|
<h4>Go to a date</h4>
|
||||||
<GregorianJumper minJDN={START_JDN} maxJDN={END_JDN} todayJDN={this.state.todayJDN}
|
<GregorianJumper minJDN={START_JDN} maxJDN={END_JDN} todayJDN={todayJDN} onJump={this.goToJDN}/>
|
||||||
onJump={this.goToJDN}/>
|
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
Loading…
Reference in a new issue