From 2d7ce00caa8a50afb0b37038e54408c60234091d Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Sun, 24 Jul 2022 16:36:16 +0300 Subject: [PATCH] build(docs-infra): introduce new process for generating data for the events page (#45588) This commit introduces a new process for generating data for the AIO [events page](https://angular.io/events), which streamlines the process and minimizes duplication and manual work. For more details, see `aio/scripts/generate-events/README.md`. PR Close #45588 --- .github/workflows/update-events.yml | 42 + aio/BUILD.bazel | 1 - aio/content/marketing/events-contributing.md | 19 + aio/content/marketing/events.json | 1220 +++++++++++++---- aio/database.rules.json | 11 + aio/firebase.json | 3 + aio/package.json | 1 - aio/scripts/deploy-to-firebase/index.mjs | 3 +- aio/scripts/generate-events/README.md | 125 ++ .../apps-script-extension/appsscript.json | 13 + .../apps-script-extension/constants.js | 16 + .../apps-script-extension/persister.js | 107 ++ .../apps-script-extension/property.js | 49 + .../apps-script-extension/triggers.js | 26 + aio/scripts/generate-events/index.mjs | 83 ++ .../events/events.component.html | 83 +- .../events/events.component.spec.ts | 192 +-- .../events/events.component.ts | 87 +- .../custom-elements/events/events.module.ts | 2 +- .../custom-elements/events/events.service.ts | 6 +- aio/src/test.ts | 7 - aio/yarn.lock | 5 - 22 files changed, 1561 insertions(+), 540 deletions(-) create mode 100644 .github/workflows/update-events.yml create mode 100644 aio/content/marketing/events-contributing.md create mode 100644 aio/database.rules.json create mode 100644 aio/scripts/generate-events/README.md create mode 100644 aio/scripts/generate-events/apps-script-extension/appsscript.json create mode 100644 aio/scripts/generate-events/apps-script-extension/constants.js create mode 100644 aio/scripts/generate-events/apps-script-extension/persister.js create mode 100644 aio/scripts/generate-events/apps-script-extension/property.js create mode 100644 aio/scripts/generate-events/apps-script-extension/triggers.js create mode 100644 aio/scripts/generate-events/index.mjs diff --git a/.github/workflows/update-events.yml b/.github/workflows/update-events.yml new file mode 100644 index 0000000000000..d9a081a01ff4b --- /dev/null +++ b/.github/workflows/update-events.yml @@ -0,0 +1,42 @@ +name: Update AIO events + +on: + workflow_dispatch: + inputs: {} + schedule: + # Run every day at 15:00. + - cron: 0 15 * * * + +# Declare default permissions as read only. +permissions: + contents: read + +jobs: + update_events: + name: Update `events.json` (if necessary) + if: github.repository == 'angular/angular' + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2 + with: + # Setting `persist-credentials: false` prevents the github-action account from being the + # account that is attempted to be used for authentication, instead the remote is set to + # an authenticated URL. + persist-credentials: false + - name: Install AIO dependencies + run: yarn --cwd=aio install + - name: Generate `events.json` + run: node aio/scripts/generate-events/index.mjs --ignore-invalid-dates + - name: Create a PR (if necessary) + uses: gkalpak/dev-infra/github-actions/create-pr-for-changes@88c198ae1a3462223cb5c1e83338e4b94b435283 + with: + branch-prefix: docs-update-events + pr-title: 'docs: update events' + pr-description: | + Generated `events.json` with the latest events retrieved from the Firebase DB. + pr-labels: | + action: review + comp: docs + target: patch + angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }} diff --git a/aio/BUILD.bazel b/aio/BUILD.bazel index e2ff4a5ad54f4..810b62e04ecb7 100644 --- a/aio/BUILD.bazel +++ b/aio/BUILD.bazel @@ -90,7 +90,6 @@ TEST_DEPS = APPLICATION_DEPS + [ "@aio_npm//karma-jasmine", "@aio_npm//karma-jasmine-html-reporter", "@aio_npm//puppeteer", - "@aio_npm//timezone-mock", ] architect( diff --git a/aio/content/marketing/events-contributing.md b/aio/content/marketing/events-contributing.md new file mode 100644 index 0000000000000..285911b02226b --- /dev/null +++ b/aio/content/marketing/events-contributing.md @@ -0,0 +1,19 @@ +# Contributing to `events.json` + + +## About this list + +We maintain a list of events (conferences, meetups, etc.) where our team has or will be presenting. +This data is stored in `events.json`. + + +## How to get an event listed + +The `events.json` file is periodically generated from data stored in a Firebase database. +See [here](https://github.com/angular/angular/blob/main/aio/scripts/generate-events/README.md) for more details. + +If you want to get an event listed, please get in touch with the Angular DevRel team at [devrel@angular.io](mailto:devrel@angular.io). + +> WARNING: +> The `events.json` file is only intended to be updated via a [script](https://github.com/angular/angular/blob/main/aio/scripts/generate-events/index.mjs). +> Do not manually edit the file, because your changes will be overwritten the next time the script runs. diff --git a/aio/content/marketing/events.json b/aio/content/marketing/events.json index 6ecc6bfabc7d6..5beab3f32420b 100644 --- a/aio/content/marketing/events.json +++ b/aio/content/marketing/events.json @@ -1,337 +1,1099 @@ [ - { - "name": "NG Poland", - "location": "Warsaw, Poland", - "linkUrl": "https://ng-poland.pl/", + { + "date": { + "start": "2019-01-09" + }, + "name": "ngAtl" + }, + { + "date": { + "start": "2019-02-23" + }, + "name": "ngIndia" + }, + { + "date": { + "start": "2019-03-07" + }, + "name": "MVPMix" + }, + { + "date": { + "start": "2019-04-05" + }, + "name": "JSFest" + }, + { + "date": { + "start": "2019-04-09" + }, + "name": "Google Cloud Next" + }, + { + "date": { + "start": "2019-04-13" + }, + "name": "IT-KONEKT" + }, + { + "date": { + "start": "2019-05-01" + }, + "name": "ng-conf" + }, + { + "date": { + "start": "2019-05-07" + }, + "name": "Google I/O" + }, + { + "date": { + "start": "2019-05-16" + }, + "name": "FullStack" + }, + { + "date": { + "start": "2019-05-26" + }, + "name": "ng-vikings" + }, + { + "date": { + "start": "2019-06-06" + }, + "name": "WeRDevs" + }, + { + "date": { + "start": "2019-06-07" + }, + "name": "JSNation" + }, + { + "date": { + "start": "2019-06-11" + }, + "name": "AngularMix @ DevInt" + }, + { + "date": { + "start": "2019-06-12" + }, + "name": "AngularUp" + }, + { + "date": { + "start": "2019-06-19" + }, + "name": "Developer Week NYC" + }, + { + "date": { + "start": "2019-07-06" + }, + "name": "ng-MY" + }, + { + "date": { + "start": "2019-07-13" + }, + "name": "ng-Japan" + }, + { + "date": { + "start": "2019-08-01" + }, + "name": "Angular Denver" + }, + { + "date": { + "start": "2019-08-31" + }, + "name": "buildersconf" + }, + { + "date": { + "start": "2019-09-19" + }, + "name": "Angular Connect" + }, + { + "date": { + "start": "2019-08-11" + }, + "name": "JSConf US" + }, + { + "date": { + "start": "2019-10-07" + }, + "name": "ngRome" + }, + { + "date": { + "start": "2019-10-16" + }, + "name": "Connect Tech" + }, + { + "date": { + "start": "2019-10-21" + }, + "name": "NWA Tech Summit" + }, + { + "date": { + "start": "2019-10-27" + }, + "name": "GDE Summit" + }, + { + "date": { + "start": "2019-11-04" + }, + "name": "Devoxx BE" + }, + { + "date": { + "start": "2019-11-11" + }, + "name": "Chrome Dev Summit" + }, + { + "date": { + "start": "2019-11-14" + }, + "name": "ISTA" + }, + { + "date": { + "start": "2019-11-18" + }, + "name": "DevIntersections Vegas" + }, + { + "date": { + "start": "2019-11-21" + }, + "name": "ngPoland" + }, + { + "date": { + "start": "2019-11-22" + }, + "name": "jsPoland" + }, + { + "date": { + "start": "2019-11-23" + }, + "name": "ngChina" + }, + { + "date": { + "start": "2019-11-23" + }, + "name": "jsTalks" + }, + { + "date": { + "start": "2019-11-26" + }, + "name": "FrontEnd Con" + }, + { + "date": { + "start": "2019-12-05" + }, + "name": "ng-BE" + }, + { + "date": { + "start": "2020-02-23" + }, + "name": "ngIndia" + }, + { + "date": { + "start": "2020-04-01" + }, + "name": "ng-conf" + }, + { + "date": { + "start": "2020-04-07" + }, + "name": "Angular Toronto" + }, + { + "date": { + "start": "2020-04-09" + }, + "name": "Angular Sydney" + }, + { + "date": { + "start": "2020-04-15" + }, + "name": "SofiaJS" + }, + { + "date": { + "start": "2020-05-07" + }, + "name": "JS Vidcon" + }, + { + "date": { + "start": "2020-05-07" + }, + "name": "GDG Indore" + }, + { + "date": { + "start": "2020-05-14" + }, + "name": "ngMorocco" + }, + { + "date": { + "start": "2020-05-15" + }, + "name": "Web Day" + }, + { + "date": { + "start": "2020-05-20" + }, + "name": "Warsaw meetup" + }, + { + "date": { + "start": "2020-05-25" + }, + "name": "ngVikings" + }, + { + "date": { + "start": "2020-06-02" + }, + "name": "JNation" + }, + { + "date": { + "start": "2020-06-10" + }, + "name": "Angular Uruguay" + }, + { + "date": { + "start": "2020-06-15" + }, + "name": "ngTurkey" + }, + { + "date": { + "start": "2020-06-17" + }, + "name": "Angular San Diego" + }, + { + "date": { + "start": "2020-06-18" + }, + "name": "JSNation" + }, + { + "date": { + "start": "2020-06-19" + }, + "name": "GDG Reading" + }, + { + "date": { + "start": "2020-06-22" + }, + "name": "Angular Israel" + }, + { + "date": { + "start": "2020-06-24" + }, + "name": "ngHeidelberg" + }, + { + "date": { + "start": "2020-06-25" + }, + "name": "Angular Seattle (Reactive)" + }, + { + "date": { + "start": "2020-06-25" + }, + "name": "Angular Belarus" + }, + { + "date": { + "start": "2020-06-26" + }, + "name": "Kulkul Workshop" + }, + { + "date": { + "start": "2020-07-01" + }, + "name": "Angular Rome" + }, + { + "date": { + "start": "2020-07-08" + }, + "name": "Angular Birmingham" + }, + { + "date": { + "start": "2020-07-08" + }, + "name": "Angular-IL" + }, + { + "date": { + "start": "2020-07-09" + }, + "name": "Angular Bogota" + }, + { + "date": { + "start": "2020-07-10" + }, + "name": "web.dev Live India" + }, + { + "date": { + "start": "2020-07-13" + }, + "name": "Angular Kansas City" + }, + { + "date": { + "start": "2020-07-14" + }, + "name": "Angular MTV online" + }, + { + "date": { + "start": "2020-07-21" + }, + "name": "DevBG" + }, + { + "date": { + "start": "2020-07-21" + }, + "name": "Reactive Forms Barcelona" + }, + { + "date": { + "start": "2020-07-22" + }, + "linkUrl": "https://kommunity.com/ngturkey/events/build-like-google-with-stephen-fluin-angular-turkey-0e685d3b", + "name": "ngTurkey" + }, + { + "date": { + "start": "2020-07-24" + }, + "name": "Angular NL" + }, + { + "date": { + "start": "2020-07-25" + }, + "linkUrl": "https://ng-girls.org/kc-2020/", + "name": "ngGirls" + }, + { + "date": { + "start": "2020-07-29" + }, + "linkUrl": "https://techcommunity.microsoft.com/t5/azure-developer-community-blog/create-frontend-a-one-of-a-kind-live-event-from-microsoft-about/ba-p/1524677", + "name": "Create: Frontend" + }, + { + "date": { + "start": "2020-08-03" + }, + "name": "JavaScript Israel" + }, + { + "date": { + "start": "2020-08-04" + }, + "name": "Angular MTV" + }, + { + "date": { + "start": "2020-08-11" + }, + "name": "Angular Kharkiv" + }, + { + "date": { + "start": "2020-08-12" + }, + "name": "ngCopenhagen" + }, + { + "date": { + "start": "2020-08-14" + }, + "name": "NgConf Colombia" + }, + { + "date": { + "start": "2020-08-18" + }, + "name": "Angular Arizona" + }, + { + "date": { + "start": "2020-08-18" + }, + "name": "ng-be August session" + }, + { + "date": { + "start": "2020-08-20" + }, + "name": "Angular London" + }, + { + "date": { + "start": "2020-09-04" + }, + "name": "ThisDot State of Angular" + }, + { + "date": { + "start": "2020-09-16" + }, + "name": "Angular Warsaw" + }, + { + "date": { + "start": "2020-09-19" + }, + "name": "JavaScript LA" + }, + { + "date": { + "start": "2020-09-24" + }, + "name": "Dutch Angular" + }, + { + "date": { + "start": "2020-09-29" + }, + "linkUrl": "http://infoshare.pl/", + "name": "Infoshare.pl" + }, + { + "date": { + "start": "2020-10-01" + }, + "name": "GIDS.WEB Live 2020" + }, + { + "date": { + "start": "2020-10-01" + }, + "name": "Angular Online This Dot Labs" + }, + { + "date": { + "start": "2020-10-07" + }, + "name": "Astea Conference" + }, + { + "date": { + "start": "2020-10-09" + }, + "name": "TSConf" + }, + { + "date": { + "start": "2020-10-15" + }, + "name": "GDG DevFest Norway" + }, + { + "date": { + "start": "2020-10-16" + }, + "linkUrl": "https://devfestturkey.com/registration", + "name": "GDG Devfest Turkey (Anatolia)" + }, + { + "date": { + "start": "2020-10-17" + }, + "linkUrl": "https://gdg.community.dev/events/details/google-gdg-london-presents-devfest-uk-ireland-2020-with-google-developer-groups/", + "name": "GDG Devfest UK + Ireland" + }, + { "date": { - "start": "2022-10-25", - "end": "2022-10-25" + "start": "2020-10-18" }, - "workshopsDate": { - "start": "2022-10-24", - "end": "2022-10-24" - } + "linkUrl": "https://www.meetup.com/GCDCSaudi/events/273405502/", + "name": "GDG Devfest Saudi" }, { - "name": "JS Poland", - "location": "Warsaw, Poland", - "linkUrl": "https://js-poland.pl/", "date": { - "start": "2022-10-26", - "end": "2022-10-26" + "start": "2020-10-20" }, - "workshopsDate": { - "start": "2022-10-24", - "end": "2022-10-24" - } + "name": "ngrome" }, { - "name": "NG-DE", - "location": "Berlin, Germany", - "linkUrl": "https://ng-de.org/", "date": { - "start": "2022-10-06", - "end": "2022-10-07" + "start": "2020-10-20" }, - "workshopsDate": { - "start": "2022-10-05", - "end": "2022-10-05" - } + "linkUrl": "https://www.meetup.com/angular-heidelberg/events/273768194/", + "name": "ngHeidelberg" }, { - "name": "ng-conf", - "location": " Salt Lake City, Utah or Online", - "linkUrl": "https://2022.ng-conf.org/", "date": { - "start": "2022-08-31", - "end": "2022-09-02" + "start": "2020-10-24" }, - "workshopsDate": { - "start": "2022-08-29", - "end": "2022-08-30" - } + "name": "Angular Honduras" }, { - "name": "ngIndia", - "location": "Delhi, India", - "linkUrl": "https://www.ng-ind.com/", "date": { - "start": "2022-05-21", - "end": "2022-05-21" - } + "start": "2020-10-27" + }, + "name": "Firebase Summit" + }, + { + "date": { + "start": "2020-10-11" + }, + "linkUrl": "http://dev-con.ro/", + "name": "Dev-Con.ro" + }, + { + "date": { + "start": "2020-11-12" + }, + "linkUrl": "https://www.meetup.com/Dutch-Angular-group/events/", + "name": "Dutch Angular" + }, + { + "date": { + "start": "2020-11-20" + }, + "name": "ngChina" + }, + { + "date": { + "start": "2020-11-24" + }, + "name": "Allianz Developer Conference" + }, + { + "date": { + "start": "2021-01-21" + }, + "name": "NWC JS" + }, + { + "date": { + "start": "2021-01-26" + }, + "name": "Angular Utah" + }, + { + "date": { + "start": "2021-01-28" + }, + "name": "Discord Q&A" + }, + { + "date": { + "start": "2021-02-04" + }, + "name": "Contributor Days" + }, + { + "date": { + "start": "2021-02-09" + }, + "name": "Angular Nation" + }, + { + "date": { + "start": "2021-03-05" + }, + "name": "ng-conf Honduras" + }, + { + "date": { + "start": "2021-03-08" + }, + "name": "IWD/Techknow" + }, + { + "date": { + "start": "2021-03-16" + }, + "name": "SAP user group" + }, + { + "date": { + "start": "2021-03-30" + }, + "name": "College OSS Consortium" + }, + { + "date": { + "start": "2021-04-01" + }, + "name": "State of Angular" + }, + { + "date": { + "start": "2021-04-05" + }, + "name": "Adventures in Angular‌" + }, + { + "date": { + "start": "2021-04-21" + }, + "name": "ng-conf" + }, + { + "date": { + "start": "2021-05-18" + }, + "name": "Google I/O" + }, + { + "date": { + "start": "2021-05-23" + }, + "name": "dev.pro JSConf" + }, + { + "date": { + "start": "2021-05-25" + }, + "name": "Daily Dev" + }, + { + "date": { + "start": "2021-06-01" + }, + "name": "Angular Singapore" + }, + { + "date": { + "start": "2021-06-03" + }, + "name": "OpenJS World 2021" + }, + { + "date": { + "start": "2021-06-03" + }, + "name": "State of Angular EcoSystem" + }, + { + "date": { + "start": "2021-06-06" + }, + "name": "JSNation" + }, + { + "date": { + "start": "2021-06-25" + }, + "name": "I/O GDG North Lebanon" + }, + { + "date": { + "start": "2021-06-29" + }, + "name": "Future of Testing: Mobile" + }, + { + "date": { + "start": "2021-06-30" + }, + "name": "AngularRDU" + }, + { + "date": { + "start": "2021-07-09" + }, + "name": "ngRome" + }, + { + "date": { + "start": "2021-07-13" + }, + "name": "AWT: Angular KC" + }, + { + "date": { + "start": "2021-07-15" + }, + "name": "JUG BG" + }, + { + "date": { + "start": "2021-07-15" + }, + "name": "WebRush" + }, + { + "date": { + "start": "2021-07-19" + }, + "name": "AWT: Angular MTV" + }, + { + "date": { + "start": "2021-07-26" + }, + "name": "AWT: ATL Angular" + }, + { + "date": { + "start": "2021-08-17" + }, + "name": "AWT: Angular SF" + }, + { + "date": { + "start": "2021-08-20" + }, + "name": "Syntax 2021" + }, + { + "date": { + "start": "2021-08-30" + }, + "name": "Twitter Spaces: Web Performance & UX" + }, + { + "date": { + "start": "2021-08-26" + }, + "name": "AWT: Angular Nation" + }, + { + "date": { + "start": "2021-09-16" + }, + "name": "Web Rush" + }, + { + "date": { + "start": "2021-09-23" + }, + "name": "AWT: Angular Seattle" + }, + { + "date": { + "start": "2021-09-28" + }, + "name": "iJS" + }, + { + "date": { + "start": "2021-09-29" + }, + "name": "AWT: Angular Athens" + }, + { + "date": { + "start": "2021-10-13" + }, + "name": "AWT: GDG Memphis" + }, + { + "date": { + "start": "2021-10-14" + }, + "name": "State of Angular" + }, + { + "date": { + "start": "2021-10-26" + }, + "name": "DevReach" + }, + { + "date": { + "start": "2021-10-20" + }, + "name": "The Angular Podcast" + }, + { + "date": { + "start": "2021-10-20" + }, + "name": "Angular Copenhagen" + }, + { + "date": { + "start": "2021-11-10" + }, + "name": "Angular Sussex" + }, + { + "date": { + "start": "2021-11-12" + }, + "linkUrl": "http://angularday.it/", + "name": "AngularDay.it" + }, + { + "date": { + "start": "2021-11-17" + }, + "name": "Angular Cairo" + }, + { + "date": { + "start": "2021-12-02" + }, + "name": "EnterpriseNG" + }, + { + "date": { + "start": "2021-12-03" + }, + "name": "EnterpriseNG" + }, + { + "date": { + "start": "2021-12-01" + }, + "name": "Angular Tokyo" + }, + { + "date": { + "start": "2021-12-08" + }, + "name": "Angular Belgrade" + }, + { + "date": { + "start": "2021-12-15" + }, + "name": "Angular Mega Meetup (Athens, Dutch, UK)" + }, + { + "date": { + "start": "2021-12-10" + }, + "name": "Midwest DevFest" + }, + { + "date": { + "start": "2022-01-18" + }, + "name": "AWT: Europe & Africa" + }, + { + "date": { + "start": "2022-01-21" + }, + "name": "NGPoland" + }, + { + "date": { + "start": "2022-01-25" + }, + "name": "Angular World Tour: Kenya" + }, + { + "date": { + "start": "2022-01-28" + }, + "name": "ng-Keralam" }, { - "name": "Angular Global Summit", - "location": "Online", - "linkUrl": "https://link.geekle.us/angular/offsite", "date": { - "start": "2022-03-29", - "end": "2022-03-30" - } + "start": "2022-02-22" + }, + "name": "NG Yerevan" }, { - "name": "Angular Global Summit", - "location": "Online", - "linkUrl": "https://link.geekle.us/angular/offsite", "date": { - "start": "2021-06-01", - "end": "2021-06-02" - } + "start": "2022-02-22" + }, + "name": "The State of JS Survey Stream" }, { - "name": "ng-conf", - "location": "Online", - "linkUrl": "https://www.2021.ng-conf.org/", "date": { - "start": "2021-04-22", - "end": "2021-04-23" + "start": "2022-03-01" }, - "workshopsDate": { - "start": "2021-04-12", - "end": "2021-04-15" - } + "name": "State of Angular" }, { - "name": "ng-china", - "location": "Online", - "linkUrl": "https://ng-china.org/", "date": { - "start": "2020-11-21", - "end": "2020-11-22" - } + "start": "2022-03-22" + }, + "name": "Angular Pakistan" }, { - "name": "EnterpriseNG", - "location": "Online", - "linkUrl": "https://www.ng-conf.org/", "date": { - "start": "2020-11-19", - "end": "2020-11-20" - } + "start": "2022-03-23" + }, + "name": "Munich Meetup" }, { - "name": "ngrome", - "location": "Online", - "linkUrl": "https://ngrome.io/", "date": { - "start": "2020-10-20", - "end": "2020-10-20" - } + "start": "2022-03-30" + }, + "name": "Hubs Web Conference" }, { - "name": "DevReach", - "location": "Online", - "linkUrl": "https://www.telerik.com/devreach/online/agenda-thursday#sessions", "date": { - "start": "2020-10-19", - "end": "2020-10-23" - } + "start": "2022-04-26" + }, + "name": "AWT: Isreal" }, { - "name": "ngVikings", - "location": "Oslo, Norway", - "linkUrl": "https://ngvikings.org/", "date": { - "start": "2020-05-25", - "end": "2020-05-26" + "start": "2022-05-11" }, - "workshopsDate": { - "start": "2020-05-27", - "end": "2020-05-27" - } + "name": "Google I/O" }, { - "name": "ng-conf", - "location": "Salt Lake City, Utah", - "linkUrl": "https://ng-conf.org/", "date": { - "start": "2020-04-01", - "end": "2020-04-03" - } + "start": "2022-05-18" + }, + "name": "Angular Belgrade" }, { - "name": "ngIndia", - "location": "Delhi, India", - "linkUrl": "https://www.ng-ind.com/", "date": { - "start": "2020-02-29", - "end": "2020-02-29" - } + "start": "2022-05-25" + }, + "name": "Ioniconf" }, { - "name": "ReactiveConf", - "location": "Prague, Czech Republic", - "linkUrl": "https://reactiveconf.com/", "date": { - "start": "2019-10-30", - "end": "2019-11-01" - } + "start": "2022-05-25" + }, + "name": "Angular Athens x VueJS Athens" }, { - "name": "NG Rome MMXIX", - "location": "Rome, Italy", - "linkUrl": "https://ngrome.io", - "tooltip": "NG Rome MMXIX - The Italian Angular Conference", "date": { - "start": "2019-10-07", - "end": "2019-10-07" + "start": "2022-05-26" }, - "workshopsDate": { - "start": "2019-10-06", - "end": "2019-10-06" - } + "name": "Google I/O Extended: GDG NYC" }, { - "name": "AngularConnect", - "location": "London, UK", - "linkUrl": "https://www.angularconnect.com/?utm_source=angular.io&utm_medium=referral", "date": { - "start": "2019-09-19", - "end": "2019-09-20" - } + "start": "2022-05-27" + }, + "name": "CityJS Greece" }, { - "name": "NG-DE", - "location": "Berlin, Germany", - "linkUrl": "https://ng-de.org/", "date": { - "start": "2019-08-30", - "end": "2019-08-31" + "start": "2022-05-18" }, - "workshopsDate": { - "start": "2019-08-29", - "end": "2019-08-29" - } + "name": "Angular World Tour: Hungary" }, { - "name": "ng-japan", - "location": "Tokyo, Japan", - "linkUrl": "https://ngjapan.org/", "date": { - "start": "2019-07-13", - "end": "2019-07-13" - } + "start": "2022-06-03" + }, + "name": "RenderATL" }, { - "name": "ngVikings", - "location": "Copenhagen, Denmark", - "linkUrl": "https://ngvikings.org/", "date": { - "start": "2019-05-27", - "end": "2019-05-28" + "start": "2022-06-16" }, - "workshopsDate": { - "start": "2019-05-26", - "end": "2019-05-26" - } + "name": "OpenSource@Google" }, { - "name": "ng-conf", - "location": "Salt Lake City, Utah", - "linkUrl": "https://ng-conf.org/", "date": { - "start": "2019-05-01", - "end": "2019-05-03" - } + "start": "2022-08-31" + }, + "name": "ng-conf" }, { - "name": "ng-India", - "location": "Gurgaon, India", - "linkUrl": "https://www.ng-ind.com/", "date": { - "start": "2019-02-23", - "end": "2019-02-23" - } + "start": "2022-09-12" + }, + "name": "DevReach" }, { - "name": "ngAtlanta", - "location": "Atlanta, Georgia", - "linkUrl": "https://ng-atl.org/", "date": { - "start": "2019-01-09", - "end": "2019-01-12" - } + "start": "2022-10-06" + }, + "name": "NG-DE" }, { - "name": "AngularConnect", - "location": "London, United Kingdom", - "linkUrl": "https://past.angularconnect.com/2018", "date": { - "start": "2018-11-05", - "end": "2018-11-07" - } + "start": "2022-10-06" + }, + "name": "Nordic.js" }, { - "name": "ReactiveConf", - "location": "Prague, Czech Republic", - "linkUrl": "https://reactiveconf.com/", "date": { - "start": "2018-10-29", - "end": "2018-10-31" - } + "start": "2022-10-07" + }, + "name": "angularday" }, { - "name": "AngularMix", - "location": "Orlando, Florida", - "linkUrl": "https://angularmix.com/", "date": { - "start": "2018-10-10", - "end": "2018-10-12" - } + "start": "2022-10-18" + }, + "name": "Codemotion Milan" }, { - "name": "Angular Conf Australia", - "location": "Melbourne, Australia", - "linkUrl": "https://www.angularconf.com.au/", "date": { - "start": "2018-06-22", - "end": "2018-06-22" - } + "start": "2022-10-25" + }, + "name": "NG-Poland" }, { - "name": "ng-japan", - "location": "Tokyo, Japan", - "linkUrl": "https://ngjapan.org/en.html", "date": { - "start": "2018-06-16", - "end": "2018-06-16" - } + "start": "2022-10-26" + }, + "name": "JS Poland" }, { - "name": "WeAreDevelopers", - "location": "Vienna, Austria", - "linkUrl": "https://www.wearedevelopers.com/", - "tooltip": "WeAreDevs", "date": { - "start": "2018-05-16", - "end": "2018-05-18" - } + "start": "2022-10-27" + }, + "name": "Porto Tech Hub" }, { - "name": "ng-conf", - "location": "Salt Lake City, Utah", - "linkUrl": "https://ng-conf.org/", "date": { - "start": "2018-04-18", - "end": "2018-04-20" - } + "start": "2022-11-10" + }, + "name": "JavaScript Day (JetBrains)" }, { - "name": "ngVikings", - "location": "Helsinki, Finland", - "linkUrl": "https://ngvikings.org/", "date": { - "start": "2018-03-01", - "end": "2018-03-02" - } + "start": "2022-12-02" + }, + "name": "ngRome" }, { - "name": "ngAtlanta", - "location": "Atlanta, Georgia", - "linkUrl": "https://ng-atl.org/", "date": { - "start": "2018-01-30", - "end": "2018-01-30" - } + "start": "2022-12-06" + }, + "name": "Angular Contributor Days" } -] +] \ No newline at end of file diff --git a/aio/database.rules.json b/aio/database.rules.json new file mode 100644 index 0000000000000..f0a294a1bebb9 --- /dev/null +++ b/aio/database.rules.json @@ -0,0 +1,11 @@ +{ + "rules": { + ".read": false, + ".write": false, + + "events": { + ".read": true, + ".write": false + } + } +} diff --git a/aio/firebase.json b/aio/firebase.json index 89a056181eef4..34e903709db8e 100644 --- a/aio/firebase.json +++ b/aio/firebase.json @@ -253,5 +253,8 @@ ] } ] + }, + "database": { + "rules": "database.rules.json" } } diff --git a/aio/package.json b/aio/package.json index 7ef2c6a8b9277..3521efcf4b1a9 100644 --- a/aio/package.json +++ b/aio/package.json @@ -173,7 +173,6 @@ "semver": "^7.3.5", "shelljs": "^0.8.5", "stemmer": "^2.0.0", - "timezone-mock": "^1.1.3", "tree-kill": "^1.1.0", "ts-node": "^10.8.1", "tsec": "^0.2.2", diff --git a/aio/scripts/deploy-to-firebase/index.mjs b/aio/scripts/deploy-to-firebase/index.mjs index 3481066fc7592..3cca4e17b9c50 100644 --- a/aio/scripts/deploy-to-firebase/index.mjs +++ b/aio/scripts/deploy-to-firebase/index.mjs @@ -383,7 +383,8 @@ function deploy(data) { firebase(`use "${projectId}"`); firebase('target:clear hosting aio'); firebase(`target:apply hosting aio "${siteId}"`); - firebase(`deploy --only hosting:aio --message "Commit: ${currentCommit}" --non-interactive`); + firebase( + `deploy --only database,hosting:aio --message "Commit: ${currentCommit}" --non-interactive`); u.logSectionHeader('Run post-deploy actions.'); postDeployActions.forEach(fn => fn(data)); diff --git a/aio/scripts/generate-events/README.md b/aio/scripts/generate-events/README.md new file mode 100644 index 0000000000000..64c2fbe8dbf76 --- /dev/null +++ b/aio/scripts/generate-events/README.md @@ -0,0 +1,125 @@ +# Generating data for `angular.io/events` + +This document and the contents of this directory contain information and source code related to generating data for the [angular.io Events page](https://angular.io/events). + + +## Directory contents + +The following list gives a brief description of the contents of this directory and their purpose. +For more details see the following sections. + +- `apps-script-extension/`: + The source code for the Apps Script extension that needs to be added to the Google Sheet spreadsheet. +- `index.mjs`: + The script for retrieving the data from the Firebase database and generating the `events.json` file for angular.io. + + +## Background + +The "Events" page on angular.io has two sections: One for upcoming events and one for past events. + +Originally, the events were hard-coded into the page's HTML, which meant that the page had to be updated twice for each event (once to add it to the list of upcoming events and once more to move it to the list of past events). + +Later, the setup was changed so that the events were loaded as JSON and passed to an [EventsComponent](../../src/app/custom-elements/events/), which was able to categorize them as "upcoming" or "past" based on the date. +This reduced the maintenance overhead by only requiring one update per event (just to add it to the `events.json` file that was part of the angular.io source code). + +However, since the DevRel team had to maintain a separate list of events outside angular.io (in a more suitable format for their needs), that setup still required unnecessary work and resulted in having to manually duplicate the data in two places. Additionally, due to the extra overhead of updating the events list on angular.io (creating a pull request, getting it approved, merged and finally deployed), the events page was often out of date. + +This document describes the latest, revised process for generating the events data with the aim of: +- Minimizing the manual overhead. +- Avoiding data duplication. +- Ensuring the freshness of the data on angular.io. +- Minimizing changes to the current DevRel team workflow. + + +## The current process + +This section describes the current setup and process for generating events data for angular.io. + + +### Overview + +In a nutshell, the setup can be summarized as follows: + +1. The DevRel team keeps information about events in a Google Sheets spreadsheet (in the appropriate format). +2. An Apps Script extension on the spreadsheet periodically saves the relevant information (such as event names and dates) in a Firebase Realtime Database. +3. There is a script (that can be run periodically) which can query the database and generate `events.json` based on the latest data. + + +#### Apps Script extension overview + +In a nutshell, the Apps Script extension works as follows: + +1. An [`onEdit` trigger](https://developers.google.com/apps-script/guides/triggers#onedite) is invoked every time the spreadsheet is edited and checks whether a team allocation sheet was edited. + If so, it adds the name of the sheet to a list of edited sheets. +2. A [time-driven trigger](https://developers.google.com/apps-script/guides/triggers/installable#time-driven_triggers) is invoked periodically and checks to see if there are any sheets that have been edited since the last invocation. + If so, it extracts the event data from each edited sheet and updates the Firebase database. + +Useful resource: https://stackoverflow.com/questions/53207906/how-to-integrate-firebase-into-google-apps-script-without-using-deprecated-dat#answer-53211786 + + +### How to set up + +1. Have a [Google Sheets](https://www.google.com/sheets/about/) spreadsheet for keeping event information. + The spreadsheet must follow some format requirements in order for the script to be able to extract event information. + Look at the source code in [apps-script-extension/](./apps-script-extension/) for details, but the main requirements are: + - There should be a sheet named `XXXX Team Allocation` for each year, where `XXXX` is the year (for example, `2022 Team Allocation`). + - Each team allocation sheet should have the event dates on the first row (potentially after some empty cells) and the dates should be displayed in the format `M/D` (for example, `4/11` for April 11th). + - Each team allocation sheet should have the event names on the second row (each under the corresponding date cell). + Event names can optionally be links pointing to the event's web page. + +2. Create an [Apps Script extension](https://developers.google.com/apps-script/guides/sheets) for the aforementioned spreadsheet with the source code from the [apps-script-extension/](./apps-script-extension/) directory. + To do this, open the spreadsheet, click on `Extensions > Apps Script`, create the necessary files as seen in the source code (with the difference that the `.js` extension must be replaced with `.gs`) and copy the source code. + For `appsscript.json`, follow the instructions [here](https://developers.google.com/apps-script/concepts/manifests#editing_a_manifest) to make it appear in the in-browser editor. + +3. Set up a [time-driven trigger](https://developers.google.com/apps-script/guides/triggers/installable#time-driven_triggers) to run the `updateEventsOnFirebase()` function (found in [persister.gs](./apps-script-extension/persister.js)). + Adjust the frequency according to your needs. + +4. Have a [Firebase project](https://firebase.google.com/) with [Realtime Database](https://firebase.google.com/products/realtime-database) enabled. + +5. Follow the instructions [here](https://firebase.google.com/docs/rules/manage-deploy#generate_a_configuration_file) to set up [security rules](https://firebase.google.com/docs/rules) and make sure they are deployed to the Firebase project as needed. + You can see the database security rules used for this project in [database.rules.json](../../database.rules.json). + These rules will allow anyone to read the events from the database, but only someone with access to the Firebase project will be able to update the events in the database. + NOTE: For this project, the rules are deployed as part of the `deploy_aio` CI job. + +6. Ensure that the account that was used to create the Google Sheets trigger on step 3 also has access to the Firebase project (otherwise, the trigger will fail to update the database when events change in the spreadsheet). + +7. Wire the [index.mjs](./index.mjs) script to run when necessary to generate an updated `events.json` file. + NOTE: For this project, there is a [GitHub Action](https://github.com/angular/angular/blob/main/.github/workflows/update-events.yml) that periodically runs the script and creates a pull request (if necessary). + +8. Ensure that both [constants.gs](./apps-script-extension/constants.js) and [index.mjs](./index.mjs) point to the correct database URL. + + +### How to update + +Although the source code in [apps-script-extension/](./apps-script-extension/) and the actual code used in the spreadsheet are independent, it is advised for versioning purposes to keep the two in sync. +Whenever a change is needed to be made to the Apps Script extension, the change should be applied in both places. + + +### Trade-offs/Alternatives considered + +This section describes trade-offs made and alternative implementations/variations that were considered. + +**Trade-offs:** + +- The current implementation provides minimal data for each event (name, start date and optionally link to web site). + Specifically, compared to the previous implementation, data that was dropped includes: end dates, workshop dates (when applicable), mandatory link to web site and optional tooltips (to be shown on hover). + This decision was made in order to be able to keep the existing workflow/data layout of the DevRel spreadsheet and may be revisited in the future. + +**Alternatives considered:** + +- We considered using [Cloud Firestore](https://firebase.google.com/products/firestore), which is Firebase's newer database offering, but communicating with it [via REST](https://firebase.google.com/docs/firestore/reference/rest) seemed more involved than with the [Realtime Database](https://firebase.google.com/docs/reference/rest/database). + +- We considered updating the database on each edit, since this would avoid the need for a time-driven trigger. + This proved to be problematic for the following reasons: + - It is very inefficient to do without debouncing (as there could be multiple consecutive updates in a short time). + - Debouncing doesn't seem to be possible in Apps Script extensions without using [installable triggers](https://developers.google.com/apps-script/guides/triggers/installable). + See below on why `installable triggers` proved problematic as well. + +- We considered reducing the setup requirements by programmatically adding an [installable trigger](https://developers.google.com/apps-script/guides/triggers/installable) to update the database shortly after an edit. + This proved problematic, because it would require all users with write access to the spreadsheet to also: + - Grant authorization for the [firebase.database](https://www.googleapis.com/auth/firebase.database), [script.external_request](https://www.googleapis.com/auth/script.external_request) and [script.scriptapp](https://www.googleapis.com/auth/script.scriptapp) OAuth scopes to the extension. + - Have access to the Firebase project. + + We decided it would be more "ergonomic" to have someone (for example, the DevRel lead) with access to the Firebase project manually create a time-driven installable trigger. + This entails an extra step, but is a one-time action, so the overhead is minimal. diff --git a/aio/scripts/generate-events/apps-script-extension/appsscript.json b/aio/scripts/generate-events/apps-script-extension/appsscript.json new file mode 100644 index 0000000000000..7f0dce26025db --- /dev/null +++ b/aio/scripts/generate-events/apps-script-extension/appsscript.json @@ -0,0 +1,13 @@ +{ + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "oauthScopes": [ + "https://www.googleapis.com/auth/firebase.database", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/spreadsheets.currentonly", + "https://www.googleapis.com/auth/userinfo.email" + ], + "runtimeVersion": "V8", + "timeZone": "UTC" +} diff --git a/aio/scripts/generate-events/apps-script-extension/constants.js b/aio/scripts/generate-events/apps-script-extension/constants.js new file mode 100644 index 0000000000000..eda0685c38c9e --- /dev/null +++ b/aio/scripts/generate-events/apps-script-extension/constants.js @@ -0,0 +1,16 @@ +/** + * The base URL for the database. + * + * Used to persist data from the spreadsheet. + */ +// README: Keep in sync with `../index.mjs`. +const DB_BASE_URL = 'https://angular-io.firebaseio.com'; + +/** + * The regex to match the name of sheets that contain team allocation data, which needs to be stored + * in Firebase. + * + * All "Team Allocation" sheets must have names that match this pattern in order for them to be + * taken into account. + */ +const TEAM_ALLOCATION_SHEET_NAME_RE = /^(\d\d\d\d) team allocation$/i; diff --git a/aio/scripts/generate-events/apps-script-extension/persister.js b/aio/scripts/generate-events/apps-script-extension/persister.js new file mode 100644 index 0000000000000..d2907280559e1 --- /dev/null +++ b/aio/scripts/generate-events/apps-script-extension/persister.js @@ -0,0 +1,107 @@ +/** + * Helper function to be invoked by a manually set-up, time-based trigger in order to update the + * data on Firebase. + */ +function updateEventsOnFirebase() { + const persister = new Persister(); + persister.updateDb(); +} + +/** + * Helper class to encapsulate logic about persisting events changes in the spreadsheet to the + * database (currently Firebase Realtime Database). + */ +class Persister { + /** + * Update the database with the latest data from edited sheets. + * + * If no sheets have been edited, this is a no-op. + */ + updateDb() { + this._log('Updating database...'); + + // Get the data for each edited sheet. + const data = this._getDataForEditedSheets(); + + // If no sheets have been edited, there is nothing to do. + if (data.length === 0) { + this._log('No sheets edited. Exiting...'); + return; + } + + // Update the database. + const partialEvents = data.reduce((acc, dataForSheet) => { + acc[dataForSheet.year] = dataForSheet.events; + return acc; + }, {}); + + const res = UrlFetchApp.fetch(`${DB_BASE_URL}/events.json?print=silent`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`, + 'Content-type': 'application/json', + }, + payload: JSON.stringify(partialEvents), + }); + + this._log(`Database updated: ${res.getResponseCode()} - ${res.getContentText()}`); + } + + _getDataForEditedSheets() { + // Get edited sheets that need processing. + const editedSheets = Property.editedSheets.get() || []; + this._log(`Edited sheets (${editedSheets.length}): ${editedSheets.join(', ') || '-'}`); + + // Delete the corresponding property, indicating that no processing is pending. + Property.editedSheets.delete(); + + // Get the events data for edited sheets. + const ss = SpreadsheetApp.getActiveSpreadsheet(); + return editedSheets.map(name => this._getDataForSheet(ss.getSheetByName(name))); + } + + _getDataForSheet(sheet) { + const year = TEAM_ALLOCATION_SHEET_NAME_RE.exec(sheet.getName())[1]; + + // Get event dates per cell index. + // Dates display value is expected to be in the format `M/D`. For example: `5/15` + const startDatesRange = sheet.getRange('1:1'); + const startDates = startDatesRange. + getDisplayValues()[0]. + map((x, i) => ({ + index: i, + value: x, + })). + filter(x => x.value !== ''). + map(x => ({ + index: x.index, + date: `${year}-${x.value.split('/').map(p => p.padStart(2, '0')).join('-')}`, + })); + + // Get other event info (name, url) per cell index and merge with dates. + const namesRange = sheet.getRange('2:2'); + const events = namesRange. + getRichTextValues()[0]. + map((x, i) => ({ + index: i, + value: { + text: x.getText(), + url: x.getLinkUrl() || undefined, + }, + })). + filter(x => !/^(?:total)?$/i.test(x.value.text)). + map(x => ({ + name: x.value.text, + linkUrl: x.value.url, + date: { + start: (startDates.find(y => y.index === x.index) || {}).date, + }, + })); + + return {year, events}; + } + + _log(msg) { + Logger.log(`[Persister] ${msg}`); + } +} diff --git a/aio/scripts/generate-events/apps-script-extension/property.js b/aio/scripts/generate-events/apps-script-extension/property.js new file mode 100644 index 0000000000000..5061ef5a135ee --- /dev/null +++ b/aio/scripts/generate-events/apps-script-extension/property.js @@ -0,0 +1,49 @@ +/** + * Helper class to interact with project-wide properties. + */ +class Property { + /** + * Create a new `Property` instance for the specified key. + * + * @param {string} key - The key of the property. + */ + constructor(key) { + this._key = `property:${key}`; + } + + /** + * Delete this property. + */ + delete() { + Property._PROPS.deleteProperty(this._key); + } + + /** + * Get the current value of this property. + * + * @return {unknown} - The current value of this property or `null` if this property does not + * exist. + */ + get() { + const storedValue = Property._PROPS.getProperty(this._key); + return storedValue && JSON.parse(storedValue); + } + + /** + * Set the value of this property. + * Any existing value will be replaced. + * + * @param {unknown} newValue - The new value to set. + */ + set(newValue) { + if (newValue == null) { + this.delete(); + } else { + Property._PROPS.setProperty(this._key, JSON.stringify(newValue)); + } + } +} + +Property.editedSheets = new Property('editedSheets'); + +Property._PROPS = PropertiesService.getScriptProperties(); diff --git a/aio/scripts/generate-events/apps-script-extension/triggers.js b/aio/scripts/generate-events/apps-script-extension/triggers.js new file mode 100644 index 0000000000000..0f7dead040fac --- /dev/null +++ b/aio/scripts/generate-events/apps-script-extension/triggers.js @@ -0,0 +1,26 @@ +/** + * Hook to handle the event of editing a cell in the spreadsheet and thus potentially modifying the + * events data. + * + * @see https://developers.google.com/apps-script/guides/triggers#onedite + * + * @param {object} evt - An event object that contains information about the context that caused the + * trigger to fire. + */ +function onEdit(evt) { + // Check whether one of the "Team Allocation" sheets was edited. + const editedSheetName = evt.range.getSheet().getName(); + + if (!TEAM_ALLOCATION_SHEET_NAME_RE.test(editedSheetName)) { + return; + } + + // Add the sheet to the list of edited sheets (if not already there). + const editedSheets = Property.editedSheets.get() || []; + + if (!editedSheets.includes(editedSheetName)) { + editedSheets.push(editedSheetName); + editedSheets.sort(); + Property.editedSheets.set(editedSheets); + } +} diff --git a/aio/scripts/generate-events/index.mjs b/aio/scripts/generate-events/index.mjs new file mode 100644 index 0000000000000..0f05c724c3579 --- /dev/null +++ b/aio/scripts/generate-events/index.mjs @@ -0,0 +1,83 @@ +// Imports +import {writeFileSync} from 'node:fs'; +import {get} from 'node:https'; +import {dirname, resolve as resolvePath} from 'node:path'; +import {argv} from 'node:process'; +import {fileURLToPath} from 'node:url'; + + +// Constants +const __dirname = dirname(fileURLToPath(import.meta.url)); +// README: Keep in sync with `./apps-script-project/constants.js`. +const DB_BASE_URL = 'https://angular-io.firebaseio.com'; +const EVENTS_FILE_PATH = resolvePath(__dirname, '../../content/marketing/events.json'); + +// Run +_main(argv.slice(2)).catch(err => { + console.error(err); + process.exit(1); +}); + +// Helpers +async function _main(args) { + console.log(`\nGenerating events list from '${DB_BASE_URL}'.`); + + // Read arguments. + const ignoreInvalidDates = args.includes('--ignore-invalid-dates'); + + // Fetch events. + const data = await fetchData(`${DB_BASE_URL}/events.json`); + let events = [].concat(...Object.values(data ?? {})); + + // Validate event dates. + const eventsWithInvalidDates = events.filter(eventHasInvalidDate); + + if (eventsWithInvalidDates.length > 0) { + console.warn( + `The following ${eventsWithInvalidDates.length} event(s) have invalid dates:` + + eventsWithInvalidDates.map(evt => `\n - ${JSON.stringify(evt)}`).join('')); + + if (ignoreInvalidDates) { + console.warn('Events with invalid dates will be ignored.'); + + const ignoredEvents = new Set(eventsWithInvalidDates); + events = events.filter(evt => !ignoredEvents.has(evt)); + } else { + console.error('Failed to generate events list.'); + process.exit(1); + } + } + + // Write events to file. + writeFileSync(EVENTS_FILE_PATH, JSON.stringify(events, null, 2)); + + console.log(`Successfully generated events list in '${EVENTS_FILE_PATH}'.\n`); +} + +function fetchData(url) { + return new Promise((resolve, reject) => { + get(url, response => { + let responseText = ''; + + response + .on('data', d => responseText += d) + .on('end', () => resolve(JSON.parse(responseText))) + .on('error', err => reject(err)); + }).on('error', err => reject(err)); + }); +} + +function eventHasInvalidDate(event) { + return !event.date || !event.date.start || isInvalidDate(event.date.start); +} + +function isInRange(num, min, max) { + return (min <= num) && (num <= max); +} + +function isInvalidDate(date) { + return !/^\d{4}-\d{2}-\d{2}$/.test(date) || + !isInRange(Number(date.slice(0, 4)), 2000, 2100) || + !isInRange(Number(date.slice(5, 7)), 1, 12) || + !isInRange(Number(date.slice(8, 10)), 1, 31); +} diff --git a/aio/src/app/custom-elements/events/events.component.html b/aio/src/app/custom-elements/events/events.component.html index b64b1a3c7e530..52c21248c28a5 100644 --- a/aio/src/app/custom-elements/events/events.component.html +++ b/aio/src/app/custom-elements/events/events.component.html @@ -1,52 +1,41 @@

Where we'll be presenting:

-
-

We don't have any upcoming speaking engagements at the moment.

-

Until something comes up, make sure you check our YouTube channel - and follow us on social media.

-

If you want us to be part of your event reach out on devrel@angular.io!

-
- - - - - - - - - - - - - - - - -
EventLocationDate
{{event.name}}{{event.location}} -
- {{getEventDates(event)}} -
-
+ +
+

We don't have any upcoming speaking engagements at the moment.

+

+ Until something comes up, make sure you check our YouTube channel + and follow us on social media. +

+

+ If you want us to be part of your event reach out on devrel@angular.io! +

+
+ + + +

Where we already presented:

- - - - - - - - - - - - - - - -
EventLocationDate
{{event.name}}{{event.location}} -
- {{getEventDates(event)}} -
-
+ + + + + + + + + + + + + + + + +
EventStart date
+ {{ event.name }} + {{ event.name }} + {{ event.date.start }}
+
diff --git a/aio/src/app/custom-elements/events/events.component.spec.ts b/aio/src/app/custom-elements/events/events.component.spec.ts index ebc73c377e4d4..b48662c6714d2 100644 --- a/aio/src/app/custom-elements/events/events.component.spec.ts +++ b/aio/src/app/custom-elements/events/events.component.spec.ts @@ -1,7 +1,6 @@ import { Injector } from '@angular/core'; import { Subject } from 'rxjs'; -import * as tzMock from 'timezone-mock'; -import { Duration, Event, EventsComponent } from './events.component'; +import { AngularEvent, EventsComponent } from './events.component'; import { EventsService } from './events.service'; describe('EventsComponent', () => { @@ -40,27 +39,12 @@ describe('EventsComponent', () => { it('should separate past and upcoming events', () => { eventsService.events.next([ - createMockEvent( - 'Upcoming event 1', - {start: '2020-06-16', end: '2020-06-17'}, - {start: '2020-06-18', end: '2020-06-18'}), - createMockEvent( - 'Upcoming event 3', - {start: '2222-01-01', end: '2222-01-02'}), - createMockEvent( - 'Past event 2', - {start: '2020-06-13', end: '2020-06-14'}), - createMockEvent( - 'Upcoming event 2', - {start: '2020-06-17', end: '2020-06-18'}, - {start: '2020-06-16', end: '2020-06-16'}), - createMockEvent( - 'Past event 1', - {start: '2020-05-30', end: '2020-05-31'}), - createMockEvent( - 'Past event 3', - {start: '2020-06-14', end: '2020-06-14'}, - {start: '2020-06-16', end: '2020-06-17'}), + createMockEvent('Upcoming event 1', {start: '2020-06-16'}), + createMockEvent('Upcoming event 3', {start: '2222-01-01'}), + createMockEvent('Past event 2', {start: '2020-06-13'}), + createMockEvent('Upcoming event 2', {start: '2020-06-17'}), + createMockEvent('Past event 1', {start: '2020-05-30'}), + createMockEvent('Past event 3', {start: '2020-06-14'}), ]); expect(component.pastEvents.map(evt => evt.name)).toEqual(jasmine.arrayWithExactContents( @@ -70,46 +54,24 @@ describe('EventsComponent', () => { ['Upcoming event 1', 'Upcoming event 2', 'Upcoming event 3'])); }); - it('should order past events in reverse chronological order (ignoring workshops dates)', () => { + it('should order past events in reverse chronological order', () => { eventsService.events.next([ - createMockEvent( - 'Past event 2', - {start: '1999-12-13', end: '1999-12-14'}, - {start: '1999-12-11', end: '1999-12-11'}), - createMockEvent( - 'Past event 4', - {start: '2020-01-16', end: '2020-01-17'}, - {start: '2020-01-14', end: '2020-01-15'}), - createMockEvent( - 'Past event 3', - {start: '2020-01-15', end: '2020-01-16'}, - {start: '2020-01-17', end: '2020-01-18'}), - createMockEvent( - 'Past event 1', - {start: '1999-12-12', end: '1999-12-15'}), + createMockEvent('Past event 2', {start: '1999-12-13'}), + createMockEvent('Past event 4', {start: '2020-01-16'}), + createMockEvent('Past event 3', {start: '2020-01-15'}), + createMockEvent('Past event 1', {start: '1999-12-12'}), ]); expect(component.pastEvents.map(evt => evt.name)).toEqual( ['Past event 4', 'Past event 3', 'Past event 2', 'Past event 1']); }); - it('should order upcoming events in chronological order (ignoring workshops dates)', () => { + it('should order upcoming events in chronological order', () => { eventsService.events.next([ - createMockEvent( - 'Upcoming event 2', - {start: '2020-12-13', end: '2020-12-14'}, - {start: '2020-12-11', end: '2020-12-11'}), - createMockEvent( - 'Upcoming event 4', - {start: '2021-01-16', end: '2021-01-17'}, - {start: '2021-01-14', end: '2021-01-15'}), - createMockEvent( - 'Upcoming event 3', - {start: '2021-01-15', end: '2021-01-16'}, - {start: '2021-01-17', end: '2021-01-18'}), - createMockEvent( - 'Upcoming event 1', - {start: '2020-12-12', end: '2020-12-15'}), + createMockEvent('Upcoming event 2', {start: '2020-12-13'}), + createMockEvent('Upcoming event 4', {start: '2021-01-16'}), + createMockEvent('Upcoming event 3', {start: '2021-01-15'}), + createMockEvent('Upcoming event 1', {start: '2020-12-12'}), ]); expect(component.upcomingEvents.map(evt => evt.name)).toEqual( @@ -118,137 +80,25 @@ describe('EventsComponent', () => { it('should treat ongoing events as upcoming', () => { eventsService.events.next([ - createMockEvent( - 'Ongoing event 1', - {start: '2020-06-14', end: '2020-06-16'}), - createMockEvent( - 'Ongoing event 2', - {start: '2020-06-14', end: '2020-06-15'}, - {start: '2020-06-13', end: '2020-06-13'}), + createMockEvent('Ongoing event 1', {start: '2020-06-15'}), ]); expect(component.pastEvents).toEqual([]); expect(component.upcomingEvents.map(evt => evt.name)).toEqual(jasmine.arrayWithExactContents( - ['Ongoing event 1', 'Ongoing event 2'])); + ['Ongoing event 1'])); }); }); - describe('getEventDates()', () => { - // Test on different timezones to ensure that event dates are processed correctly regardless of - // the user's local time. - const timezones: tzMock.TimeZone[] = [ - 'Australia/Adelaide', // UTC+9.5/10.5 - 'Brazil/East', // UTC-3 - 'UTC', // UTC - ]; - - for (const tz of timezones) { - describe(`on timezone ${tz}`, () => { - // NOTE: `timezone-mock` does not work correctly if used together with Jasmine's mock clock. - beforeEach(() => tzMock.register(tz)); - afterEach(() => tzMock.unregister()); - - describe('(without workshops)', () => { - it('should correctly format the main event date', () => { - const testEvent = createMockEvent('Test', {start: '2020-06-20', end: '2020-06-20'}); - expect(component.getEventDates(testEvent)).toBe('June 20, 2020'); - }); - - it('should correctly format the main event date spanning mupliple days', () => { - const testEvent = createMockEvent('Test', {start: '2019-09-19', end: '2019-09-21'}); - expect(component.getEventDates(testEvent)).toBe('September 19-21, 2019'); - }); - - it('should correctly format the main event date spanning mupliple months', () => { - const testEvent = createMockEvent('Test', {start: '2019-10-30', end: '2019-11-01'}); - expect(component.getEventDates(testEvent)).toBe('October 30 - November 1, 2019'); - }); - - it('should correctly format event dates at the beginning/end of the year', () => { - const testEvent = createMockEvent('Test', {start: '2021-01-01', end: '2021-12-31'}); - expect(component.getEventDates(testEvent)).toBe('January 1 - December 31, 2021'); - }); - }); - - describe('(with workshops)', () => { - it('should correctly format event dates with workshops after main event', () => { - const testEvent = createMockEvent( - 'Test', - {start: '2020-07-25', end: '2020-07-26'}, - {start: '2020-07-27', end: '2020-07-27'}); - - expect(component.getEventDates(testEvent)) - .toBe('July 25-26 (conference), July 27 (workshops), 2020'); - }); - - it('should correctly format event dates with workshops before main event', () => { - const testEvent = createMockEvent( - 'Test', - {start: '2019-10-07', end: '2019-10-07'}, - {start: '2019-10-06', end: '2019-10-06'}); - - expect(component.getEventDates(testEvent)) - .toBe('October 6 (workshops), October 7 (conference), 2019'); - }); - - it('should correctly format event dates spanning multiple days', () => { - const testEvent = createMockEvent( - 'Test', - {start: '2019-08-30', end: '2019-08-31'}, - {start: '2019-08-28', end: '2019-08-29'}); - - expect(component.getEventDates(testEvent)) - .toBe('August 28-29 (workshops), August 30-31 (conference), 2019'); - }); - - it('should correctly format event dates with workshops on different month before the main event', - () => { - const testEvent = createMockEvent( - 'Test', - {start: '2020-08-01', end: '2020-08-02'}, - {start: '2020-07-30', end: '2020-07-31'}); - - expect(component.getEventDates(testEvent)) - .toBe('July 30-31 (workshops), August 1-2 (conference), 2020'); - }); - - it('should correctly format event dates with workshops on different month after the main event', - () => { - const testEvent = createMockEvent( - 'Test', - {start: '2020-07-30', end: '2020-07-31'}, - {start: '2020-08-01', end: '2020-08-02'}); - - expect(component.getEventDates(testEvent)) - .toBe('July 30-31 (conference), August 1-2 (workshops), 2020'); - }); - - it('should correctly format event dates spanning multiple months', () => { - const testEvent = createMockEvent( - 'Test', - {start: '2020-07-31', end: '2020-08-01'}, - {start: '2020-07-30', end: '2020-08-01'}); - - expect(component.getEventDates(testEvent)) - .toBe('July 30 - August 1 (workshops), July 31 - August 1 (conference), 2020'); - }); - }); - }); - } - }); - // Helpers class TestEventsService { - events = new Subject(); + events = new Subject(); } - function createMockEvent(name: string, date: Duration, workshopsDate?: Duration): Event { + function createMockEvent(name: string, date: AngularEvent['date']): AngularEvent { return { name, - location: '', linkUrl: '', date, - workshopsDate, }; } }); diff --git a/aio/src/app/custom-elements/events/events.component.ts b/aio/src/app/custom-elements/events/events.component.ts index f3343ee9e1639..f777c7b722f93 100644 --- a/aio/src/app/custom-elements/events/events.component.ts +++ b/aio/src/app/custom-elements/events/events.component.ts @@ -3,34 +3,13 @@ import { Component, OnInit } from '@angular/core'; import { EventsService } from './events.service'; const DAY = 24 * 60 * 60 * 1000; -const MONTHS = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', -]; -export type date = string; // of the format `YYYY-MM-DD`. -export interface Duration { - start: date; - end: date; -} - -export interface Event { +export interface AngularEvent { name: string; - location: string; - linkUrl: string; - tooltip?: string; - date: Duration; - workshopsDate?: Duration; + linkUrl?: string; + date: { + start: `${number}-${number}-${number}`; // Date string in the format: `YYYY-MM-DD` + }; } @Component({ @@ -39,64 +18,24 @@ export interface Event { }) export class EventsComponent implements OnInit { - pastEvents: Event[]; - upcomingEvents: Event[]; + pastEvents: AngularEvent[]; + upcomingEvents: AngularEvent[]; constructor(private eventsService: EventsService) { } ngOnInit() { this.eventsService.events.subscribe(events => { this.pastEvents = events - .filter(event => new Date(event.date.end).getTime() < Date.now() - DAY) - .sort((l: Event, r: Event) => isBefore(l.date, r.date) ? 1 : -1); + .filter(event => isInThePast(event)) + .sort((l: AngularEvent, r: AngularEvent) => (l.date.start < r.date.start) ? 1 : -1); this.upcomingEvents = events - .filter(event => new Date(event.date.end).getTime() >= Date.now() - DAY) - .sort((l: Event, r: Event) => isBefore(l.date, r.date) ? -1 : 1); + .filter(event => !isInThePast(event)) + .sort((l: AngularEvent, r: AngularEvent) => (l.date.start < r.date.start) ? -1 : 1); }); } - - getEventDates(event: Event) { - let dateString; - - // Check if there is a workshop - if (event.workshopsDate) { - const mainEventDateString = `${processDate(event.date)} (conference)`; - const workshopsDateString = `${processDate(event.workshopsDate)} (workshops)`; - const areWorkshopsBeforeEvent = isBefore(event.workshopsDate, event.date); - - dateString = areWorkshopsBeforeEvent ? - `${workshopsDateString}, ${mainEventDateString}` : - `${mainEventDateString}, ${workshopsDateString}`; - } else { - // If no work shop date create conference date string - dateString = processDate(event.date); - } - dateString = `${dateString}, ${new Date(event.date.end).getUTCFullYear()}`; - return dateString; - } -} - -function processDate(dates: Duration) { - // Covert Date sting to date object for comparisons - const startDate = new Date(dates.start); - const endDate = new Date(dates.end); - - // Create a date string in the start like January 31 - let processedDate = `${MONTHS[startDate.getUTCMonth()]} ${startDate.getUTCDate()}`; - - // If they are in different months add the string '- February 2' Making the final string January 31 - February 2 - if (startDate.getUTCMonth() !== endDate.getUTCMonth()) { - processedDate = `${processedDate} - ${MONTHS[endDate.getUTCMonth()]} ${endDate.getUTCDate()}`; - } else if (startDate.getUTCDate() !== endDate.getUTCDate()) { - // If not add - date eg it will make // January 30-31 - processedDate = `${processedDate}-${endDate.getUTCDate()}`; - } - - return processedDate; } -function isBefore(duration1: Duration, duration2: Duration): boolean { - return (duration1.start < duration2.start) || - (duration1.start === duration2.start && duration1.end < duration2.end); +function isInThePast(event: AngularEvent): boolean { + return new Date(event.date.start).getTime() < Date.now() - DAY; } diff --git a/aio/src/app/custom-elements/events/events.module.ts b/aio/src/app/custom-elements/events/events.module.ts index c04c412569b3f..7da1e4cbeb752 100644 --- a/aio/src/app/custom-elements/events/events.module.ts +++ b/aio/src/app/custom-elements/events/events.module.ts @@ -7,7 +7,7 @@ import { WithCustomElementComponent } from '../element-registry'; @NgModule({ imports: [ CommonModule ], declarations: [ EventsComponent ], - providers: [ EventsService] + providers: [ EventsService ], }) export class EventsModule implements WithCustomElementComponent { customElementComponent: Type = EventsComponent; diff --git a/aio/src/app/custom-elements/events/events.service.ts b/aio/src/app/custom-elements/events/events.service.ts index c4d8ecd0e257d..11bb3a582a6fc 100644 --- a/aio/src/app/custom-elements/events/events.service.ts +++ b/aio/src/app/custom-elements/events/events.service.ts @@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http'; import { ConnectableObservable, Observable, of } from 'rxjs'; import { catchError, publishLast } from 'rxjs/operators'; -import { Event } from './events.component'; +import { AngularEvent } from './events.component'; import { CONTENT_URL_PREFIX } from 'app/documents/document.service'; import { Logger } from 'app/shared/logger.service'; @@ -12,7 +12,7 @@ const eventsPath = CONTENT_URL_PREFIX + 'events.json'; @Injectable() export class EventsService { - events: Observable; + events: Observable; constructor(private http: HttpClient, private logger: Logger) { this.events = this.getEvents(); @@ -26,7 +26,7 @@ export class EventsService { }), publishLast() ); - (events as ConnectableObservable).connect(); + (events as ConnectableObservable).connect(); return events; } } diff --git a/aio/src/test.ts b/aio/src/test.ts index 805174d5e8aa2..c04c876075f97 100644 --- a/aio/src/test.ts +++ b/aio/src/test.ts @@ -7,13 +7,6 @@ import { platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; -// Needed for `assert` polyfill uses `process`. -// See: https://github.com/browserify/commonjs-assert/blob/bba838e9ba9e28edf3127ce6974624208502f6bc/internal/assert/assertion_error.js#L138 -// The `assert` polyfill is needed because of `timezone-mock` which is a Node.JS library but in being used in Browser. -(globalThis as any).process = { - env: {}, -}; - declare const require: { context(path: string, deep?: boolean, filter?: RegExp): { (id: string): T; diff --git a/aio/yarn.lock b/aio/yarn.lock index 4c0291063d5a8..dd3d2fddf27cf 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -11621,11 +11621,6 @@ timers-ext@^0.1.7: es5-ext "~0.10.46" next-tick "1" -timezone-mock@^1.1.3: - version "1.3.4" - resolved "https://registry.yarnpkg.com/timezone-mock/-/timezone-mock-1.3.4.tgz#16cc40bfad10d461f8a41ed7396eeb6ea1e1b2b3" - integrity sha512-B0CGmOgMPVUZqp63eU/FGcDaL68JjHeiVnCF24K99Kj6AwCV15BHWMLCv8ZKSUq5oyVHTtg7p1ajOWfXB+0wnQ== - title-case@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa"