diff --git a/gcal/README.md b/gcal/README.md new file mode 100644 index 0000000..d3d2b5d --- /dev/null +++ b/gcal/README.md @@ -0,0 +1 @@ +# Gregorian Calendar diff --git a/gcal/config-overrides.js b/gcal/config-overrides.js new file mode 100644 index 0000000..5d15766 --- /dev/null +++ b/gcal/config-overrides.js @@ -0,0 +1,9 @@ +const {aliasWebpack, aliasJest} = require('react-app-alias-ex'); + +const options = {}; +module.exports = aliasWebpack(options); +module.exports.jest = function (config) { + const result = aliasJest(options)(config); + result.moduleDirectories.unshift(require('path').resolve(__dirname, '../node_modules')); + return result; +}; diff --git a/gcal/package.json b/gcal/package.json new file mode 100644 index 0000000..21fac2d --- /dev/null +++ b/gcal/package.json @@ -0,0 +1,51 @@ +{ + "name": "gcal", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.16.2", + "@testing-library/react": "^12.1.2", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.4.0", + "@types/node": "^16.11.24", + "@types/react": "^17.0.39", + "@types/react-dom": "^17.0.11", + "bootstrap": "~5.1.3", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "5.0.0", + "sass": "^1.49.7", + "sass-loader": "^12.4.0", + "typescript": "^4.5.5", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/bootstrap": "^5.1.9", + "react-app-alias-ex": "^2.1.0", + "react-app-rewired": "^2.2.1" + } +} diff --git a/gcal/public/index.html b/gcal/public/index.html new file mode 100644 index 0000000..74d85f8 --- /dev/null +++ b/gcal/public/index.html @@ -0,0 +1,53 @@ + + + + + + + + Gregorian Calendar + + + + +
+ + + + diff --git a/gcal/public/robots.txt b/gcal/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/gcal/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/gcal/src/App.tsx b/gcal/src/App.tsx new file mode 100644 index 0000000..4b5a1a4 --- /dev/null +++ b/gcal/src/App.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import {Calendar} from './Calendar'; +import {JulianMonth} from '@common/gregorian'; +import {DayChanger} from '@common/ui/DayChanger'; +import {jdnJulian} from '@common/julian'; +import MonthBasedApp from '@common/ui/MonthBasedApp'; + +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}; + } + + override defaultSelector(todayJDN: number) { + const [year, month] = jdnJulian(todayJDN); + return {year, month}; + } + + render() { + const {selector: {year, month}, todayJDN} = this.state; + return <> + this.setState({selector: {year, month}})}/> + + + ; + } +} diff --git a/gcal/src/Calendar.scss b/gcal/src/Calendar.scss new file mode 100644 index 0000000..be0744b --- /dev/null +++ b/gcal/src/Calendar.scss @@ -0,0 +1,7 @@ +@import 'bootstrap/scss/functions'; +@import 'bootstrap/scss/variables'; +@import 'bootstrap/scss/mixins'; +@import 'bootstrap/scss/grid'; +@import '@common/ui/consts.scss'; +@import '@common/ui/MonthBasedCalendar.scss'; +@import '@common/ui/SevenDayWeek.scss'; diff --git a/gcal/src/Calendar.tsx b/gcal/src/Calendar.tsx new file mode 100644 index 0000000..15cf03d --- /dev/null +++ b/gcal/src/Calendar.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import './Calendar.scss'; +import { + formatJG, + gregorianJDN, + gregorianMonthDays, + jdnGregorian, + JulianDay, + JulianMonth, + monthName, + weekdayNames, +} from '@common/gregorian'; +import {jdnLongCount} from '@common/longCount'; +import {jdnJulian, julianJDN, julianMonthDays} from '@common/julian'; +import {frDateFormat, frEndJD, frStartJD, jdnFrench} from '@common/french'; +import {useMobileTooltipProps} from '@common/ui/MobileTooltip'; +import {MonthBasedCalendar} from '@common/ui/MonthBasedCalendar'; + +type JulianYear = number; + +type MonthProps = { + year: JulianYear; + month: JulianMonth; +}; + +type DateProps = MonthProps & { + day: JulianDay; +}; + +function WeekdayName({name}: { name: string }): JSX.Element { + return
{name}
; +} + +function DayDetail({jdn}: { jdn: number }): JSX.Element { + const lc = jdnLongCount(jdn); + const mobile = useMobileTooltipProps(); + return
+
JD {jdn}
+
+ J.{' '} + {formatJG(jdnJulian(jdn))} +
+ {lc &&
+ LC{' '} + {lc.join('.\u200b')} +
} + {jdn >= frStartJD && jdn <= frEndJD &&
+ FR{' '} + {frDateFormat(jdnFrench(jdn))} +
} +
; +} + +function Day({year, month, day, todayJDN}: DateProps & { todayJDN: number }): JSX.Element { + const jdn = gregorianJDN(year, month, day); + return
+
{day}
+
{weekdayNames[(jdn + 1) % 7]}
+ +
; +} + +function Month({year, month, todayJDN}: MonthProps & { todayJDN: number }): JSX.Element { + const decadeHeads = weekdayNames.map((name, i) => ); + const firstJDN = gregorianJDN(year, month, 1); + const firstWeekday = (firstJDN + 1) % 7; + const daysTotal = gregorianMonthDays(year, month); + return
+
{decadeHeads}
+
{ + Array.from(Array(6).keys()).flatMap(i => { + if (i * 7 - firstWeekday + 1 > daysTotal) + return []; + return Array.from(Array(7).keys()).map(j => { + const day = i * 7 + j - firstWeekday + 1 as JulianDay; + if (day < 1 || day > daysTotal) + return
; + return
+ +
; + }); + }) + }
+
; +} + +export class Calendar extends MonthBasedCalendar { + override parseYear(year: string): JulianYear { + return +year; + } + + override parseMonth(month: string): JulianMonth { + return +month as JulianMonth; + } + + override yearToString(year: JulianYear): string { + return year.toString(); + } + + override monthToString(month: JulianMonth): string { + return month.toString(); + } + + private goToNormalized(year: number, month: number) { + while (month < 1) { + --year; + month += 12; + } + + while (month > 12) { + ++year; + month -= 12; + } + + this.goTo(year, month as JulianMonth); + } + + override prevYear = () => { + this.goToNormalized(this.props.year - 1, this.props.month); + }; + + override prevMonth = () => { + this.goToNormalized(this.props.year, this.props.month - 1); + }; + + override nextYear = () => { + this.goToNormalized(this.props.year + 1, this.props.month); + }; + + override nextMonth = () => { + this.goToNormalized(this.props.year, this.props.month + 1); + }; + + override isValidYear(year: string): boolean { + return /^-?\d+/.test(year); + } + + override jdnLookup(jdn: number): { year: JulianYear; month: JulianMonth } { + const [year, month] = jdnGregorian(jdn); + return {year, month}; + } + + override monthName(year: JulianYear, month: JulianMonth): string { + return `${monthName(month)} ${year}`; + } + + override renderMonthOptions(): JSX.Element[] { + return Array.from(Array(12).keys()).map(i => { + const month = i + 1 as JulianMonth; + return ; + }); + } + + override renderBody(): JSX.Element { + return + } +} diff --git a/gcal/src/index.tsx b/gcal/src/index.tsx new file mode 100644 index 0000000..8578ae3 --- /dev/null +++ b/gcal/src/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import 'bootstrap/js/dist/collapse'; +import '@common/ui/index.scss'; +import App from './App'; +import reportWebVitals from '@common/ui/reportWebVitals'; +import {MobileTooltipProvider} from '@common/ui/MobileTooltip'; + +ReactDOM.render( + + + + + , + document.getElementById('root'), +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/gcal/src/react-app-env.d.ts b/gcal/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/gcal/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/gcal/src/setupTests.ts b/gcal/src/setupTests.ts new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/gcal/src/setupTests.ts @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/gcal/tsconfig.json b/gcal/tsconfig.json new file mode 100644 index 0000000..9d6c7c6 --- /dev/null +++ b/gcal/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.paths.json", + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} diff --git a/gcal/tsconfig.paths.json b/gcal/tsconfig.paths.json new file mode 100644 index 0000000..1cacae9 --- /dev/null +++ b/gcal/tsconfig.paths.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@common/*": ["../common/src/*"] + } + } +} \ No newline at end of file