diff --git a/common/src/ui/BaseApp.tsx b/common/src/ui/BaseApp.tsx new file mode 100644 index 0000000..d47c021 --- /dev/null +++ b/common/src/ui/BaseApp.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import {dateJDN} from '../gregorian'; + +type AppState = { + selector: Selector; + todayJDN: number, +}; + +export default abstract class BaseApp extends React.Component<{}, AppState> { + state: AppState; + + 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; +} diff --git a/common/src/ui/MonthBasedApp.tsx b/common/src/ui/MonthBasedApp.tsx new file mode 100644 index 0000000..f2d2560 --- /dev/null +++ b/common/src/ui/MonthBasedApp.tsx @@ -0,0 +1,22 @@ +import BaseApp from './BaseApp'; + + +type Selector = { + year: Year; + month: Month; +} + +export default abstract class MonthBasedApp extends BaseApp> { + abstract parseYearMonth(year: string, month: string): Selector | null; + + override parsePath(): Selector | 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) { + return `/${year}/${month}`; + } +} diff --git a/frcal/src/App.tsx b/frcal/src/App.tsx index e833e7c..a855221 100644 --- a/frcal/src/App.tsx +++ b/frcal/src/App.tsx @@ -1,99 +1,42 @@ import React from 'react'; import {Calendar} from './Calendar'; import {FrenchMonth, frEndJD, frStartJD, frSupportedYear, jdnFrench} from '@common/french'; -import {dateJDN} from '@common/gregorian'; +import {JulianMonth} from '@common/gregorian'; import {TimeOfDay} from './TimeOfDay'; import {GregorianJumper} from '@common/dateJump'; +import MonthBasedApp from '@common/ui/MonthBasedApp'; -type YearMonth = { - year: number; - month: FrenchMonth; -} +export default class App extends MonthBasedApp { + override parseYearMonth(year: string, month: string) { + if (!frSupportedYear(+year) || +month < 1 || +month > 13) + return null; + return {year: +year, month: +month as JulianMonth}; + } -function parseURL(): YearMonth | null { - const match = /\/(-?\d+)\/(\d+)/.exec(window.location.pathname); - if (!match) - return null; - - const month = +match[2]; - 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()); + override defaultSelector(todayJDN: number) { const {year, month} = jdnFrench(todayJDN); - - this.state = { - ...(parseURL() || {year, month}), - todayJDN, - }; - this.updateStateFromURL = this.updateStateFromURL.bind(this); + return {year, month}; } - 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) => { const {year, month} = jdnFrench(Math.min(Math.max(frStartJD, jdn), frEndJD)); - this.setState({year, month}); + this.setState({selector: {year, month}}); }; render() { + const {selector: {year, month}, todayJDN} = this.state; return <> { - this.setState({year, month}); - }}/> + year={year} month={month} todayJDN={todayJDN} + onSwitch={(year, month) => this.setState({selector: {year, month}})}/>

Go to a date

-
; } } - -export default App; diff --git a/jcal/src/App.tsx b/jcal/src/App.tsx index 157d537..4fb5f00 100644 --- a/jcal/src/App.tsx +++ b/jcal/src/App.tsx @@ -1,103 +1,45 @@ import React from 'react'; import {Calendar} from './Calendar'; -import {dateJDN, gregorianJDN, JulianMonth} from '@common/gregorian'; +import {gregorianJDN, JulianMonth} from '@common/gregorian'; import {DayChanger} from '@common/ui/DayChanger'; import {jdnJulian} from '@common/julian'; import {GregorianJumper} from '@common/dateJump'; +import MonthBasedApp from '@common/ui/MonthBasedApp'; // Not real limitations other than JS number precision. const START_JDN = gregorianJDN(-10_000_000_000_000, 1, 1); const END_JDN = gregorianJDN(10_000_000_000_000, 12, 31); -type YearMonth = { - year: number; - month: JulianMonth; -} +export default class App extends MonthBasedApp { + override parseYearMonth(year: string, month: string) { + if (+month < 1 || +month > 12) + return null; + return {year: +year, month: +month as JulianMonth}; + } -function parseURL(): YearMonth | null { - const match = /\/(-?\d+)\/(\d+)/.exec(window.location.pathname); - if (!match) - return null; - - const month = +match[2]; - 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()); + override defaultSelector(todayJDN: number) { const [year, month] = jdnJulian(todayJDN); - - this.state = { - ...(parseURL() || {year, month}), - todayJDN, - }; - this.updateStateFromURL = this.updateStateFromURL.bind(this); + return {year, month}; } - 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) => { const [year, month] = jdnJulian(jdn); - this.setState({year, month}); + this.setState({selector: {year, month}}); }; render() { + const {selector: {year, month}, todayJDN} = this.state; return <> { - this.setState({year, month}); - }}/> + year={year} month={month} todayJDN={todayJDN} + onSwitch={(year, month) => this.setState({selector: {year, month}})}/>

Go to a date

- +
; } } - -export default App;