From 18365af5dc026799a2b932a587c1df3c933d1d3d Mon Sep 17 00:00:00 2001 From: Tony Grosinger Date: Sat, 20 Apr 2024 15:56:48 -0700 Subject: [PATCH] Add calendar pulling from Google Calendar --- .env.example | 8 +- README.md | 19 +-- package-lock.json | 186 +++++++++++++++++++++----- package.json | 13 +- src/app/calendar/calendar.tsx | 241 ++++++++++++++++++++++++++++++++++ src/app/calendar/page.tsx | 119 +++++++++++++++++ src/app/page.tsx | 87 +++++------- src/app/rental/page.tsx | 6 + src/app/upcoming-events.json | 24 ---- src/components/Footer.tsx | 1 + src/components/Header.tsx | 2 + 11 files changed, 576 insertions(+), 130 deletions(-) create mode 100644 src/app/calendar/calendar.tsx create mode 100644 src/app/calendar/page.tsx delete mode 100644 src/app/upcoming-events.json diff --git a/.env.example b/.env.example index 349d712..36f40c3 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,7 @@ -NEXT_PUBLIC_SITE_URL=https://example.com +NEXT_PUBLIC_SITE_URL=https://westsoundhall.org +STRIPE_SECRET_KEY=sk_XXXX +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_XXXX +POSTMARK_SERVER_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +FROM_ADDRESS=support@westsoundhall.org +ADMIN_ADDRESS=board@westsoundhall.org +CALENDAR_ADDR=https://calendar.google.com/calendar/ical/westsoundcommunityclub%40gmail.com/private-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/basic.ics diff --git a/README.md b/README.md index b8f03c8..2ec3d96 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,7 @@ https://westsoundhall.org ## Running -Pre-build containers are created whenever a version is tagged in this -repository. Pull the [latest -version](https://git.grosinger.net/tgrosinger/-/packages/container/west-sound-hall/) -and run on a server with Docker available. +Pre-build containers are created whenever a version is tagged in this repository. Pull the [latest version](https://git.grosinger.net/tgrosinger/-/packages/container/west-sound-hall/) and run on a server with Docker available. ```sh docker run -p 3000:3000 git.grosinger.net/tgrosinger/west-sound-hall:0.0.14 @@ -17,11 +14,11 @@ docker run -p 3000:3000 git.grosinger.net/tgrosinger/west-sound-hall:0.0.14 ## Updating -### Events on the Homepage +### Events on the Homepage and the Calendar -The homepage has a list of upcoming events. This list is created from [`src/app/upcoming-events.json`](https://git.grosinger.net/tgrosinger/west-sound-hall/src/branch/main/src/app/upcoming-events.json). To update the events listed, modify that file, tag a new version, and then update the running container to the latest version. +The events on the calendar are loaded from the westsoundcommunityclub@gmail.com Google Calendar. -Events in the past will be automatically hidden from view. +Please note that all events on the calendar will be displayed. If an event should not reveal the title to the public, add the word "Private" to the event description (not the title). ### News Posts @@ -41,11 +38,7 @@ To get started, first install the npm dependencies: npm install ``` -Next, create a `.env.local` file in the root of your project and set the `NEXT_PUBLIC_SITE_URL` variable to your site's public URL: - -``` -NEXT_PUBLIC_SITE_URL=https://example.com -``` +Next, copy the `.env.example` file from this directory and call it `.env.local`. Fill in the values that have been redacted with their actual secrets. Be sure to use the test environment key from Stripe unless you are setting up production. Next, run the development server: @@ -59,4 +52,4 @@ Finally, open [http://localhost:3000](http://localhost:3000) in your browser to This site is based off of the Spotlight template from Tailwind, and licensed under the [Tailwind UI license](https://tailwindui.com/license). -It was purchased by Tony Grosinger. +It was purchased by [Tony Grosinger](mailto:tony@grosinger.net). diff --git a/package-lock.json b/package-lock.json index 0d52947..c0f524c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,15 +18,13 @@ "@stripe/stripe-js": "2.2.1", "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "^0.5.4", - "@types/node": "20.4.7", - "@types/react": "18.2.18", - "@types/react-dom": "18.2.7", - "@types/webpack-env": "^1.18.1", "autoprefixer": "^10.4.12", "cheerio": "^1.0.0-rc.12", "clsx": "^1.2.1", + "dayjs": "^1.11.10", "fast-glob": "^3.2.11", "feed": "^4.2.2", + "ical": "^0.8.0", "next": "13.4.16", "next-themes": "^0.2.1", "postmark": "4.0.2", @@ -39,6 +37,11 @@ "typescript": "5.1.6" }, "devDependencies": { + "@types/ical": "^0.8.3", + "@types/node": "20.4.7", + "@types/react": "18.2.18", + "@types/react-dom": "18.2.7", + "@types/webpack-env": "^1.18.1", "eslint": "8.45.0", "eslint-config-next": "13.4.16", "prettier": "^3.0.1", @@ -701,6 +704,33 @@ "@types/unist": "^2" } }, + "node_modules/@types/ical": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@types/ical/-/ical-0.8.3.tgz", + "integrity": "sha512-qPejGORaXOstmqyKzp0Qw9nXDPiWiahiJJcx4zMB0zJVg0rLfJ6bDip/naqagEqYTjKl/LI91399hR8zFwRJ5A==", + "dev": true, + "dependencies": { + "rrule": "2.6.4" + } + }, + "node_modules/@types/ical/node_modules/rrule": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.4.tgz", + "integrity": "sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==", + "dev": true, + "dependencies": { + "tslib": "^1.10.0" + }, + "optionalDependencies": { + "luxon": "^1.21.3" + } + }, + "node_modules/@types/ical/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -755,6 +785,7 @@ "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "dev": true, "dependencies": { "@types/react": "*" } @@ -772,7 +803,8 @@ "node_modules/@types/webpack-env": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.1.tgz", - "integrity": "sha512-D0HJET2/UY6k9L6y3f5BL+IDxZmPkYmPT4+qBrRdmRLYRuV0qNKizMgTvYxXZYn+36zjPeoDZAEYBCM6XB+gww==" + "integrity": "sha512-D0HJET2/UY6k9L6y3f5BL+IDxZmPkYmPT4+qBrRdmRLYRuV0qNKizMgTvYxXZYn+36zjPeoDZAEYBCM6XB+gww==", + "dev": true }, "node_modules/@typescript-eslint/parser": { "version": "5.62.0", @@ -2243,6 +2275,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3472,9 +3509,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -4110,6 +4147,14 @@ "node": ">=14.18.0" } }, + "node_modules/ical": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/ical/-/ical-0.8.0.tgz", + "integrity": "sha512-/viUSb/RGLLnlgm0lWRlPBtVeQguQRErSPYl3ugnUaKUnzQswKqOG3M8/P1v1AB5NJwlHTuvTq1cs4mpeG2rCg==", + "dependencies": { + "rrule": "2.4.1" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5596,6 +5641,15 @@ "node": ">=10" } }, + "node_modules/luxon": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz", + "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==", + "optional": true, + "engines": { + "node": "*" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -6978,9 +7032,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -7644,9 +7698,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -7662,9 +7716,9 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -8388,6 +8442,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrule": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.4.1.tgz", + "integrity": "sha512-+NcvhETefswZq13T8nkuEnnQ6YgUeZaqMqVbp+ZiFDPCbp3AVgQIwUvNVDdMNrP05bKZG9ddDULFp0qZZYDrxg==", + "optionalDependencies": { + "luxon": "^1.3.3" + } + }, "node_modules/run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", @@ -8859,9 +8921,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -10911,6 +10973,33 @@ "@types/unist": "^2" } }, + "@types/ical": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@types/ical/-/ical-0.8.3.tgz", + "integrity": "sha512-qPejGORaXOstmqyKzp0Qw9nXDPiWiahiJJcx4zMB0zJVg0rLfJ6bDip/naqagEqYTjKl/LI91399hR8zFwRJ5A==", + "dev": true, + "requires": { + "rrule": "2.6.4" + }, + "dependencies": { + "rrule": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.4.tgz", + "integrity": "sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==", + "dev": true, + "requires": { + "luxon": "^1.21.3", + "tslib": "^1.10.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -10965,6 +11054,7 @@ "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "dev": true, "requires": { "@types/react": "*" } @@ -10982,7 +11072,8 @@ "@types/webpack-env": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.1.tgz", - "integrity": "sha512-D0HJET2/UY6k9L6y3f5BL+IDxZmPkYmPT4+qBrRdmRLYRuV0qNKizMgTvYxXZYn+36zjPeoDZAEYBCM6XB+gww==" + "integrity": "sha512-D0HJET2/UY6k9L6y3f5BL+IDxZmPkYmPT4+qBrRdmRLYRuV0qNKizMgTvYxXZYn+36zjPeoDZAEYBCM6XB+gww==", + "dev": true }, "@typescript-eslint/parser": { "version": "5.62.0", @@ -12041,6 +12132,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -12956,9 +13052,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "for-each": { "version": "0.3.3", @@ -13400,6 +13496,14 @@ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", "dev": true }, + "ical": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/ical/-/ical-0.8.0.tgz", + "integrity": "sha512-/viUSb/RGLLnlgm0lWRlPBtVeQguQRErSPYl3ugnUaKUnzQswKqOG3M8/P1v1AB5NJwlHTuvTq1cs4mpeG2rCg==", + "requires": { + "rrule": "2.4.1" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -14463,6 +14567,12 @@ "yallist": "^4.0.0" } }, + "luxon": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz", + "integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==", + "optional": true + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -15382,9 +15492,9 @@ } }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, "napi-build-utils": { "version": "1.0.2", @@ -15840,13 +15950,13 @@ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" }, "postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "requires": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" } }, "postcss-import": { @@ -16310,6 +16420,14 @@ "glob": "^7.1.3" } }, + "rrule": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.4.1.tgz", + "integrity": "sha512-+NcvhETefswZq13T8nkuEnnQ6YgUeZaqMqVbp+ZiFDPCbp3AVgQIwUvNVDdMNrP05bKZG9ddDULFp0qZZYDrxg==", + "requires": { + "luxon": "^1.3.3" + } + }, "run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", @@ -16623,9 +16741,9 @@ "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" }, "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" }, "source-map-support": { "version": "0.5.21", diff --git a/package.json b/package.json index d104452..ec5331b 100644 --- a/package.json +++ b/package.json @@ -20,15 +20,13 @@ "@stripe/stripe-js": "2.2.1", "@tailwindcss/forms": "0.5.7", "@tailwindcss/typography": "^0.5.4", - "@types/node": "20.4.7", - "@types/react": "18.2.18", - "@types/react-dom": "18.2.7", - "@types/webpack-env": "^1.18.1", "autoprefixer": "^10.4.12", "cheerio": "^1.0.0-rc.12", "clsx": "^1.2.1", + "dayjs": "^1.11.10", "fast-glob": "^3.2.11", "feed": "^4.2.2", + "ical": "^0.8.0", "next": "13.4.16", "next-themes": "^0.2.1", "postmark": "4.0.2", @@ -41,9 +39,14 @@ "typescript": "5.1.6" }, "devDependencies": { + "@types/ical": "^0.8.3", + "@types/node": "20.4.7", + "@types/react": "18.2.18", + "@types/react-dom": "18.2.7", + "@types/webpack-env": "^1.18.1", "eslint": "8.45.0", "eslint-config-next": "13.4.16", "prettier": "^3.0.1", "prettier-plugin-tailwindcss": "^0.5.2" } -} \ No newline at end of file +} diff --git a/src/app/calendar/calendar.tsx b/src/app/calendar/calendar.tsx new file mode 100644 index 0000000..9cc3dd7 --- /dev/null +++ b/src/app/calendar/calendar.tsx @@ -0,0 +1,241 @@ +'use client' + +import { + ChevronLeftIcon, + ChevronRightIcon, + ClockIcon, +} from '@heroicons/react/20/solid' + +import dayjs from "dayjs"; +import React from 'react'; +import { Calendar, Event } from './page'; +import Link from 'next/link'; + +interface day { + date: dayjs.Dayjs; + isCurrentMonth: boolean; + isToday: boolean; +} + + +function classNames(...classes: string[]): string { + return classes.filter(Boolean).join(' '); +} + +const getEvents = (cal: Calendar, d: dayjs.Dayjs): Event[] => { + const year = cal[d.year()]; + if (!year) { + return []; + } + + const month = year[d.month()]; + if (!month) { + return []; + } + + return month[d.date()] || []; +} + +export const CalendarComponent: React.FC<{ calendar: Calendar }> = ({ calendar }) => { + const [selectedDay, setSelectedDay] = React.useState(dayjs().startOf('day')); + const [selectedMonth, setSelectedMonth] = React.useState(dayjs().startOf('month')); + + const days = React.useMemo(() => { + // Number of greyed days shown before the beginning of the current month. + const renderedDaysBeforeMonth = selectedMonth.day(); + const firstDay = selectedMonth.subtract(renderedDaysBeforeMonth, 'day'); + const month = selectedMonth.month(); + const today = dayjs().startOf('day'); + + let current = firstDay; + const days: day[] = []; + + // Fill out the first week with days from the previous month. + for (let i = 0; i < renderedDaysBeforeMonth; i++) { + days.push({ + date: current, + isCurrentMonth: false, + isToday: current.isSame(today), + }); + current = current.add(1, 'day'); + } + + // Add the days for the selected month. + while (current.month() === month) { + days.push({ + date: current, + isCurrentMonth: true, + isToday: current.isSame(today), + }); + current = current.add(1, 'day'); + } + + // Finish out the week with days from the following month. + while (current.day() > 0) { + days.push({ + date: current, + isCurrentMonth: false, + isToday: current.isSame(today), + }); + current = current.add(1, 'day'); + } + + return days; + }, [selectedMonth]); + + const isSelectedDay = (d: day): boolean => { + return d.date.isSame(selectedDay); + } + + const selectedDayEvents = React.useMemo(() => { + return getEvents(calendar, selectedDay); + }, [calendar, selectedDay]); + + // TODO: Dark mode + + return ( +
+
+

+ +

+
+
+ + + + +
+
+
+
+
+ {[['M', 'on'], ['T', 'ue'], ['W', 'ed'], ['T', 'hu'], ['F', 'ri'], ['S', 'at'], ['S', 'un']].map((d) => ( +
+ {d[0]}{d[1]} +
+ ))} +
+
+
+ {days.map((day) => { + const events = getEvents(calendar, day.date); + const isSelected = isSelectedDay(day); + + return ( + + ); + })} +
+
+
+
+

+ Events on +

+ {selectedDayEvents.length > 0 && ( +
+
    + {selectedDayEvents.map((event) => { + const start = dayjs(event.start); + return ( +
  1. +
    +

    {event.name}

    + +
    +
  2. + ); + })} +
+
+ )} + {selectedDayEvents.length === 0 && ( +

No events on this day

+ )} +
+
+

+ If you are interested in reserving the hall, please see the + hall rental page + . +

+
+
+ ) +} diff --git a/src/app/calendar/page.tsx b/src/app/calendar/page.tsx new file mode 100644 index 0000000..6897396 --- /dev/null +++ b/src/app/calendar/page.tsx @@ -0,0 +1,119 @@ +import * as ical from "ical"; +import dayjs from "dayjs"; +import React from 'react'; +import { CalendarComponent } from "./calendar"; +import { Container } from "@/components/Container"; + +const icalAddr = process.env.CALENDAR_ADDR || ''; + +export type Calendar = Record; +type MonthEvents = Record; +type DayEvents = Record; + +let calendar: Calendar; +let upcomingEvents: Event[] = []; +let calendarLastRefresh: dayjs.Dayjs; + +export async function getCalendar(): Promise { + const now = dayjs(); + if (!calendar || !calendarLastRefresh || now.diff(calendarLastRefresh, 'hours') >= 1) { + await loadCalendar(); + calendarLastRefresh = now; + } + + return calendar; +} + +export async function getUpcomingEvents(): Promise { + await getCalendar(); + return upcomingEvents; +} + +/** Event must be a "plain object" so that it can be passed to the Calendar client component. */ +export interface Event { + id: string; + name: string; + allDay: boolean; + start: Date; + end: Date; +} + +async function loadCalendar(): Promise { + if (icalAddr === '') { + return; + } + + const icalContents = await (await fetch(icalAddr)).text(); + const events = ical.parseICS(icalContents); + const thisMonth = dayjs().startOf('month'); + const yesterday = dayjs().startOf('day').subtract(1, 'day'); + const upcoming: Event[] = [] + + const cal: Calendar = {}; + for (const id in events) { + if (!events.hasOwnProperty(id)) { + continue; + } + + const event = events[id]; + const start = dayjs(event.start); + const end = dayjs(event.end); + const allDay = start.hour() === 0 && start.minute() === 0 && end.diff(start, 'hours') === 24; + const privateEvent = event.description?.toLowerCase().includes("private"); + + const converted = { + id: event.uid || "", + name: privateEvent ? "Private event" : (event.summary || ""), + allDay: allDay, + start: start.toDate(), + end: end.toDate(), + } + + // Don't spend any more time on events that aren't going to be displayed. + if (start.isBefore(thisMonth)) { + continue; + } + + if (upcoming.length < 4 && start.isAfter(yesterday) && !privateEvent) { + upcoming.push(converted); + } + + const year = start.year(); + const month = start.month(); + const day = start.date(); + + if (!(year in cal)) { + cal[year] = {}; + } + const yearMap = cal[year]; + + if (!(month in yearMap)) { + yearMap[month] = {}; + } + const monthMap = yearMap[month]; + + if (day in monthMap) { + monthMap[day].push(converted); + } else { + monthMap[day] = [converted]; + } + } + + calendar = cal; + upcomingEvents = upcoming; +} + +export default async function Calendar() { + const calendar = await getCalendar(); + + return ( + +
+

+ Hall Calendar +

+
+ +
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 9da6560..15df316 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,7 +7,8 @@ import { CalendarDaysIcon } from '@heroicons/react/24/solid' import exteriorFrontImage from '@/images/photos/exterior-front.png' import { type BlogPostWithSlug, getAllBlogPosts } from '@/lib/articles' import { formatDate } from '@/lib/formatDate' -import { promises as fs } from 'fs'; +import { getUpcomingEvents, Event } from './calendar/page' +import dayjs from 'dayjs' function LinkButton({ @@ -42,42 +43,50 @@ function Article({ article }: { article: BlogPostWithSlug }) { ) } -interface Meeting { - title: string - date: string - startTime: string - endTime?: string - notes?: string -} +function EventListItem({ event }: { event: Event }) { + const start = dayjs(event.start); + const end = dayjs(event.end); + + const date = start.format('YYYY-MM-DD'); -function MeetingListItem({ meeting }: { meeting: Meeting }) { return (
  • Title
    - {meeting.title} + {event.name}
    Date
    - {meeting.date} + {date}
    Time
    - {meeting.endTime + {event.allDay ?
    - {' '} - {' '} - {' '} -
    - :
    - {' '} + All day
    + : (event.end + ? ( +
    + {' '} + {' '} + {' '} +
    + ) : ( +
    + {' '} +
    + ) + ) }
  • @@ -85,35 +94,7 @@ function MeetingListItem({ meeting }: { meeting: Meeting }) { } async function Events() { - const now = new Date(); - const nowYear = now.getFullYear(); - const nowMonth = now.getMonth() + 1; - const nowDay = now.getDate(); - - const file = await fs.readFile(process.cwd() + '/src/app/upcoming-events.json', 'utf8'); - const allEvents: Array = JSON.parse(file); - - // Remove any events in the past. - const events = allEvents.filter((e) => { - const [year, month, day] = e.date.split('-'); - - const parsedYear = parseInt(year) - if (parsedYear > nowYear) { - return true - } else if (parsedYear < nowYear) { - return false - } - - const parsedMonth = parseInt(month) - if (parsedMonth > nowMonth) { - return true - } else if (parsedMonth < nowMonth) { - return false - } - - const parsedDay = parseInt(day) - return parsedDay >= nowDay - }); + const events = await getUpcomingEvents(); return (
    @@ -122,8 +103,8 @@ async function Events() { Upcoming Events
      - {events.map((meeting, idx) => ( - + {events.map((event, idx) => ( + ))}
    {/* diff --git a/src/app/rental/page.tsx b/src/app/rental/page.tsx index 7e841b4..a862c77 100644 --- a/src/app/rental/page.tsx +++ b/src/app/rental/page.tsx @@ -80,6 +80,12 @@ export default function Rental() { damage to the Hall or cleaning is necessary, deductions will be made at the discretion of the Board.

    +

    + Please check the + calendar + for availability. +

    diff --git a/src/app/upcoming-events.json b/src/app/upcoming-events.json deleted file mode 100644 index 7e76cc0..0000000 --- a/src/app/upcoming-events.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "title": "February Potluck", - "date": "2024-02-17", - "startTime": "6:00pm", - "notes": "With historic West Sound slide show." - }, - { - "title": "Town Hall Meeting", - "date": "2024-03-06", - "startTime": "5:00pm", - "endTime": "7:00pm" - }, - { - "title": "April Potluck", - "date": "2024-04-20", - "startTime": "6:00pm" - }, - { - "title": "May Potluck", - "date": "2024-05-18", - "startTime": "6:00pm" - } -] \ No newline at end of file diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index cec0fd7..b17aa9b 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -29,6 +29,7 @@ export function Footer() {
    History News + Calendar Rental Club
    diff --git a/src/components/Header.tsx b/src/components/Header.tsx index dfce6ef..75497a9 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -105,6 +105,7 @@ function MobileNavigation( Home Hall History News + Calendar Rental Club @@ -152,6 +153,7 @@ function DesktopNavigation(props: React.ComponentPropsWithoutRef<'nav'>) { Home History News + Calendar Rental Club