From cd8a2e3044b78da611498de805a9dd5a239bc2a7 Mon Sep 17 00:00:00 2001 From: Philip Johnson Date: Wed, 20 Nov 2024 11:50:31 -1000 Subject: [PATCH] Deploy website - based on 87061e8da009bf84440f1f6be51bbf769b369cdc --- 404.html | 4 +- assets/js/0058b4c6.03a51135.js | 1 + assets/js/0058b4c6.e179fb10.js | 1 - assets/js/10921c5b.0e4c824d.js | 1 + assets/js/10921c5b.4d956c8c.js | 1 - assets/js/32cdd552.4c0ae06e.js | 1 - assets/js/32cdd552.bf0ad874.js | 1 + assets/js/3ad77611.133c0910.js | 1 + assets/js/3ad77611.dafd6774.js | 1 - assets/js/7d1225b6.44565266.js | 1 + assets/js/7d1225b6.c945d809.js | 1 - assets/js/a1e7621f.aba6ed83.js | 1 - assets/js/a6327429.52a1365e.js | 1 + assets/js/a6327429.b29917b1.js | 1 - assets/js/a9a08fef.b569d67f.js | 1 + assets/js/bc1f8660.74e6e406.js | 1 - assets/js/ed0568ab.67eb3729.js | 1 + assets/js/ed0568ab.a5d4194d.js | 1 - assets/js/f3759001.73ba512d.js | 1 - assets/js/f3759001.fcc2bb17.js | 1 + assets/js/main.2143a0af.js | 2 - assets/js/main.fc7d4566.js | 2 + ...CENSE.txt => main.fc7d4566.js.LICENSE.txt} | 0 assets/js/runtime~main.5ac53f25.js | 1 - assets/js/runtime~main.910de26c.js | 1 + blog.html | 4 +- blog/2023/02/10/welcome.html | 4 +- blog/archive.html | 4 +- docs/business.html | 4 +- docs/business/market-size.html | 4 +- docs/business/milestones.html | 4 +- docs/business/roadmap.html | 4 +- docs/develop.html | 6 +- docs/develop/architecture.html | 10 +- docs/develop/backups.html | 8 +- docs/develop/coding-standards.html | 6 +- docs/develop/dart-analyze.html | 6 +- docs/develop/{design => }/data-model.html | 51 ++--- docs/develop/deployment.html | 6 +- docs/develop/design/badges.html | 8 +- docs/develop/design/data-model-old.html | 193 ------------------ docs/develop/design/data-mutation.html | 6 +- docs/develop/design/features.html | 10 +- docs/develop/design/input-fields.html | 10 +- docs/develop/design/with-widgets.html | 8 +- docs/develop/installation.html | 6 +- docs/develop/integrity-check.html | 6 +- docs/develop/managing-firebase-data.html | 8 +- docs/develop/onboarding.html | 6 +- .../release-0.0/chatgpt-feedback.html | 6 +- .../release-0.0/customer-feedback.html | 6 +- docs/develop/releases/release-0.0/design.html | 6 +- .../release-0.0/entrepreneur-feedback.html | 6 +- docs/develop/releases/release-1.0/cvp.html | 6 +- .../release-1.0/end-of-season-feedback.html | 6 +- docs/develop/releases/release-1.0/goals.html | 6 +- .../release-1.0/onboarding-feedback.html | 6 +- docs/develop/scripts.html | 6 +- docs/develop/testing.html | 6 +- docs/home/food-security.html | 4 +- docs/home/innovations.html | 4 +- docs/home/related-work.html | 4 +- docs/home/serious-gardeners.html | 4 +- docs/home/sneak-peek.html | 4 +- docs/home/team.html | 4 +- docs/home/welcome.html | 4 +- docs/user-guide/adding-plantings.html | 4 +- .../adding-vendors-crops-varieties.html | 4 +- docs/user-guide/badges.html | 4 +- docs/user-guide/chat-rooms.html | 4 +- docs/user-guide/define-a-garden.html | 4 +- docs/user-guide/downloading.html | 4 +- docs/user-guide/explore-a-chapter.html | 4 +- docs/user-guide/explore-a-garden.html | 4 +- docs/user-guide/geobot.html | 4 +- docs/user-guide/guided-tour.html | 4 +- docs/user-guide/observations.html | 4 +- docs/user-guide/outcomes.html | 4 +- docs/user-guide/overview.html | 4 +- docs/user-guide/privacy.html | 4 +- docs/user-guide/registration.html | 4 +- docs/user-guide/scenarios.html | 4 +- docs/user-guide/seeds.html | 4 +- docs/user-guide/tasks.html | 4 +- docs/user-guide/terms-and-conditions.html | 4 +- img/develop/data-entity-overview.png | Bin 0 -> 302689 bytes img/develop/data-model-dependencies.png | Bin 0 -> 509596 bytes index.html | 4 +- markdown-page.html | 4 +- sitemap.xml | 2 +- 90 files changed, 188 insertions(+), 397 deletions(-) create mode 100644 assets/js/0058b4c6.03a51135.js delete mode 100644 assets/js/0058b4c6.e179fb10.js create mode 100644 assets/js/10921c5b.0e4c824d.js delete mode 100644 assets/js/10921c5b.4d956c8c.js delete mode 100644 assets/js/32cdd552.4c0ae06e.js create mode 100644 assets/js/32cdd552.bf0ad874.js create mode 100644 assets/js/3ad77611.133c0910.js delete mode 100644 assets/js/3ad77611.dafd6774.js create mode 100644 assets/js/7d1225b6.44565266.js delete mode 100644 assets/js/7d1225b6.c945d809.js delete mode 100644 assets/js/a1e7621f.aba6ed83.js create mode 100644 assets/js/a6327429.52a1365e.js delete mode 100644 assets/js/a6327429.b29917b1.js create mode 100644 assets/js/a9a08fef.b569d67f.js delete mode 100644 assets/js/bc1f8660.74e6e406.js create mode 100644 assets/js/ed0568ab.67eb3729.js delete mode 100644 assets/js/ed0568ab.a5d4194d.js delete mode 100644 assets/js/f3759001.73ba512d.js create mode 100644 assets/js/f3759001.fcc2bb17.js delete mode 100644 assets/js/main.2143a0af.js create mode 100644 assets/js/main.fc7d4566.js rename assets/js/{main.2143a0af.js.LICENSE.txt => main.fc7d4566.js.LICENSE.txt} (100%) delete mode 100644 assets/js/runtime~main.5ac53f25.js create mode 100644 assets/js/runtime~main.910de26c.js rename docs/develop/{design => }/data-model.html (90%) delete mode 100644 docs/develop/design/data-model-old.html create mode 100644 img/develop/data-entity-overview.png create mode 100644 img/develop/data-model-dependencies.png diff --git a/404.html b/404.html index f83a0bd6e..c7fef2631 100644 --- a/404.html +++ b/404.html @@ -5,8 +5,8 @@ Page Not Found | Geo Garden Club - - + +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

diff --git a/assets/js/0058b4c6.03a51135.js b/assets/js/0058b4c6.03a51135.js new file mode 100644 index 000000000..bf051a0e4 --- /dev/null +++ b/assets/js/0058b4c6.03a51135.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4088],{6462:e=>{e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"homeSidebar":[{"type":"category","collapsed":false,"label":"About GeoGardenClub","items":[{"type":"link","label":"Welcome","href":"/docs/home/welcome","docId":"home/welcome","unlisted":false},{"type":"link","label":"\\"Serious\\" Gardeners","href":"/docs/home/serious-gardeners","docId":"home/serious-gardeners","unlisted":false},{"type":"link","label":"Design Innovations","href":"/docs/home/innovations","docId":"home/innovations","unlisted":false},{"type":"link","label":"Garden Planning Tools","href":"/docs/home/related-work","docId":"home/related-work","unlisted":false},{"type":"link","label":"Food Security","href":"/docs/home/food-security","docId":"home/food-security","unlisted":false},{"type":"link","label":"The Team","href":"/docs/home/team","docId":"home/team","unlisted":false}],"collapsible":true},{"type":"category","collapsed":false,"label":"User Guide","items":[{"type":"link","label":"Overview","href":"/docs/user-guide/overview","docId":"user-guide/overview","unlisted":false},{"type":"link","label":"Downloading","href":"/docs/user-guide/downloading","docId":"user-guide/downloading","unlisted":false},{"type":"link","label":"Registration","href":"/docs/user-guide/registration","docId":"user-guide/registration","unlisted":false},{"type":"link","label":"Define a Garden","href":"/docs/user-guide/define-a-garden","docId":"user-guide/define-a-garden","unlisted":false},{"type":"link","label":"Add Plantings to Beds","href":"/docs/user-guide/adding-plantings","docId":"user-guide/adding-plantings","unlisted":false},{"type":"link","label":"Explore a Garden","href":"/docs/user-guide/explore-a-garden","docId":"user-guide/explore-a-garden","unlisted":false},{"type":"link","label":"Explore a Chapter","href":"/docs/user-guide/explore-a-chapter","docId":"user-guide/explore-a-chapter","unlisted":false},{"type":"link","label":"Add Crops, Varieties, Vendors to the Chapter Database","href":"/docs/user-guide/adding-vendors-crops-varieties","docId":"user-guide/adding-vendors-crops-varieties","unlisted":false},{"type":"link","label":"Planting Scenarios","href":"/docs/user-guide/scenarios","docId":"user-guide/scenarios","unlisted":false},{"type":"link","label":"Observations","href":"/docs/user-guide/observations","docId":"user-guide/observations","unlisted":false},{"type":"link","label":"GeoBot","href":"/docs/user-guide/geobot","docId":"user-guide/geobot","unlisted":false},{"type":"link","label":"Badges","href":"/docs/user-guide/badges","docId":"user-guide/badges","unlisted":false},{"type":"link","label":"outcomes","href":"/docs/user-guide/outcomes","docId":"user-guide/outcomes","unlisted":false},{"type":"link","label":"Tasks","href":"/docs/user-guide/tasks","docId":"user-guide/tasks","unlisted":false},{"type":"link","label":"Seeds","href":"/docs/user-guide/seeds","docId":"user-guide/seeds","unlisted":false},{"type":"link","label":"Chat Rooms","href":"/docs/user-guide/chat-rooms","docId":"user-guide/chat-rooms","unlisted":false},{"type":"link","label":"Frequently Asked (Gardening) Questions","href":"/docs/user-guide/guided-tour","docId":"user-guide/guided-tour","unlisted":false},{"type":"link","label":"Terms and Conditions","href":"/docs/user-guide/terms-and-conditions","docId":"user-guide/terms-and-conditions","unlisted":false},{"type":"link","label":"Privacy Policy","href":"/docs/user-guide/privacy","docId":"user-guide/privacy","unlisted":false}],"collapsible":true}],"businessSidebar":[{"type":"link","label":"Welcome","href":"/docs/business/","docId":"business/index","unlisted":false},{"type":"link","label":"Roadmap","href":"/docs/business/roadmap","docId":"business/roadmap","unlisted":false},{"type":"link","label":"Milestones","href":"/docs/business/milestones","docId":"business/milestones","unlisted":false},{"type":"link","label":"Market Size","href":"/docs/business/market-size","docId":"business/market-size","unlisted":false},{"type":"link","label":"Lean Canvas","href":"https://docs.google.com/presentation/d/1oUzy1zeraTf6PgWlk2R3Ea7Iw2Ju24Dds5mffq2o5Wg/edit#slide=id.p1"},{"type":"link","label":"Gardening vs. Farming","href":"https://docs.google.com/presentation/d/1rMu7DWJblHvVJt6CGmR8eyCN7uBMXPfBpc1rdhanxjQ/edit#slide=id.p"},{"type":"link","label":"Home Gardening Pain Points","href":"https://docs.google.com/presentation/d/1TKDWQI60PxRhBpMGW0tvMyXX0nmgEBz71-DmoT2LxfU/edit#slide=id.g11d82564388_0_187"},{"type":"link","label":"BAI 2024 Pitch Deck","href":"https://geogardenclub.com/pdf/bai-pitch-2024.pdf"},{"type":"link","label":"Business Documents Repo","href":"https://github.com/geogardenclub/documents"},{"type":"link","label":"Developer Guide","href":"/docs/develop/"}],"developSidebar":[{"type":"link","label":"Welcome","href":"/docs/develop/","docId":"develop/index","unlisted":false},{"type":"link","label":"Onboarding","href":"/docs/develop/onboarding","docId":"develop/onboarding","unlisted":false},{"type":"link","label":"Installation","href":"/docs/develop/installation","docId":"develop/installation","unlisted":false},{"type":"link","label":"Scripts","href":"/docs/develop/scripts","docId":"develop/scripts","unlisted":false},{"type":"link","label":"Coding Standards","href":"/docs/develop/coding-standards","docId":"develop/coding-standards","unlisted":false},{"type":"link","label":"Architecture","href":"/docs/develop/architecture","docId":"develop/architecture","unlisted":false},{"type":"link","label":"Data Model","href":"/docs/develop/data-model","docId":"develop/data-model","unlisted":false},{"type":"link","label":"Managing Firebase data","href":"/docs/develop/managing-firebase-data","docId":"develop/managing-firebase-data","unlisted":false},{"type":"link","label":"Deployment","href":"/docs/develop/deployment","docId":"develop/deployment","unlisted":false},{"type":"link","label":"Backups","href":"/docs/develop/backups","docId":"develop/backups","unlisted":false},{"type":"category","collapsed":true,"label":"Design Patterns","items":[{"type":"link","label":"Features","href":"/docs/develop/design/features","docId":"develop/design/features","unlisted":false},{"type":"link","label":"Badges","href":"/docs/develop/design/badges","docId":"develop/design/badges","unlisted":false},{"type":"link","label":"Input Fields","href":"/docs/develop/design/input-fields","docId":"develop/design/input-fields","unlisted":false},{"type":"link","label":"\\"With\\" widgets","href":"/docs/develop/design/with-widgets","docId":"develop/design/with-widgets","unlisted":false},{"type":"link","label":"Data Mutation","href":"/docs/develop/design/data-mutation","docId":"develop/design/data-mutation","unlisted":false}],"collapsible":true},{"type":"category","collapsed":false,"label":"Quality Assurance","items":[{"type":"link","label":"Dart analyze","href":"/docs/develop/dart-analyze","docId":"develop/dart-analyze","unlisted":false},{"type":"link","label":"Testing","href":"/docs/develop/testing","docId":"develop/testing","unlisted":false},{"type":"link","label":"Database Integrity Checking","href":"/docs/develop/integrity-check","docId":"develop/integrity-check","unlisted":false}],"collapsible":true},{"type":"category","collapsed":true,"label":"Prior Releases","items":[{"type":"category","label":"Release 1.0 (Technology Evaluation)","collapsed":true,"items":[{"type":"link","label":"Technology Goals","href":"/docs/develop/releases/release-1.0/goals","docId":"develop/releases/release-1.0/goals","unlisted":false},{"type":"link","label":"Core Value Propositions","href":"/docs/develop/releases/release-1.0/cvp","docId":"develop/releases/release-1.0/cvp","unlisted":false},{"type":"link","label":"Onboarding Feedback","href":"/docs/develop/releases/release-1.0/onboarding-feedback","docId":"develop/releases/release-1.0/onboarding-feedback","unlisted":false},{"type":"link","label":"End of Season Feedback","href":"/docs/develop/releases/release-1.0/end-of-season-feedback","docId":"develop/releases/release-1.0/end-of-season-feedback","unlisted":false}],"collapsible":true},{"type":"category","collapsed":true,"label":"Release 0.0 (Mockup)","items":[{"type":"link","label":"Design and implementation","href":"/docs/develop/releases/release-0.0/design","docId":"develop/releases/release-0.0/design","unlisted":false},{"type":"link","label":"Customer feedback","href":"/docs/develop/releases/release-0.0/customer-feedback","docId":"develop/releases/release-0.0/customer-feedback","unlisted":false},{"type":"link","label":"Entrepreneur feedback","href":"/docs/develop/releases/release-0.0/entrepreneur-feedback","docId":"develop/releases/release-0.0/entrepreneur-feedback","unlisted":false},{"type":"link","label":"ChatGPT feedback","href":"/docs/develop/releases/release-0.0/chatgpt-feedback","docId":"develop/releases/release-0.0/chatgpt-feedback","unlisted":false}],"collapsible":true}],"collapsible":true},{"type":"link","label":"Business Development Guide","href":"/docs/business/"}]},"docs":{"business/index":{"id":"business/index","title":"Welcome to the GGC Business Development Guide","description":"Welcome to the Business Development Guide for the Geo Garden Club project.","sidebar":"businessSidebar"},"business/market-size":{"id":"business/market-size","title":"Market Size Estimation (USA)","description":"Here is some data that can help provide a sense for the potential market size for GGC in the United States.","sidebar":"businessSidebar"},"business/milestones":{"id":"business/milestones","title":"Milestones","description":"| Year | Date | Milestone |","sidebar":"businessSidebar"},"business/roadmap":{"id":"business/roadmap","title":"Roadmap","description":"This roadmap documents our approach incremental development and release of our technology.","sidebar":"businessSidebar"},"develop/architecture":{"id":"develop/architecture","title":"Architecture","description":"The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the \\"Riverpod Architecture\\". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the architecture module in my mobile application development course.","sidebar":"developSidebar"},"develop/backups":{"id":"develop/backups","title":"Backups","description":"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn\'t lose too much work.","sidebar":"developSidebar"},"develop/coding-standards":{"id":"develop/coding-standards","title":"Coding Standards","description":"In GGC, coding standards are similar to design patterns, but focus on practices that reduce or avoid \\"technical debt\\".","sidebar":"developSidebar"},"develop/dart-analyze":{"id":"develop/dart-analyze","title":"Dart analyze","description":"One form of quality assurance in GGC is the use of Dart Analyze. This is a static analysis tool that looks for common code problems.","sidebar":"developSidebar"},"develop/data-model":{"id":"develop/data-model","title":"Data Model","description":"This page explains the data model (i.e. the set of entities and their relationships) for GGC, along with a rationale for the design decisions that we\'ve made along the way.","sidebar":"developSidebar"},"develop/deployment":{"id":"develop/deployment","title":"Deployment","description":"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.","sidebar":"developSidebar"},"develop/design/badges":{"id":"develop/design/badges","title":"Badges","description":"Goals","sidebar":"developSidebar"},"develop/design/data-mutation":{"id":"develop/design/data-mutation","title":"Data Mutation","description":"Prelude: AsyncValue","sidebar":"developSidebar"},"develop/design/features":{"id":"develop/design/features","title":"Features","description":"The GGC app loosely follows the \\"feature first\\" design philosophy expressed in Andrea Bizzotto\'s article Flutter Project Structure","sidebar":"developSidebar"},"develop/design/input-fields":{"id":"develop/design/input-fields","title":"Input Fields","description":"Motivation","sidebar":"developSidebar"},"develop/design/with-widgets":{"id":"develop/design/with-widgets","title":"\\"With\\" widgets","description":"Why \\"with\\"?","sidebar":"developSidebar"},"develop/index":{"id":"develop/index","title":"Welcome to the GGC Developers Guide","description":"Welcome to the Developer\'s Guide for the Geo Garden Club project.","sidebar":"developSidebar"},"develop/installation":{"id":"develop/installation","title":"Installation","description":"Flutter","sidebar":"developSidebar"},"develop/integrity-check":{"id":"develop/integrity-check","title":"Database Integrity Checking","description":"We use a Firebase database to store the data associated with GGC. One of the many important quality assurance issues associated with managing Firebase data is to ascertain whether or not the database contents are in a consistent state. In other words, does the database exhibit database integrity?","sidebar":"developSidebar"},"develop/managing-firebase-data":{"id":"develop/managing-firebase-data","title":"Managing Firebase data","description":"We use a Firebase database to store the data associated with GGC. There are several important issues associated with managing Firebase data.","sidebar":"developSidebar"},"develop/onboarding":{"id":"develop/onboarding","title":"Onboarding","description":"Welcome, new GGC developer! This page provides a checklist of things required to get started with our technology.","sidebar":"developSidebar"},"develop/releases/release-0.0/chatgpt-feedback":{"id":"develop/releases/release-0.0/chatgpt-feedback","title":"ChatGPT feedback","description":"Just for fun, I had a conversation with ChatGPT in April 2023 about how to design a gardening app. Much of its responses were uninsightful, but it did come up with some banging ideas for badges.","sidebar":"developSidebar"},"develop/releases/release-0.0/customer-feedback":{"id":"develop/releases/release-0.0/customer-feedback","title":"Customer feedback","description":"Following the Lean Startup principle of \\"validated learning\\", we performed an evaluation study of the technology innovations present in the mockup with 24 experienced gardeners during the summer and fall of 2022. We report on the results of that study in the following 15 minute video (or 7 minutes, if you run the video at 2x speed):","sidebar":"developSidebar"},"develop/releases/release-0.0/design":{"id":"develop/releases/release-0.0/design","title":"Design and implementation","description":"In 2022, we built a simple single page web application using React. The goal of the system was to provide an executable mockup to perform \\"customer discovery\\". The mockup showed potential users our vision of some of the features that would be made available in our production technology, and enabled us to gather feedback on the potential utility and value of our vision.","sidebar":"developSidebar"},"develop/releases/release-0.0/entrepreneur-feedback":{"id":"develop/releases/release-0.0/entrepreneur-feedback","title":"Entrepreneur feedback","description":"The following sections document feedback received from entrepreneurs and high tech professionals (who are not necessarily gardeners, and thus not in our customer demographics). Our request to these folks was to evaluate the viability of Agile/Geo Garden Club as a commercial technology venture, and to help us identify opportunities for improvement. We got a lot of useful feedback.","sidebar":"developSidebar"},"develop/releases/release-1.0/cvp":{"id":"develop/releases/release-1.0/cvp","title":"Core Value Propositions","description":"The goal of the 1.0 (Technology Evaluation) release is to provide an app to a small group that will use the app and provide us with feedback. The 1.0 release will partially test our business model by helping us evaluate its success at implementing the \\"core value propositions\\" (CVPs) for GGC.","sidebar":"developSidebar"},"develop/releases/release-1.0/end-of-season-feedback":{"id":"develop/releases/release-1.0/end-of-season-feedback","title":"End of Season Feedback","description":"In September 2024, Jenna sent out an 8 question survey to users to request feedback. We received five responses. Here is a summary of the feedback:","sidebar":"developSidebar"},"develop/releases/release-1.0/goals":{"id":"develop/releases/release-1.0/goals","title":"Technology Goals","description":"Here are the goals for the 1.0 (Technology Evaluation) release. Some of these goals are motivated by Champion Building: How to successfully adopt a developer tool. Although this blog post focuses on how to get developers in an organization to adopt a new tool, the recommendation seem very applicable to getting gardeners in a community to adopt GGC.","sidebar":"developSidebar"},"develop/releases/release-1.0/onboarding-feedback":{"id":"develop/releases/release-1.0/onboarding-feedback","title":"Onboarding Feedback","description":"Jenna personally onboarded six users to Release 1.0 and gathered the following feedback from them either during the onboarding session or shortly thereafter.","sidebar":"developSidebar"},"develop/scripts":{"id":"develop/scripts","title":"Scripts","description":"GGC development is supported by a number of Unix shell scripts. All scripts are named starting with \\"run\\" and use snake case to separate words in the script name.","sidebar":"developSidebar"},"develop/testing":{"id":"develop/testing","title":"Testing","description":"Another form of quality assurance in GGC is testing.","sidebar":"developSidebar"},"home/food-security":{"id":"home/food-security","title":"Food Security","description":"Food security is the elephant in the room, and a major motivation for founding GeoGardenClub. Here is a brief introduction to the relationship between food security and home gardens.","sidebar":"homeSidebar"},"home/innovations":{"id":"home/innovations","title":"Design Innovations","description":"What should an app provide if it is intended to support the needs of \\"serious\\" gardeners? Here is GeoGardenClub\'s answer:","sidebar":"homeSidebar"},"home/related-work":{"id":"home/related-work","title":"Garden Planning Tools","description":"If you search for \\"garden planning tools\\" on the Internet, you\'ll find dozens of applications. Most of those are essentially \\"landscape architecture\\" tools for people who want to design the visual look of their (flower) gardens. This is an interesting design problem, but not the problem addressed by Geo Garden Club.","sidebar":"homeSidebar"},"home/serious-gardeners":{"id":"home/serious-gardeners","title":"\\"Serious\\" Gardeners","description":"The founders of GeoGardenClub view food production as a vast continuum of activities and levels of commitment, as shown in the following diagram:","sidebar":"homeSidebar"},"home/sneak-peek":{"id":"home/sneak-peek","title":"Mobile App Sneak Peek","description":"We are working on the alpha release of the GeoGardenClub mobile app, with an expected release date of early 2024. While the app is not yet ready for prime time, we thought it would be fun to show you some selected screen shots so you can get an idea of where we\'re heading."},"home/team":{"id":"home/team","title":"The Team","description":"Jenna Deane","sidebar":"homeSidebar"},"home/welcome":{"id":"home/welcome","title":"Welcome","description":"GGC started in 2021 when a small group of gardeners in Bellingham, Washington realized that, despite the availability of dozens of home gardening apps, we were planning and managing our gardens using spreadsheets, Word docs, and even paper and pencil!","sidebar":"homeSidebar"},"user-guide/adding-plantings":{"id":"user-guide/adding-plantings","title":"Add Plantings to Beds","description":"Add plantings","sidebar":"homeSidebar"},"user-guide/adding-vendors-crops-varieties":{"id":"user-guide/adding-vendors-crops-varieties","title":"Add Crops, Varieties, Vendors to the Chapter Database","description":"The crops, varieties, and vendors in each chapter\'s database are crowdsourced from the chapter\'s members. This ensures that the database is exclusive to what is grown in the area. To add a crop, variety, or vendor navigate to the appropriate index screen from the side navigation menu.","sidebar":"homeSidebar"},"user-guide/badges":{"id":"user-guide/badges","title":"Badges","description":"Why Badges?","sidebar":"homeSidebar"},"user-guide/chat-rooms":{"id":"user-guide/chat-rooms","title":"Chat Rooms","description":"GGC allows gardeners to communicate with each other through chat rooms. Chat rooms are a great way to ask questions, share knowledge, and build community. Chat rooms are organized by chapter or garden.","sidebar":"homeSidebar"},"user-guide/define-a-garden":{"id":"user-guide/define-a-garden","title":"Define a Garden","description":"Defining a garden in GeoGardenClub requires you to create three kinds of entities: a \\"Garden\\", one or more \\"Beds\\", and one or more \\"Plantings\\". (You can also define a fourth type of entity, \\"Outcomes\\", but we\'ll get to that later).","sidebar":"homeSidebar"},"user-guide/downloading":{"id":"user-guide/downloading","title":"Downloading","description":"Enroll as a GGC Beta Tester","sidebar":"homeSidebar"},"user-guide/explore-a-chapter":{"id":"user-guide/explore-a-chapter","title":"Explore a Chapter","description":"Find the Chapter Index Screens","sidebar":"homeSidebar"},"user-guide/explore-a-garden":{"id":"user-guide/explore-a-garden","title":"Explore a Garden","description":"The summary card for every Garden in a Chapter contains a button labeled \\"Details\\". Pressing that button takes you to a screen with a bottom nav bar containing icons taking you to four screens: Timeline, Filter, Outcomes, and Tasks. Each view presents a useful perspective on that Garden.","sidebar":"homeSidebar"},"user-guide/geobot":{"id":"user-guide/geobot","title":"GeoBot","description":"Why GeoBot?","sidebar":"homeSidebar"},"user-guide/guided-tour":{"id":"user-guide/guided-tour","title":"Frequently Asked (Gardening) Questions","description":"GeoGardenClub was created to increase the performance of home, community, and school gardens and efficiency of the gardener by answering questions a gardener might have as they plan or work through the gardening season. Here are some of the questions that GeoGardenClub can help answer:","sidebar":"homeSidebar"},"user-guide/observations":{"id":"user-guide/observations","title":"Observations","description":"GGC allows gardeners to make \\"observations\\" regarding a planting of a plant variety on a specific day. Observations can include phenomena such as successful germination, first flower, first harvest, diseases, or pests, document outcomes like yields, or simply record the progress of a planting.","sidebar":"homeSidebar"},"user-guide/outcomes":{"id":"user-guide/outcomes","title":"outcomes","description":"300---","sidebar":"homeSidebar"},"user-guide/overview":{"id":"user-guide/overview","title":"Overview","description":"Welcome to the User Guide for the GeoGardenClub app.","sidebar":"homeSidebar"},"user-guide/privacy":{"id":"user-guide/privacy","title":"Privacy Policy","description":"Last updated June 20, 2024","sidebar":"homeSidebar"},"user-guide/registration":{"id":"user-guide/registration","title":"Registration","description":"After downloading the app, open it to see the login screen. Tap Register to create a new account.","sidebar":"homeSidebar"},"user-guide/scenarios":{"id":"user-guide/scenarios","title":"Planting Scenarios","description":"Growing plants in a greenhouse or other climate controlled environment","sidebar":"homeSidebar"},"user-guide/seeds":{"id":"user-guide/seeds","title":"Seeds","description":"Geo Garden Club believes that seed saving and sharing is a critical component of building resilient gardening communities. By saving seeds, gardeners can ensure that they have access to the varieties that grow best in their area. By sharing seeds, gardeners can help others in their community grow the same varieties.","sidebar":"homeSidebar"},"user-guide/tasks":{"id":"user-guide/tasks","title":"Tasks","description":"What are Tasks?","sidebar":"homeSidebar"},"user-guide/terms-and-conditions":{"id":"user-guide/terms-and-conditions","title":"Terms and Conditions","description":"Last updated February 10, 2024","sidebar":"homeSidebar"}}}}')}}]); \ No newline at end of file diff --git a/assets/js/0058b4c6.e179fb10.js b/assets/js/0058b4c6.e179fb10.js deleted file mode 100644 index 300c2879f..000000000 --- a/assets/js/0058b4c6.e179fb10.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4088],{6462:e=>{e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"homeSidebar":[{"type":"category","collapsed":false,"label":"About GeoGardenClub","items":[{"type":"link","label":"Welcome","href":"/docs/home/welcome","docId":"home/welcome","unlisted":false},{"type":"link","label":"\\"Serious\\" Gardeners","href":"/docs/home/serious-gardeners","docId":"home/serious-gardeners","unlisted":false},{"type":"link","label":"Design Innovations","href":"/docs/home/innovations","docId":"home/innovations","unlisted":false},{"type":"link","label":"Garden Planning Tools","href":"/docs/home/related-work","docId":"home/related-work","unlisted":false},{"type":"link","label":"Food Security","href":"/docs/home/food-security","docId":"home/food-security","unlisted":false},{"type":"link","label":"The Team","href":"/docs/home/team","docId":"home/team","unlisted":false}],"collapsible":true},{"type":"category","collapsed":false,"label":"User Guide","items":[{"type":"link","label":"Overview","href":"/docs/user-guide/overview","docId":"user-guide/overview","unlisted":false},{"type":"link","label":"Downloading","href":"/docs/user-guide/downloading","docId":"user-guide/downloading","unlisted":false},{"type":"link","label":"Registration","href":"/docs/user-guide/registration","docId":"user-guide/registration","unlisted":false},{"type":"link","label":"Define a Garden","href":"/docs/user-guide/define-a-garden","docId":"user-guide/define-a-garden","unlisted":false},{"type":"link","label":"Add Plantings to Beds","href":"/docs/user-guide/adding-plantings","docId":"user-guide/adding-plantings","unlisted":false},{"type":"link","label":"Explore a Garden","href":"/docs/user-guide/explore-a-garden","docId":"user-guide/explore-a-garden","unlisted":false},{"type":"link","label":"Explore a Chapter","href":"/docs/user-guide/explore-a-chapter","docId":"user-guide/explore-a-chapter","unlisted":false},{"type":"link","label":"Add Crops, Varieties, Vendors to the Chapter Database","href":"/docs/user-guide/adding-vendors-crops-varieties","docId":"user-guide/adding-vendors-crops-varieties","unlisted":false},{"type":"link","label":"Planting Scenarios","href":"/docs/user-guide/scenarios","docId":"user-guide/scenarios","unlisted":false},{"type":"link","label":"Observations","href":"/docs/user-guide/observations","docId":"user-guide/observations","unlisted":false},{"type":"link","label":"GeoBot","href":"/docs/user-guide/geobot","docId":"user-guide/geobot","unlisted":false},{"type":"link","label":"Badges","href":"/docs/user-guide/badges","docId":"user-guide/badges","unlisted":false},{"type":"link","label":"outcomes","href":"/docs/user-guide/outcomes","docId":"user-guide/outcomes","unlisted":false},{"type":"link","label":"Tasks","href":"/docs/user-guide/tasks","docId":"user-guide/tasks","unlisted":false},{"type":"link","label":"Seeds","href":"/docs/user-guide/seeds","docId":"user-guide/seeds","unlisted":false},{"type":"link","label":"Chat Rooms","href":"/docs/user-guide/chat-rooms","docId":"user-guide/chat-rooms","unlisted":false},{"type":"link","label":"Frequently Asked (Gardening) Questions","href":"/docs/user-guide/guided-tour","docId":"user-guide/guided-tour","unlisted":false},{"type":"link","label":"Terms and Conditions","href":"/docs/user-guide/terms-and-conditions","docId":"user-guide/terms-and-conditions","unlisted":false},{"type":"link","label":"Privacy Policy","href":"/docs/user-guide/privacy","docId":"user-guide/privacy","unlisted":false}],"collapsible":true}],"businessSidebar":[{"type":"link","label":"Welcome","href":"/docs/business/","docId":"business/index","unlisted":false},{"type":"link","label":"Roadmap","href":"/docs/business/roadmap","docId":"business/roadmap","unlisted":false},{"type":"link","label":"Milestones","href":"/docs/business/milestones","docId":"business/milestones","unlisted":false},{"type":"link","label":"Market Size","href":"/docs/business/market-size","docId":"business/market-size","unlisted":false},{"type":"link","label":"Lean Canvas","href":"https://docs.google.com/presentation/d/1oUzy1zeraTf6PgWlk2R3Ea7Iw2Ju24Dds5mffq2o5Wg/edit#slide=id.p1"},{"type":"link","label":"Gardening vs. Farming","href":"https://docs.google.com/presentation/d/1rMu7DWJblHvVJt6CGmR8eyCN7uBMXPfBpc1rdhanxjQ/edit#slide=id.p"},{"type":"link","label":"Home Gardening Pain Points","href":"https://docs.google.com/presentation/d/1TKDWQI60PxRhBpMGW0tvMyXX0nmgEBz71-DmoT2LxfU/edit#slide=id.g11d82564388_0_187"},{"type":"link","label":"BAI 2024 Pitch Deck","href":"https://geogardenclub.com/pdf/bai-pitch-2024.pdf"},{"type":"link","label":"Business Documents Repo","href":"https://github.com/geogardenclub/documents"},{"type":"link","label":"Developer Guide","href":"/docs/develop/"}],"developSidebar":[{"type":"link","label":"Welcome","href":"/docs/develop/","docId":"develop/index","unlisted":false},{"type":"link","label":"Onboarding","href":"/docs/develop/onboarding","docId":"develop/onboarding","unlisted":false},{"type":"link","label":"Installation","href":"/docs/develop/installation","docId":"develop/installation","unlisted":false},{"type":"link","label":"Scripts","href":"/docs/develop/scripts","docId":"develop/scripts","unlisted":false},{"type":"link","label":"Coding Standards","href":"/docs/develop/coding-standards","docId":"develop/coding-standards","unlisted":false},{"type":"link","label":"Architecture","href":"/docs/develop/architecture","docId":"develop/architecture","unlisted":false},{"type":"link","label":"Managing Firebase data","href":"/docs/develop/managing-firebase-data","docId":"develop/managing-firebase-data","unlisted":false},{"type":"link","label":"Deployment","href":"/docs/develop/deployment","docId":"develop/deployment","unlisted":false},{"type":"link","label":"Backups","href":"/docs/develop/backups","docId":"develop/backups","unlisted":false},{"type":"category","collapsed":true,"label":"Design Details","items":[{"type":"link","label":"Anatomy of a feature","href":"/docs/develop/design/features","docId":"develop/design/features","unlisted":false},{"type":"link","label":"Data Model","href":"/docs/develop/design/data-model","docId":"develop/design/data-model","unlisted":false},{"type":"link","label":"Badges","href":"/docs/develop/design/badges","docId":"develop/design/badges","unlisted":false},{"type":"link","label":"GGC Input Fields","href":"/docs/develop/design/input-fields","docId":"develop/design/input-fields","unlisted":false},{"type":"link","label":"\\"With\\" widgets","href":"/docs/develop/design/with-widgets","docId":"develop/design/with-widgets","unlisted":false},{"type":"link","label":"Data Mutation","href":"/docs/develop/design/data-mutation","docId":"develop/design/data-mutation","unlisted":false}],"collapsible":true},{"type":"category","collapsed":false,"label":"Quality Assurance","items":[{"type":"link","label":"Dart analyze","href":"/docs/develop/dart-analyze","docId":"develop/dart-analyze","unlisted":false},{"type":"link","label":"Testing","href":"/docs/develop/testing","docId":"develop/testing","unlisted":false},{"type":"link","label":"Database Integrity Checking","href":"/docs/develop/integrity-check","docId":"develop/integrity-check","unlisted":false}],"collapsible":true},{"type":"category","collapsed":true,"label":"Prior Releases","items":[{"type":"category","label":"Release 1.0 (Technology Evaluation)","collapsed":true,"items":[{"type":"link","label":"Technology Goals","href":"/docs/develop/releases/release-1.0/goals","docId":"develop/releases/release-1.0/goals","unlisted":false},{"type":"link","label":"Core Value Propositions","href":"/docs/develop/releases/release-1.0/cvp","docId":"develop/releases/release-1.0/cvp","unlisted":false},{"type":"link","label":"Onboarding Feedback","href":"/docs/develop/releases/release-1.0/onboarding-feedback","docId":"develop/releases/release-1.0/onboarding-feedback","unlisted":false},{"type":"link","label":"End of Season Feedback","href":"/docs/develop/releases/release-1.0/end-of-season-feedback","docId":"develop/releases/release-1.0/end-of-season-feedback","unlisted":false}],"collapsible":true},{"type":"category","collapsed":true,"label":"Release 0.0 (Mockup)","items":[{"type":"link","label":"Design and implementation","href":"/docs/develop/releases/release-0.0/design","docId":"develop/releases/release-0.0/design","unlisted":false},{"type":"link","label":"Customer feedback","href":"/docs/develop/releases/release-0.0/customer-feedback","docId":"develop/releases/release-0.0/customer-feedback","unlisted":false},{"type":"link","label":"Entrepreneur feedback","href":"/docs/develop/releases/release-0.0/entrepreneur-feedback","docId":"develop/releases/release-0.0/entrepreneur-feedback","unlisted":false},{"type":"link","label":"ChatGPT feedback","href":"/docs/develop/releases/release-0.0/chatgpt-feedback","docId":"develop/releases/release-0.0/chatgpt-feedback","unlisted":false}],"collapsible":true}],"collapsible":true},{"type":"link","label":"Business Development Guide","href":"/docs/business/"}]},"docs":{"business/index":{"id":"business/index","title":"Welcome to the GGC Business Development Guide","description":"Welcome to the Business Development Guide for the Geo Garden Club project.","sidebar":"businessSidebar"},"business/market-size":{"id":"business/market-size","title":"Market Size Estimation (USA)","description":"Here is some data that can help provide a sense for the potential market size for GGC in the United States.","sidebar":"businessSidebar"},"business/milestones":{"id":"business/milestones","title":"Milestones","description":"| Year | Date | Milestone |","sidebar":"businessSidebar"},"business/roadmap":{"id":"business/roadmap","title":"Roadmap","description":"This roadmap documents our approach incremental development and release of our technology.","sidebar":"businessSidebar"},"develop/architecture":{"id":"develop/architecture","title":"Architecture","description":"The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the \\"Riverpod Architecture\\". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the architecture module in my mobile application development course.","sidebar":"developSidebar"},"develop/backups":{"id":"develop/backups","title":"Backups","description":"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn\'t lose too much work.","sidebar":"developSidebar"},"develop/coding-standards":{"id":"develop/coding-standards","title":"Coding Standards","description":"In GGC, coding standards are similar to design patterns, but focus on practices that reduce or avoid \\"technical debt\\".","sidebar":"developSidebar"},"develop/dart-analyze":{"id":"develop/dart-analyze","title":"Dart analyze","description":"One form of quality assurance in GGC is the use of Dart Analyze. This is a static analysis tool that looks for common code problems.","sidebar":"developSidebar"},"develop/deployment":{"id":"develop/deployment","title":"Deployment","description":"For the GeoGardenClub project, deployment refers to the process by which a version of the GeoGardenClub app is made available on a physical device such as an Apple or Android phone or tablet.","sidebar":"developSidebar"},"develop/design/badges":{"id":"develop/design/badges","title":"Badges","description":"Goals","sidebar":"developSidebar"},"develop/design/data-model":{"id":"develop/design/data-model","title":"Data Model","description":"This page explains the data model (i.e. the set of entities and their relationships) for GGC, along with a rationale for the design decisions that we\'ve made along the way.","sidebar":"developSidebar"},"develop/design/data-model-old":{"id":"develop/design/data-model-old","title":"Data Model","description":"This page documents the data model intended to satisfy the 1.0 release requirements."},"develop/design/data-mutation":{"id":"develop/design/data-mutation","title":"Data Mutation","description":"Prelude: AsyncValue","sidebar":"developSidebar"},"develop/design/features":{"id":"develop/design/features","title":"Anatomy of a feature","description":"The GGC app loosely follows the \\"feature first\\" design philosophy expressed in Andrea Bizzotto\'s article Flutter Project Structure","sidebar":"developSidebar"},"develop/design/input-fields":{"id":"develop/design/input-fields","title":"GGC Input Fields","description":"Motivation","sidebar":"developSidebar"},"develop/design/with-widgets":{"id":"develop/design/with-widgets","title":"\\"With\\" widgets","description":"Why \\"with\\"?","sidebar":"developSidebar"},"develop/index":{"id":"develop/index","title":"Welcome to the GGC Developers Guide","description":"Welcome to the Developer\'s Guide for the Geo Garden Club project.","sidebar":"developSidebar"},"develop/installation":{"id":"develop/installation","title":"Installation","description":"Flutter","sidebar":"developSidebar"},"develop/integrity-check":{"id":"develop/integrity-check","title":"Database Integrity Checking","description":"We use a Firebase database to store the data associated with GGC. One of the many important quality assurance issues associated with managing Firebase data is to ascertain whether or not the database contents are in a consistent state. In other words, does the database exhibit database integrity?","sidebar":"developSidebar"},"develop/managing-firebase-data":{"id":"develop/managing-firebase-data","title":"Managing Firebase data","description":"We use a Firebase database to store the data associated with GGC. There are several important issues associated with managing Firebase data.","sidebar":"developSidebar"},"develop/onboarding":{"id":"develop/onboarding","title":"Onboarding","description":"Welcome, new GGC developer! This page provides a checklist of things required to get started with our technology.","sidebar":"developSidebar"},"develop/releases/release-0.0/chatgpt-feedback":{"id":"develop/releases/release-0.0/chatgpt-feedback","title":"ChatGPT feedback","description":"Just for fun, I had a conversation with ChatGPT in April 2023 about how to design a gardening app. Much of its responses were uninsightful, but it did come up with some banging ideas for badges.","sidebar":"developSidebar"},"develop/releases/release-0.0/customer-feedback":{"id":"develop/releases/release-0.0/customer-feedback","title":"Customer feedback","description":"Following the Lean Startup principle of \\"validated learning\\", we performed an evaluation study of the technology innovations present in the mockup with 24 experienced gardeners during the summer and fall of 2022. We report on the results of that study in the following 15 minute video (or 7 minutes, if you run the video at 2x speed):","sidebar":"developSidebar"},"develop/releases/release-0.0/design":{"id":"develop/releases/release-0.0/design","title":"Design and implementation","description":"In 2022, we built a simple single page web application using React. The goal of the system was to provide an executable mockup to perform \\"customer discovery\\". The mockup showed potential users our vision of some of the features that would be made available in our production technology, and enabled us to gather feedback on the potential utility and value of our vision.","sidebar":"developSidebar"},"develop/releases/release-0.0/entrepreneur-feedback":{"id":"develop/releases/release-0.0/entrepreneur-feedback","title":"Entrepreneur feedback","description":"The following sections document feedback received from entrepreneurs and high tech professionals (who are not necessarily gardeners, and thus not in our customer demographics). Our request to these folks was to evaluate the viability of Agile/Geo Garden Club as a commercial technology venture, and to help us identify opportunities for improvement. We got a lot of useful feedback.","sidebar":"developSidebar"},"develop/releases/release-1.0/cvp":{"id":"develop/releases/release-1.0/cvp","title":"Core Value Propositions","description":"The goal of the 1.0 (Technology Evaluation) release is to provide an app to a small group that will use the app and provide us with feedback. The 1.0 release will partially test our business model by helping us evaluate its success at implementing the \\"core value propositions\\" (CVPs) for GGC.","sidebar":"developSidebar"},"develop/releases/release-1.0/end-of-season-feedback":{"id":"develop/releases/release-1.0/end-of-season-feedback","title":"End of Season Feedback","description":"In September 2024, Jenna sent out an 8 question survey to users to request feedback. We received five responses. Here is a summary of the feedback:","sidebar":"developSidebar"},"develop/releases/release-1.0/goals":{"id":"develop/releases/release-1.0/goals","title":"Technology Goals","description":"Here are the goals for the 1.0 (Technology Evaluation) release. Some of these goals are motivated by Champion Building: How to successfully adopt a developer tool. Although this blog post focuses on how to get developers in an organization to adopt a new tool, the recommendation seem very applicable to getting gardeners in a community to adopt GGC.","sidebar":"developSidebar"},"develop/releases/release-1.0/onboarding-feedback":{"id":"develop/releases/release-1.0/onboarding-feedback","title":"Onboarding Feedback","description":"Jenna personally onboarded six users to Release 1.0 and gathered the following feedback from them either during the onboarding session or shortly thereafter.","sidebar":"developSidebar"},"develop/scripts":{"id":"develop/scripts","title":"Scripts","description":"GGC development is supported by a number of Unix shell scripts. All scripts are named starting with \\"run\\" and use snake case to separate words in the script name.","sidebar":"developSidebar"},"develop/testing":{"id":"develop/testing","title":"Testing","description":"Another form of quality assurance in GGC is testing.","sidebar":"developSidebar"},"home/food-security":{"id":"home/food-security","title":"Food Security","description":"Food security is the elephant in the room, and a major motivation for founding GeoGardenClub. Here is a brief introduction to the relationship between food security and home gardens.","sidebar":"homeSidebar"},"home/innovations":{"id":"home/innovations","title":"Design Innovations","description":"What should an app provide if it is intended to support the needs of \\"serious\\" gardeners? Here is GeoGardenClub\'s answer:","sidebar":"homeSidebar"},"home/related-work":{"id":"home/related-work","title":"Garden Planning Tools","description":"If you search for \\"garden planning tools\\" on the Internet, you\'ll find dozens of applications. Most of those are essentially \\"landscape architecture\\" tools for people who want to design the visual look of their (flower) gardens. This is an interesting design problem, but not the problem addressed by Geo Garden Club.","sidebar":"homeSidebar"},"home/serious-gardeners":{"id":"home/serious-gardeners","title":"\\"Serious\\" Gardeners","description":"The founders of GeoGardenClub view food production as a vast continuum of activities and levels of commitment, as shown in the following diagram:","sidebar":"homeSidebar"},"home/sneak-peek":{"id":"home/sneak-peek","title":"Mobile App Sneak Peek","description":"We are working on the alpha release of the GeoGardenClub mobile app, with an expected release date of early 2024. While the app is not yet ready for prime time, we thought it would be fun to show you some selected screen shots so you can get an idea of where we\'re heading."},"home/team":{"id":"home/team","title":"The Team","description":"Jenna Deane","sidebar":"homeSidebar"},"home/welcome":{"id":"home/welcome","title":"Welcome","description":"GGC started in 2021 when a small group of gardeners in Bellingham, Washington realized that, despite the availability of dozens of home gardening apps, we were planning and managing our gardens using spreadsheets, Word docs, and even paper and pencil!","sidebar":"homeSidebar"},"user-guide/adding-plantings":{"id":"user-guide/adding-plantings","title":"Add Plantings to Beds","description":"Add plantings","sidebar":"homeSidebar"},"user-guide/adding-vendors-crops-varieties":{"id":"user-guide/adding-vendors-crops-varieties","title":"Add Crops, Varieties, Vendors to the Chapter Database","description":"The crops, varieties, and vendors in each chapter\'s database are crowdsourced from the chapter\'s members. This ensures that the database is exclusive to what is grown in the area. To add a crop, variety, or vendor navigate to the appropriate index screen from the side navigation menu.","sidebar":"homeSidebar"},"user-guide/badges":{"id":"user-guide/badges","title":"Badges","description":"Why Badges?","sidebar":"homeSidebar"},"user-guide/chat-rooms":{"id":"user-guide/chat-rooms","title":"Chat Rooms","description":"GGC allows gardeners to communicate with each other through chat rooms. Chat rooms are a great way to ask questions, share knowledge, and build community. Chat rooms are organized by chapter or garden.","sidebar":"homeSidebar"},"user-guide/define-a-garden":{"id":"user-guide/define-a-garden","title":"Define a Garden","description":"Defining a garden in GeoGardenClub requires you to create three kinds of entities: a \\"Garden\\", one or more \\"Beds\\", and one or more \\"Plantings\\". (You can also define a fourth type of entity, \\"Outcomes\\", but we\'ll get to that later).","sidebar":"homeSidebar"},"user-guide/downloading":{"id":"user-guide/downloading","title":"Downloading","description":"Enroll as a GGC Beta Tester","sidebar":"homeSidebar"},"user-guide/explore-a-chapter":{"id":"user-guide/explore-a-chapter","title":"Explore a Chapter","description":"Find the Chapter Index Screens","sidebar":"homeSidebar"},"user-guide/explore-a-garden":{"id":"user-guide/explore-a-garden","title":"Explore a Garden","description":"The summary card for every Garden in a Chapter contains a button labeled \\"Details\\". Pressing that button takes you to a screen with a bottom nav bar containing icons taking you to four screens: Timeline, Filter, Outcomes, and Tasks. Each view presents a useful perspective on that Garden.","sidebar":"homeSidebar"},"user-guide/geobot":{"id":"user-guide/geobot","title":"GeoBot","description":"Why GeoBot?","sidebar":"homeSidebar"},"user-guide/guided-tour":{"id":"user-guide/guided-tour","title":"Frequently Asked (Gardening) Questions","description":"GeoGardenClub was created to increase the performance of home, community, and school gardens and efficiency of the gardener by answering questions a gardener might have as they plan or work through the gardening season. Here are some of the questions that GeoGardenClub can help answer:","sidebar":"homeSidebar"},"user-guide/observations":{"id":"user-guide/observations","title":"Observations","description":"GGC allows gardeners to make \\"observations\\" regarding a planting of a plant variety on a specific day. Observations can include phenomena such as successful germination, first flower, first harvest, diseases, or pests, document outcomes like yields, or simply record the progress of a planting.","sidebar":"homeSidebar"},"user-guide/outcomes":{"id":"user-guide/outcomes","title":"outcomes","description":"300---","sidebar":"homeSidebar"},"user-guide/overview":{"id":"user-guide/overview","title":"Overview","description":"Welcome to the User Guide for the GeoGardenClub app.","sidebar":"homeSidebar"},"user-guide/privacy":{"id":"user-guide/privacy","title":"Privacy Policy","description":"Last updated June 20, 2024","sidebar":"homeSidebar"},"user-guide/registration":{"id":"user-guide/registration","title":"Registration","description":"After downloading the app, open it to see the login screen. Tap Register to create a new account.","sidebar":"homeSidebar"},"user-guide/scenarios":{"id":"user-guide/scenarios","title":"Planting Scenarios","description":"Growing plants in a greenhouse or other climate controlled environment","sidebar":"homeSidebar"},"user-guide/seeds":{"id":"user-guide/seeds","title":"Seeds","description":"Geo Garden Club believes that seed saving and sharing is a critical component of building resilient gardening communities. By saving seeds, gardeners can ensure that they have access to the varieties that grow best in their area. By sharing seeds, gardeners can help others in their community grow the same varieties.","sidebar":"homeSidebar"},"user-guide/tasks":{"id":"user-guide/tasks","title":"Tasks","description":"What are Tasks?","sidebar":"homeSidebar"},"user-guide/terms-and-conditions":{"id":"user-guide/terms-and-conditions","title":"Terms and Conditions","description":"Last updated February 10, 2024","sidebar":"homeSidebar"}}}}')}}]); \ No newline at end of file diff --git a/assets/js/10921c5b.0e4c824d.js b/assets/js/10921c5b.0e4c824d.js new file mode 100644 index 000000000..394004485 --- /dev/null +++ b/assets/js/10921c5b.0e4c824d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[1744],{4437:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>c,frontMatter:()=>i,metadata:()=>o,toc:()=>d});var a=n(5893),r=n(1151);const i={hide_table_of_contents:!1},s='"With" widgets',o={id:"develop/design/with-widgets",title:'"With" widgets',description:'Why "with"?',source:"@site/docs/develop/design/with-widgets.md",sourceDirName:"develop/design",slug:"/develop/design/with-widgets",permalink:"/docs/develop/design/with-widgets",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Input Fields",permalink:"/docs/develop/design/input-fields"},next:{title:"Data Mutation",permalink:"/docs/develop/design/data-mutation"}},l={},d=[{value:"Why "with"?",id:"why-with",level:2},{value:"WithMonarchData",id:"withmonarchdata",level:2},{value:"WithGardenData, etc",id:"withgardendata-etc",level:2}];function h(e){const t={admonition:"admonition",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.header,{children:(0,a.jsx)(t.h1,{id:"with-widgets",children:'"With" widgets'})}),"\n",(0,a.jsx)(t.h2,{id:"why-with",children:'Why "with"?'}),"\n",(0,a.jsx)(t.p,{children:'One important issue to address in a client-server architecture is the asynchronous nature of client-server communication. In other words, when the client needs data from the server, it makes a request that can take time to complete, and may not complete successfully. The client UI should not simply "freeze" during this time, and should "fail gracefully" if the request does not complete successfully.'}),"\n",(0,a.jsx)(t.p,{children:'Making things even more complicated is the desire for modern UIs to be "reactive". This means that if the server\'s database content changes (for example, one user creates a new garden), then all the other clients currently connected should see their UI automatically refresh with updated information (for example, the number of gardens in the Chapter should increase by one for all other users.)'}),"\n",(0,a.jsx)(t.p,{children:'Making things yet more complicated in GGC is the desire to support a "Storybook" style design system like Monarch, in which individual Views as well as entire Screens can be displayed with sample data values without requiring a database connection.'}),"\n",(0,a.jsx)(t.p,{children:'In GGC, we address all of these issues through a design pattern that starts with a set of widgets which we call the "With" widgets: WithAllData, WithCoreData, WithGardenData, WithMonarchData, WithObservationData, and so on.'}),"\n",(0,a.jsx)(t.p,{children:'In this design pattern, all of the data that a Widget needs to build a user interface component can always be found somewhere within three "top-level" client-side classes: ChapterCollection, GardenCollection, and UserCollection.'}),"\n",(0,a.jsx)(t.p,{children:'In addition, UI widgets come in two basic flavors, "Screens" and "Views". Screens are a kind of "top-level" UI widget which must take responsibility for building the ChapterCollection, GardenCollection, and UserCollection classes. They accomplish this by invoking a "With" widget. Views are always a "child" of a Screen widget, and must be passed ChapterCollection, GardenCollection, and UserCollection instances from their parent Widget. So, database access always happens at the Screen-level, and from then on the Views all receive locally cached data.'}),"\n",(0,a.jsx)(t.p,{children:'To support the use of Monarch, each Screen is implemented by two Widgets: the Widget that calls (for example) WithCoreData in its build method, and then invokes an "Internal" widget with populated instances of ChapterCollection, GardenCollection, and UserCollection. The two-widget structure is necessary in order to support Monarch storybooks, as will be demonstrated later below.'}),"\n",(0,a.jsx)(t.p,{children:"Let's see a simple example of the use of WithCoreData:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"class ChaptersScreen extends StatelessWidget {\n const ChaptersScreen({\n super.key,\n });\n\n @override\n Widget build(BuildContext context) {\n return WithCoreData(whenCoreData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return ChaptersScreenInternal(\n chapters: chapters, gardens: gardens, users: users);\n });\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:'In a nutshell, when the ChaptersScreen widget\'s build method is called, it will call WithCoreData. WithCoreData will retrieve the "core" Chapter, Garden, and User data from Firebase (the first time it is called during a session---after that, the local Riverpod cache of the documents will be used). Note that core data includes documents from a variety of Firebase collections, including chapters, gardens, users, crops, badges.'}),"\n",(0,a.jsx)(t.p,{children:'While this retrieval process is going on, this "With" widget will display the CircularProgressIndicator widget. Once all of the core data is successfully retrieved, then the ChaptersScreenInternal widget\'s build method will called and passed these fully populated collection class instances, and the intended screen UI will appear. If an error occurs during database retrieval, the "With" widget will display a generic error page.'}),"\n",(0,a.jsx)(t.p,{children:'The net effect is that the UI code is insulated from technicalities resulting from the asynchronous nature of data retrieval. It just wraps the code for the "happy path" inside a "With" widget and proceeds. For example, here\'s an elided version of the "Internal" widget:'}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"class ChaptersScreenInternal extends StatelessWidget {\n const ChaptersScreenInternal({\n Key? key,\n required this.chapters,\n required this.gardens,\n required this.users,\n }) : super(key: key);\n final ChapterCollection chapters;\n final GardenCollection gardens;\n final UserCollection users;\n\n @override\n Widget build(BuildContext context) {\n return Scaffold(\n drawer: DrawerView(currentUser: users.currentUser),\n appBar: AppBar(\n title: Text('Chapters (${chapters.size()})'),\n actions: [HelpButton(routeName: AppRoute.chapters.name)],\n ),\n body: ListView(children: [...]),\n bottomNavigationBar: BottomNavigationBar(...),\n );\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"What's nice is that ChaptersScreenInternal is a StatelessWidget that gets passed three collections: Chapters, Gardens, and Users, and this is all the data that it (or any of its component Views) needs to render the Screen."}),"\n",(0,a.jsx)(t.p,{children:'As you look through the code, you will see that Screen widgets generally follow this design pattern: a "top-level" Widget that calls WithCoreData, along with a callback that calls the corresponding "Internal" widget with the three collection classes (and potentially some other data).'}),"\n",(0,a.jsx)(t.h2,{id:"withmonarchdata",children:"WithMonarchData"}),"\n",(0,a.jsx)(t.p,{children:'The decomposition of a Screen into a top-level widget and an internal widget is an important design pattern in ggc_app because it makes it easy to implement Monarch stories. You can do this by writing a story that first calls WithMonarchData, and then calls the "Internal" widget with the collections created by WithMonarchData.'}),"\n",(0,a.jsx)(t.p,{children:"For example:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"Widget showChaptersScreen() {\n return WithMonarchData(whenMonarchData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return ChaptersScreenInternal(\n chapters: chapters, gardens: gardens, users: users);\n });\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"The difference between WithCoreData and WithMonarchData is that WithCoreData builds the Chapters, Gardens, and Users collections by accessing Firestore, while WithMonarchData builds the Chapters, Gardens, and Users collections from sample data stored in the assets/monarch directory."}),"\n",(0,a.jsx)(t.p,{children:"What makes Monarch so useful for UI development is that it makes it really easy to display a UI component in different states. For example, here is an example of displaying the Drawer UI component with data from two different users (one with a profile picture, one who does not):"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"Widget showDrawer() {\n return WithMonarchData(whenMonarchData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return DrawerView(currentUser: users.getUser('jennacorindeane@gmail.com'));\n });\n}\n\nWidget showDrawer2() {\n return WithMonarchData(whenMonarchData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return DrawerView(currentUser: users.getUser('johnson@hawaii.edu'));\n });\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"To view these two states using the emulator, you would have to login and logout multiple times."}),"\n",(0,a.jsxs)(t.admonition,{type:"warning",children:[(0,a.jsx)(t.p,{children:'These "Screen" and "View" design patterns in ggc_app have some important constraints:'}),(0,a.jsxs)(t.ol,{children:["\n",(0,a.jsx)(t.li,{children:'Only Screen widgets call a "With" widget. All View widgets should be passed the Chapter, User, and Garden collections from their parents.'}),"\n",(0,a.jsx)(t.li,{children:'Neither Screen nor View widgets call Riverpod providers. All of the Riverpod providers are called within the "With" widgets.'}),"\n"]})]}),"\n",(0,a.jsx)(t.h2,{id:"withgardendata-etc",children:"WithGardenData, etc"}),"\n",(0,a.jsx)(t.p,{children:'WithCoreData is responsible for retrieving "core" data, which means the data that is necessary to build the initial set of Screens that the user sees after logging in. We don\'t want to retrieve all of the data that the user might ever want to see immediately upon logging in, as that might require the UI to pause for several-to-many seconds, degrading the user experience. Instead, upon logging in, only the minimum "core" data is retrieved from the database so that the wait time is minimal.'}),"\n",(0,a.jsx)(t.p,{children:'Now, consider the situation where the user wants to navigate to the Garden Details screen. This screen will require (among other things) all of the Planting data associated with that specific garden. To retrieve additional data beyond the core data, we will provide additional "With" widgets, of which WithGardenData is an example.'}),"\n",(0,a.jsx)(t.p,{children:"Here's an example invocation of WithGardenData:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"class GardenDetailsScreen extends StatelessWidget {\n const GardenDetailsScreen({Key? key, required this.gardenID})\n : super(key: key);\n\n final String gardenID;\n @override\n Widget build(BuildContext context) {\n return WithGardenData(\n gardenID: gardenID,\n whenGardenData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return GardenDetailsScreenInternal(gardenID: gardenID,\n chapters: chapters, gardens: gardens, users: users);\n });\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"Notice that WithGardenData takes two arguments, a gardenID (used to determine which garden's detailed data to retrieve), plus the standard callback that will be passed filled out instances of ChapterCollection, GardenCollection, and UserCollection. For convenience, the GardenDetailsScreenInternal widget is passed the gardenID as well."}),"\n",(0,a.jsx)(t.p,{children:'It is important to note that "extended" With widgets like WithGardenData call WithCoreData internally, so the resulting collection instances include all of the core data, plus (in this case) the garden details data. As a result, the client code never needs to nest multiple With widgets.'}),"\n",(0,a.jsx)(t.p,{children:"Due to the wonders of Riverpod, data is cached and reactive. The user can navigate away from this garden and return to it later and the system will build the collections from local copies of the data. Even better, Riverpod will keep its local copies in sync with Firebase, so that if other users add data, the current user will see the updates when they redisplay the page."})]})}function c(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>s});var a=n(7294);const r={},i=a.createContext(r);function s(e){const t=a.useContext(i);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),a.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/10921c5b.4d956c8c.js b/assets/js/10921c5b.4d956c8c.js deleted file mode 100644 index cb5d192d7..000000000 --- a/assets/js/10921c5b.4d956c8c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[1744],{4437:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>c,frontMatter:()=>i,metadata:()=>o,toc:()=>d});var a=n(5893),r=n(1151);const i={hide_table_of_contents:!1},s='"With" widgets',o={id:"develop/design/with-widgets",title:'"With" widgets',description:'Why "with"?',source:"@site/docs/develop/design/with-widgets.md",sourceDirName:"develop/design",slug:"/develop/design/with-widgets",permalink:"/docs/develop/design/with-widgets",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"GGC Input Fields",permalink:"/docs/develop/design/input-fields"},next:{title:"Data Mutation",permalink:"/docs/develop/design/data-mutation"}},l={},d=[{value:"Why "with"?",id:"why-with",level:2},{value:"WithMonarchData",id:"withmonarchdata",level:2},{value:"WithGardenData, etc",id:"withgardendata-etc",level:2}];function h(e){const t={admonition:"admonition",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.header,{children:(0,a.jsx)(t.h1,{id:"with-widgets",children:'"With" widgets'})}),"\n",(0,a.jsx)(t.h2,{id:"why-with",children:'Why "with"?'}),"\n",(0,a.jsx)(t.p,{children:'One important issue to address in a client-server architecture is the asynchronous nature of client-server communication. In other words, when the client needs data from the server, it makes a request that can take time to complete, and may not complete successfully. The client UI should not simply "freeze" during this time, and should "fail gracefully" if the request does not complete successfully.'}),"\n",(0,a.jsx)(t.p,{children:'Making things even more complicated is the desire for modern UIs to be "reactive". This means that if the server\'s database content changes (for example, one user creates a new garden), then all the other clients currently connected should see their UI automatically refresh with updated information (for example, the number of gardens in the Chapter should increase by one for all other users.)'}),"\n",(0,a.jsx)(t.p,{children:'Making things yet more complicated in GGC is the desire to support a "Storybook" style design system like Monarch, in which individual Views as well as entire Screens can be displayed with sample data values without requiring a database connection.'}),"\n",(0,a.jsx)(t.p,{children:'In GGC, we address all of these issues through a design pattern that starts with a set of widgets which we call the "With" widgets: WithAllData, WithCoreData, WithGardenData, WithMonarchData, WithObservationData, and so on.'}),"\n",(0,a.jsx)(t.p,{children:'In this design pattern, all of the data that a Widget needs to build a user interface component can always be found somewhere within three "top-level" client-side classes: ChapterCollection, GardenCollection, and UserCollection.'}),"\n",(0,a.jsx)(t.p,{children:'In addition, UI widgets come in two basic flavors, "Screens" and "Views". Screens are a kind of "top-level" UI widget which must take responsibility for building the ChapterCollection, GardenCollection, and UserCollection classes. They accomplish this by invoking a "With" widget. Views are always a "child" of a Screen widget, and must be passed ChapterCollection, GardenCollection, and UserCollection instances from their parent Widget. So, database access always happens at the Screen-level, and from then on the Views all receive locally cached data.'}),"\n",(0,a.jsx)(t.p,{children:'To support the use of Monarch, each Screen is implemented by two Widgets: the Widget that calls (for example) WithCoreData in its build method, and then invokes an "Internal" widget with populated instances of ChapterCollection, GardenCollection, and UserCollection. The two-widget structure is necessary in order to support Monarch storybooks, as will be demonstrated later below.'}),"\n",(0,a.jsx)(t.p,{children:"Let's see a simple example of the use of WithCoreData:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"class ChaptersScreen extends StatelessWidget {\n const ChaptersScreen({\n super.key,\n });\n\n @override\n Widget build(BuildContext context) {\n return WithCoreData(whenCoreData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return ChaptersScreenInternal(\n chapters: chapters, gardens: gardens, users: users);\n });\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:'In a nutshell, when the ChaptersScreen widget\'s build method is called, it will call WithCoreData. WithCoreData will retrieve the "core" Chapter, Garden, and User data from Firebase (the first time it is called during a session---after that, the local Riverpod cache of the documents will be used). Note that core data includes documents from a variety of Firebase collections, including chapters, gardens, users, crops, badges.'}),"\n",(0,a.jsx)(t.p,{children:'While this retrieval process is going on, this "With" widget will display the CircularProgressIndicator widget. Once all of the core data is successfully retrieved, then the ChaptersScreenInternal widget\'s build method will called and passed these fully populated collection class instances, and the intended screen UI will appear. If an error occurs during database retrieval, the "With" widget will display a generic error page.'}),"\n",(0,a.jsx)(t.p,{children:'The net effect is that the UI code is insulated from technicalities resulting from the asynchronous nature of data retrieval. It just wraps the code for the "happy path" inside a "With" widget and proceeds. For example, here\'s an elided version of the "Internal" widget:'}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"class ChaptersScreenInternal extends StatelessWidget {\n const ChaptersScreenInternal({\n Key? key,\n required this.chapters,\n required this.gardens,\n required this.users,\n }) : super(key: key);\n final ChapterCollection chapters;\n final GardenCollection gardens;\n final UserCollection users;\n\n @override\n Widget build(BuildContext context) {\n return Scaffold(\n drawer: DrawerView(currentUser: users.currentUser),\n appBar: AppBar(\n title: Text('Chapters (${chapters.size()})'),\n actions: [HelpButton(routeName: AppRoute.chapters.name)],\n ),\n body: ListView(children: [...]),\n bottomNavigationBar: BottomNavigationBar(...),\n );\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"What's nice is that ChaptersScreenInternal is a StatelessWidget that gets passed three collections: Chapters, Gardens, and Users, and this is all the data that it (or any of its component Views) needs to render the Screen."}),"\n",(0,a.jsx)(t.p,{children:'As you look through the code, you will see that Screen widgets generally follow this design pattern: a "top-level" Widget that calls WithCoreData, along with a callback that calls the corresponding "Internal" widget with the three collection classes (and potentially some other data).'}),"\n",(0,a.jsx)(t.h2,{id:"withmonarchdata",children:"WithMonarchData"}),"\n",(0,a.jsx)(t.p,{children:'The decomposition of a Screen into a top-level widget and an internal widget is an important design pattern in ggc_app because it makes it easy to implement Monarch stories. You can do this by writing a story that first calls WithMonarchData, and then calls the "Internal" widget with the collections created by WithMonarchData.'}),"\n",(0,a.jsx)(t.p,{children:"For example:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"Widget showChaptersScreen() {\n return WithMonarchData(whenMonarchData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return ChaptersScreenInternal(\n chapters: chapters, gardens: gardens, users: users);\n });\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"The difference between WithCoreData and WithMonarchData is that WithCoreData builds the Chapters, Gardens, and Users collections by accessing Firestore, while WithMonarchData builds the Chapters, Gardens, and Users collections from sample data stored in the assets/monarch directory."}),"\n",(0,a.jsx)(t.p,{children:"What makes Monarch so useful for UI development is that it makes it really easy to display a UI component in different states. For example, here is an example of displaying the Drawer UI component with data from two different users (one with a profile picture, one who does not):"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"Widget showDrawer() {\n return WithMonarchData(whenMonarchData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return DrawerView(currentUser: users.getUser('jennacorindeane@gmail.com'));\n });\n}\n\nWidget showDrawer2() {\n return WithMonarchData(whenMonarchData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return DrawerView(currentUser: users.getUser('johnson@hawaii.edu'));\n });\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"To view these two states using the emulator, you would have to login and logout multiple times."}),"\n",(0,a.jsxs)(t.admonition,{type:"warning",children:[(0,a.jsx)(t.p,{children:'These "Screen" and "View" design patterns in ggc_app have some important constraints:'}),(0,a.jsxs)(t.ol,{children:["\n",(0,a.jsx)(t.li,{children:'Only Screen widgets call a "With" widget. All View widgets should be passed the Chapter, User, and Garden collections from their parents.'}),"\n",(0,a.jsx)(t.li,{children:'Neither Screen nor View widgets call Riverpod providers. All of the Riverpod providers are called within the "With" widgets.'}),"\n"]})]}),"\n",(0,a.jsx)(t.h2,{id:"withgardendata-etc",children:"WithGardenData, etc"}),"\n",(0,a.jsx)(t.p,{children:'WithCoreData is responsible for retrieving "core" data, which means the data that is necessary to build the initial set of Screens that the user sees after logging in. We don\'t want to retrieve all of the data that the user might ever want to see immediately upon logging in, as that might require the UI to pause for several-to-many seconds, degrading the user experience. Instead, upon logging in, only the minimum "core" data is retrieved from the database so that the wait time is minimal.'}),"\n",(0,a.jsx)(t.p,{children:'Now, consider the situation where the user wants to navigate to the Garden Details screen. This screen will require (among other things) all of the Planting data associated with that specific garden. To retrieve additional data beyond the core data, we will provide additional "With" widgets, of which WithGardenData is an example.'}),"\n",(0,a.jsx)(t.p,{children:"Here's an example invocation of WithGardenData:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"class GardenDetailsScreen extends StatelessWidget {\n const GardenDetailsScreen({Key? key, required this.gardenID})\n : super(key: key);\n\n final String gardenID;\n @override\n Widget build(BuildContext context) {\n return WithGardenData(\n gardenID: gardenID,\n whenGardenData: (\n {required ChapterCollection chapters,\n required GardenCollection gardens,\n required UserCollection users}) {\n return GardenDetailsScreenInternal(gardenID: gardenID,\n chapters: chapters, gardens: gardens, users: users);\n });\n }\n}\n"})}),"\n",(0,a.jsx)(t.p,{children:"Notice that WithGardenData takes two arguments, a gardenID (used to determine which garden's detailed data to retrieve), plus the standard callback that will be passed filled out instances of ChapterCollection, GardenCollection, and UserCollection. For convenience, the GardenDetailsScreenInternal widget is passed the gardenID as well."}),"\n",(0,a.jsx)(t.p,{children:'It is important to note that "extended" With widgets like WithGardenData call WithCoreData internally, so the resulting collection instances include all of the core data, plus (in this case) the garden details data. As a result, the client code never needs to nest multiple With widgets.'}),"\n",(0,a.jsx)(t.p,{children:"Due to the wonders of Riverpod, data is cached and reactive. The user can navigate away from this garden and return to it later and the system will build the collections from local copies of the data. Even better, Riverpod will keep its local copies in sync with Firebase, so that if other users add data, the current user will see the updates when they redisplay the page."})]})}function c(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>s});var a=n(7294);const r={},i=a.createContext(r);function s(e){const t=a.useContext(i);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),a.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/32cdd552.4c0ae06e.js b/assets/js/32cdd552.4c0ae06e.js deleted file mode 100644 index 713bad1ea..000000000 --- a/assets/js/32cdd552.4c0ae06e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[6900],{8608:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>d,contentTitle:()=>o,default:()=>l,frontMatter:()=>s,metadata:()=>r,toc:()=>h});var n=a(5893),i=a(1151);const s={hide_table_of_contents:!0},o="Managing Firebase data",r={id:"develop/managing-firebase-data",title:"Managing Firebase data",description:"We use a Firebase database to store the data associated with GGC. There are several important issues associated with managing Firebase data.",source:"@site/docs/develop/managing-firebase-data.md",sourceDirName:"develop",slug:"/develop/managing-firebase-data",permalink:"/docs/develop/managing-firebase-data",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!0},sidebar:"developSidebar",previous:{title:"Architecture",permalink:"/docs/develop/architecture"},next:{title:"Deployment",permalink:"/docs/develop/deployment"}},d={},h=[{value:"1. Is the database consistent?",id:"1-is-the-database-consistent",level:2},{value:"2. How do we make it consistent?",id:"2-how-do-we-make-it-consistent",level:2},{value:"2a. Updating Firebase: via the Console",id:"2a-updating-firebase-via-the-console",level:3},{value:"2b. Updating Firebase: via Database Operation",id:"2b-updating-firebase-via-database-operation",level:3},{value:"3. Is the database appropriate?",id:"3-is-the-database-appropriate",level:2}];function c(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",...(0,i.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"managing-firebase-data",children:"Managing Firebase data"})}),"\n",(0,n.jsx)(t.p,{children:"We use a Firebase database to store the data associated with GGC. There are several important issues associated with managing Firebase data."}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsx)(t.li,{children:'Is the database in a "consistent" state?'}),"\n",(0,n.jsx)(t.li,{children:"If the database is inconsistent, how do we update the database to get it to a consistent state?"}),"\n",(0,n.jsx)(t.li,{children:'Is the database "appropriate"? Does the current database representation satisfy the needs of the customer?'}),"\n"]}),"\n",(0,n.jsx)(t.h2,{id:"1-is-the-database-consistent",children:"1. Is the database consistent?"}),"\n",(0,n.jsx)(t.p,{children:'"Consistent" means that the data does not contain missing or incorrect values.'}),"\n",(0,n.jsxs)(t.p,{children:['This is a "quality assurance issue", which we address through the use of the ',(0,n.jsx)(t.a,{href:"/docs/develop/integrity-check",children:"Database Integrity Check"}),"."]}),"\n",(0,n.jsx)(t.h2,{id:"2-how-do-we-make-it-consistent",children:"2. How do we make it consistent?"}),"\n",(0,n.jsx)(t.p,{children:"There are basically two approaches to answering this question: either via the Firebase console or by using our Database Operation feature."}),"\n",(0,n.jsx)(t.h3,{id:"2a-updating-firebase-via-the-console",children:"2a. Updating Firebase: via the Console"}),"\n",(0,n.jsx)(t.p,{children:"If the inconsistency is minor and affects only a few documents, then a reasonable approach is to use the Firebase console:"}),"\n",(0,n.jsx)("img",{src:"/img/develop/firestore/firebase-console.png"}),"\n",(0,n.jsx)(t.p,{children:"The Firebase console enables you to edit, create, or delete any document, as well as search for documents satisfying a criteria."}),"\n",(0,n.jsx)(t.h3,{id:"2b-updating-firebase-via-database-operation",children:"2b. Updating Firebase: via Database Operation"}),"\n",(0,n.jsx)(t.p,{children:'Sometimes the inconsistency is not minor, and requires manipulation of dozens or hundreds of documents. This would be super painful to fix using the console. For these situations, we\'ve developed an Admin command called "Database Operation". It allows you to programmatically inspect all documents in the database, decide what to create, modify, or delete, and then invoke the appropriate mutation.'}),"\n",(0,n.jsx)(t.p,{children:"To implement a programmatic update using Database Operation, you must first implement a subclass of DatabaseOperation. For example, here is a subclass that iterates through all observations and finds some that need to be updated:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",children:"class DatabaseOperation19 extends DatabaseOperation {\n DatabaseOperation19(\n {required super.chapters,\n required super.gardens,\n required super.users,\n super.description =\n 'Fix all observations to refer to the \"Unknown\" variety, not the empty string'});\n\n @override\n void setup() {\n List observationsToSet = [];\n for (Observation observation in chapters.observations.observations) {\n if (!chapters.varieties.isVarietyID(observation.cachedVarietyID)) {\n String cropID = observation.cachedCropID;\n Variety unknownVariety =\n chapters.crops.getUnknownVariety(chapters, cropID);\n logger.d('Setting varietyID for ${observation} to $unknownVariety');\n Observation updatedObservation = observation.copyWith(\n cachedVarietyName: unknownVariety.name,\n cachedVarietyID: unknownVariety.varietyID,\n );\n observationsToSet.add(updatedObservation);\n }\n }\n\n data.observationsToSet = observationsToSet;\n }\n}\n"})}),"\n",(0,n.jsx)(t.p,{children:"When the simulator is run with this operation specified as the one to invoke in the DatabaseOperationScreen widget, then navigating to the Database Operation screen in the Admin panel might look like this:"}),"\n",(0,n.jsx)("img",{width:"500px",src:"/img/develop/firestore/db-operation.png"}),"\n",(0,n.jsx)(t.p,{children:"What's cool about the implementation of Database Operation is that when you navigate to the screen, it will tell you what it's going to do if you hit the \"Invoke Operation\" button. In this example, it will update 145 Observation documents."}),"\n",(0,n.jsx)(t.p,{children:'To do this, the setup() method is called when you visit the page, and its task is to figure out all the documents that need to be updated and then update the appropriate field in the "data" instance. This enables the page to provide feedback on how many entities of what type are going to be changed if you actually invoke the operation. (You can also use logger statements to get additional info on what the operation will do.)'}),"\n",(0,n.jsx)(t.h2,{id:"3-is-the-database-appropriate",children:"3. Is the database appropriate?"}),"\n",(0,n.jsx)(t.p,{children:"The answer is, more often than not, no. Database evolution is a continuing part of system development and enhancement. So, we must often: (a) update our entity representations, (b) update our UI, (c) update our tests, and (d) use the Database Operation command to migrate the existing data to the new representation."})]})}function l(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(c,{...e})}):c(e)}},1151:(e,t,a)=>{a.d(t,{Z:()=>r,a:()=>o});var n=a(7294);const i={},s=n.createContext(i);function o(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/32cdd552.bf0ad874.js b/assets/js/32cdd552.bf0ad874.js new file mode 100644 index 000000000..8a78c8b14 --- /dev/null +++ b/assets/js/32cdd552.bf0ad874.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[6900],{8608:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>d,contentTitle:()=>o,default:()=>l,frontMatter:()=>s,metadata:()=>r,toc:()=>h});var n=a(5893),i=a(1151);const s={hide_table_of_contents:!0},o="Managing Firebase data",r={id:"develop/managing-firebase-data",title:"Managing Firebase data",description:"We use a Firebase database to store the data associated with GGC. There are several important issues associated with managing Firebase data.",source:"@site/docs/develop/managing-firebase-data.md",sourceDirName:"develop",slug:"/develop/managing-firebase-data",permalink:"/docs/develop/managing-firebase-data",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!0},sidebar:"developSidebar",previous:{title:"Data Model",permalink:"/docs/develop/data-model"},next:{title:"Deployment",permalink:"/docs/develop/deployment"}},d={},h=[{value:"1. Is the database consistent?",id:"1-is-the-database-consistent",level:2},{value:"2. How do we make it consistent?",id:"2-how-do-we-make-it-consistent",level:2},{value:"2a. Updating Firebase: via the Console",id:"2a-updating-firebase-via-the-console",level:3},{value:"2b. Updating Firebase: via Database Operation",id:"2b-updating-firebase-via-database-operation",level:3},{value:"3. Is the database appropriate?",id:"3-is-the-database-appropriate",level:2}];function c(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",...(0,i.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"managing-firebase-data",children:"Managing Firebase data"})}),"\n",(0,n.jsx)(t.p,{children:"We use a Firebase database to store the data associated with GGC. There are several important issues associated with managing Firebase data."}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsx)(t.li,{children:'Is the database in a "consistent" state?'}),"\n",(0,n.jsx)(t.li,{children:"If the database is inconsistent, how do we update the database to get it to a consistent state?"}),"\n",(0,n.jsx)(t.li,{children:'Is the database "appropriate"? Does the current database representation satisfy the needs of the customer?'}),"\n"]}),"\n",(0,n.jsx)(t.h2,{id:"1-is-the-database-consistent",children:"1. Is the database consistent?"}),"\n",(0,n.jsx)(t.p,{children:'"Consistent" means that the data does not contain missing or incorrect values.'}),"\n",(0,n.jsxs)(t.p,{children:['This is a "quality assurance issue", which we address through the use of the ',(0,n.jsx)(t.a,{href:"/docs/develop/integrity-check",children:"Database Integrity Check"}),"."]}),"\n",(0,n.jsx)(t.h2,{id:"2-how-do-we-make-it-consistent",children:"2. How do we make it consistent?"}),"\n",(0,n.jsx)(t.p,{children:"There are basically two approaches to answering this question: either via the Firebase console or by using our Database Operation feature."}),"\n",(0,n.jsx)(t.h3,{id:"2a-updating-firebase-via-the-console",children:"2a. Updating Firebase: via the Console"}),"\n",(0,n.jsx)(t.p,{children:"If the inconsistency is minor and affects only a few documents, then a reasonable approach is to use the Firebase console:"}),"\n",(0,n.jsx)("img",{src:"/img/develop/firestore/firebase-console.png"}),"\n",(0,n.jsx)(t.p,{children:"The Firebase console enables you to edit, create, or delete any document, as well as search for documents satisfying a criteria."}),"\n",(0,n.jsx)(t.h3,{id:"2b-updating-firebase-via-database-operation",children:"2b. Updating Firebase: via Database Operation"}),"\n",(0,n.jsx)(t.p,{children:'Sometimes the inconsistency is not minor, and requires manipulation of dozens or hundreds of documents. This would be super painful to fix using the console. For these situations, we\'ve developed an Admin command called "Database Operation". It allows you to programmatically inspect all documents in the database, decide what to create, modify, or delete, and then invoke the appropriate mutation.'}),"\n",(0,n.jsx)(t.p,{children:"To implement a programmatic update using Database Operation, you must first implement a subclass of DatabaseOperation. For example, here is a subclass that iterates through all observations and finds some that need to be updated:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-dart",children:"class DatabaseOperation19 extends DatabaseOperation {\n DatabaseOperation19(\n {required super.chapters,\n required super.gardens,\n required super.users,\n super.description =\n 'Fix all observations to refer to the \"Unknown\" variety, not the empty string'});\n\n @override\n void setup() {\n List observationsToSet = [];\n for (Observation observation in chapters.observations.observations) {\n if (!chapters.varieties.isVarietyID(observation.cachedVarietyID)) {\n String cropID = observation.cachedCropID;\n Variety unknownVariety =\n chapters.crops.getUnknownVariety(chapters, cropID);\n logger.d('Setting varietyID for ${observation} to $unknownVariety');\n Observation updatedObservation = observation.copyWith(\n cachedVarietyName: unknownVariety.name,\n cachedVarietyID: unknownVariety.varietyID,\n );\n observationsToSet.add(updatedObservation);\n }\n }\n\n data.observationsToSet = observationsToSet;\n }\n}\n"})}),"\n",(0,n.jsx)(t.p,{children:"When the simulator is run with this operation specified as the one to invoke in the DatabaseOperationScreen widget, then navigating to the Database Operation screen in the Admin panel might look like this:"}),"\n",(0,n.jsx)("img",{width:"500px",src:"/img/develop/firestore/db-operation.png"}),"\n",(0,n.jsx)(t.p,{children:"What's cool about the implementation of Database Operation is that when you navigate to the screen, it will tell you what it's going to do if you hit the \"Invoke Operation\" button. In this example, it will update 145 Observation documents."}),"\n",(0,n.jsx)(t.p,{children:'To do this, the setup() method is called when you visit the page, and its task is to figure out all the documents that need to be updated and then update the appropriate field in the "data" instance. This enables the page to provide feedback on how many entities of what type are going to be changed if you actually invoke the operation. (You can also use logger statements to get additional info on what the operation will do.)'}),"\n",(0,n.jsx)(t.h2,{id:"3-is-the-database-appropriate",children:"3. Is the database appropriate?"}),"\n",(0,n.jsx)(t.p,{children:"The answer is, more often than not, no. Database evolution is a continuing part of system development and enhancement. So, we must often: (a) update our entity representations, (b) update our UI, (c) update our tests, and (d) use the Database Operation command to migrate the existing data to the new representation."})]})}function l(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(c,{...e})}):c(e)}},1151:(e,t,a)=>{a.d(t,{Z:()=>r,a:()=>o});var n=a(7294);const i={},s=n.createContext(i);function o(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/3ad77611.133c0910.js b/assets/js/3ad77611.133c0910.js new file mode 100644 index 000000000..effa29fc5 --- /dev/null +++ b/assets/js/3ad77611.133c0910.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[403],{9657:(e,r,i)=>{i.r(r),i.d(r,{assets:()=>l,contentTitle:()=>s,default:()=>c,frontMatter:()=>n,metadata:()=>d,toc:()=>h});var t=i(5893),a=i(1151);const n={hide_table_of_contents:!1},s="Badges",d={id:"develop/design/badges",title:"Badges",description:"Goals",source:"@site/docs/develop/design/badges.md",sourceDirName:"develop/design",slug:"/develop/design/badges",permalink:"/docs/develop/design/badges",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Features",permalink:"/docs/develop/design/features"},next:{title:"Input Fields",permalink:"/docs/develop/design/input-fields"}},l={},h=[{value:"Goals",id:"goals",level:2},{value:"Design principles",id:"design-principles",level:2},{value:"Types",id:"types",level:3},{value:"Levels",id:"levels",level:3},{value:"Verification (i.e. badge processing)",id:"verification-ie-badge-processing",level:3},{value:"Observation tags",id:"observation-tags",level:3},{value:"Implementation hints",id:"implementation-hints",level:3},{value:"Garden badges",id:"garden-badges",level:2},{value:"Pesticide free",id:"pesticide-free",level:3},{value:"General Criteria",id:"general-criteria",level:4},{value:"Observation tags",id:"observation-tags-1",level:4},{value:"Implementation notes",id:"implementation-notes",level:4},{value:"Pollinator Friendly",id:"pollinator-friendly",level:3},{value:"General Criteria",id:"general-criteria-1",level:4},{value:"Observation tags",id:"observation-tags-2",level:4},{value:"Implementation notes",id:"implementation-notes-1",level:4},{value:"Sustainable Soil",id:"sustainable-soil",level:3},{value:"General Criteria",id:"general-criteria-2",level:4},{value:"Observation tags",id:"observation-tags-3",level:4},{value:"Implementation notes",id:"implementation-notes-2",level:4},{value:"Water Smart",id:"water-smart",level:3},{value:"General Criteria",id:"general-criteria-3",level:4},{value:"Observation tags",id:"observation-tags-4",level:4},{value:"Implementation notes",id:"implementation-notes-3",level:4},{value:"Gardener badges",id:"gardener-badges",level:2},{value:"Community Cultivator",id:"community-cultivator",level:3},{value:"General Criteria",id:"general-criteria-4",level:4},{value:"Observation tags:",id:"observation-tags-5",level:4},{value:"Implementation notes",id:"implementation-notes-4",level:4},{value:"Compost Champion",id:"compost-champion",level:3},{value:"General Criteria",id:"general-criteria-5",level:4},{value:"Observation tags",id:"observation-tags-6",level:4},{value:"Implementation notes",id:"implementation-notes-5",level:4},{value:"Crop Whisperer",id:"crop-whisperer",level:3},{value:"General Criteria",id:"general-criteria-6",level:4},{value:"Observation tags",id:"observation-tags-7",level:4},{value:"Implementation notes",id:"implementation-notes-6",level:4},{value:"Greenhouse grower",id:"greenhouse-grower",level:3},{value:"General Criteria",id:"general-criteria-7",level:4},{value:"Observation tags",id:"observation-tags-8",level:4},{value:"Implementation notes",id:"implementation-notes-7",level:4},{value:"Permaculture Pro",id:"permaculture-pro",level:3},{value:"General Criteria",id:"general-criteria-8",level:4},{value:"Observation tags",id:"observation-tags-9",level:4},{value:"Implementation notes",id:"implementation-notes-8",level:4},{value:"Vermiculturalist",id:"vermiculturalist",level:3},{value:"General Criteria",id:"general-criteria-9",level:4},{value:"Observation tags:",id:"observation-tags-10",level:4},{value:"Implementation notes",id:"implementation-notes-9",level:4},{value:"Seed Saver",id:"seed-saver",level:3},{value:"General Criteria",id:"general-criteria-10",level:4},{value:"Observation tags:",id:"observation-tags-11",level:4},{value:"Implementation notes",id:"implementation-notes-10",level:4},{value:"Post-1.0 badges",id:"post-10-badges",level:2},{value:"Chapter Chair",id:"chapter-chair",level:3},{value:"General Criteria",id:"general-criteria-11",level:4},{value:"Connected Community",id:"connected-community",level:3},{value:"General Criteria",id:"general-criteria-12",level:4},{value:"Climate Victors",id:"climate-victors",level:3},{value:"General Criteria",id:"general-criteria-13",level:4},{value:"Pesticide Resistors",id:"pesticide-resistors",level:3},{value:"General Criteria",id:"general-criteria-14",level:4},{value:"Seed Sharers",id:"seed-sharers",level:3},{value:"General Criteria:",id:"general-criteria-15",level:4},{value:"Climate Victory",id:"climate-victory",level:3},{value:"General Criteria",id:"general-criteria-16",level:4},{value:"Observation tags",id:"observation-tags-12",level:4},{value:"Master gardener",id:"master-gardener",level:3},{value:"General Criteria",id:"general-criteria-17",level:4},{value:"Observation tags",id:"observation-tags-13",level:4},{value:"Bee Buddy",id:"bee-buddy",level:3},{value:"General Criteria",id:"general-criteria-18",level:4},{value:"Observation tags",id:"observation-tags-14",level:4},{value:"Aquaponics Ace",id:"aquaponics-ace",level:3},{value:"General Criteria",id:"general-criteria-19",level:4},{value:"Observation tags",id:"observation-tags-15",level:4},{value:"Herbalist Hero",id:"herbalist-hero",level:3},{value:"General Criteria",id:"general-criteria-20",level:4},{value:"Observation tags:",id:"observation-tags-16",level:4},{value:"Educator Extraordinaire",id:"educator-extraordinaire",level:3},{value:"General Criteria",id:"general-criteria-21",level:4},{value:"Observation tags:",id:"observation-tags-17",level:4},{value:"Orchard Orchestrator",id:"orchard-orchestrator",level:3},{value:"General Criteria",id:"general-criteria-22",level:4}];function o(e){const r={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",ol:"ol",p:"p",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(r.header,{children:(0,t.jsx)(r.h1,{id:"badges",children:"Badges"})}),"\n",(0,t.jsx)(r.h2,{id:"goals",children:"Goals"}),"\n",(0,t.jsx)(r.p,{children:"The 1.0 release implements a badge system for gardens and gardeners. This badge system is designed to accomplish the following goals:"}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.em,{children:"Foster user engagement and enjoyment through a game mechanic that publicizes achievements by gardens and gardeners."})," Gardeners should find it fun to accumulate badges that are associated with their profile and their garden(s)."]}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.em,{children:"Foster a community of practice by helping gardeners connect with others with similar interests and/or greater expertise with respect to a specific gardening topic."})," For example, if a user is interested in vermiculture, the badge system provides a mechanism for them to find other gardeners who already have experience in this area."]}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.em,{children:"Provide a useful, compact representation of garden and gardener characteristics."}),' The app provides "summary" cards for gardens and gardeners. Users should find the presence (and/or absence) of badges helpful in forming a high level understanding of these entities.']}),"\n",(0,t.jsxs)(r.li,{children:[(0,t.jsx)(r.em,{children:"Provide a mechanism that identifies ways to improve gardening practices."}),' The badge system makes visible the practices that are important to the GGC mission of food resiliency and sustainable gardening, such as seed saving, composting, and water conservation. This means that a simple heuristic for "getting better at gardening" is to simply "get more badges".']}),"\n"]}),"\n",(0,t.jsxs)(r.admonition,{title:"What about Chapters?",type:"info",children:[(0,t.jsx)(r.p,{children:"The 1.0 release will not implement Chapter-level badges for two reasons:"}),(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsx)(r.li,{children:'The primary goals for Chapter-level badges are: (a) encouraging members of a Chapter via "peer pressure" to conform to certain best practices, and (b) making possible Chapter "leaderboards" so that Chapters can assess their capabilities relative to other chapters. Neither of these are important for the 1.0 release where we will focus on a single Chapter with a relatively small number of members.'}),"\n",(0,t.jsx)(r.li,{children:"We will implement garden and gardener-based badge processing within each client, which is simple, scalable, and (hopefully) efficient. Implementing a Chapter-level badge system at scale will require Firebase cloud functions. These functions require specialized knowledge to implement correctly."}),"\n"]})]}),"\n",(0,t.jsx)(r.h2,{id:"design-principles",children:"Design principles"}),"\n",(0,t.jsx)(r.h3,{id:"types",children:"Types"}),"\n",(0,t.jsx)(r.p,{children:"The 1.0 release will implement two types of badges: garden badges and gardener badges. Garden badges reflect the characteristics of a garden across one or more years. Gardener badges reflect characteristics of a gardener across all of the gardens with which they are associated."}),"\n",(0,t.jsx)(r.h3,{id:"levels",children:"Levels"}),"\n",(0,t.jsx)(r.p,{children:"Each badge can be achieved at three levels of increasing sophistication and/or expertise. Level 1 badges are relatively easy to achieve. Level 2 and Level 3 badges indicate increasing levels of expertise or accomplishment with respect to the badge subject."}),"\n",(0,t.jsx)(r.p,{children:"Levels will be visually represented by 1-3 stars along the left side of the badge. Here`s an example:"}),"\n",(0,t.jsx)("img",{style:{borderStyle:"solid"},width:"300px",src:"/img/develop/release-1.0/badges/badge-examples.png"}),"\n",(0,t.jsx)(r.h3,{id:"verification-ie-badge-processing",children:"Verification (i.e. badge processing)"}),"\n",(0,t.jsx)(r.p,{children:'Verification of badges can be done in the following ways: "via attestation", "via observation", or "via planting". Depending upon the badge and/or level, one or more of these verification approaches might be required.'}),"\n",(0,t.jsx)(r.p,{children:'"Via attestation" means that the Gardener (owner) has simply attested that they (or their garden) adheres to certain practices. This is implemented as an "Attestation" section in the Garden and Gardener forms. For example, when creating or updating a Garden, the gardener (owner) can simply check a box to attest that the garden is pesticide-free. Gardeners are on the honor system to attest only to practices that they believe to be true.'}),"\n",(0,t.jsx)(r.p,{children:'"Via Observation" requires the Gardener (owner or editor) to post one or more Observations with one or more badge-specific tags in a single Garden.'}),"\n",(0,t.jsx)(r.p,{children:'"Via Planting" requires the Gardener (owner) to have created Planting data in a single Garden that helps to satisfy the criteria for a badge.'}),"\n",(0,t.jsxs)(r.admonition,{title:"1.0 release badge processing is client-side only",type:"warning",children:[(0,t.jsx)(r.p,{children:"As the above indicates, for the 1.0 release, badge processing occurs on the client-side, and is triggered by updates to garden, gardener, observation, or planting documents."}),(0,t.jsx)(r.p,{children:'The current criteria are designed so that they can be assessed via either WithCoreData or WithGardenData. See the Implementation Notes section associated with each badge for an indication of which "With" widget can be used.'}),(0,t.jsx)(r.p,{children:"There are many ways we could define the criteria for a badge. The criteria we choose must align with the 1.0 release design constraints. If a criteria turns out to be too expensive to verify via client-side processing, then we should change the criteria, not change the design."})]}),"\n",(0,t.jsx)(r.h3,{id:"observation-tags",children:"Observation tags"}),"\n",(0,t.jsx)(r.p,{children:"Many badges require the posting of (public) Observations to provide evidence for a specific practice. To implement badge processing, the system needs to be able to identify Observations that are intended to support achievement of a particular badge. This will be done by the user attaching one or more pre-defined tags to an Observation. Some practices (i.e. cover crops) can help the user achieve multiple badges, so the tag labels are not designed to indicate any particular badge."}),"\n",(0,t.jsx)(r.p,{children:'The system will "take the user\'s word" for the appropriateness of the tags to the Observation. We hope that the public nature of these observations will prevent users from misusing this process.'}),"\n",(0,t.jsx)(r.h3,{id:"implementation-hints",children:"Implementation hints"}),"\n",(0,t.jsx)(r.p,{children:"Badge processing has the following general implementation characteristics:"}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsx)(r.li,{children:'Triggered as part of "mutation" of Gardener, Garden, Planting, and Observation entities.'}),"\n",(0,t.jsx)(r.li,{children:"During submit() processing, the BadgeProcessor is called with the Garden, Chapter, and User collections, plus the entities about to be mutated. It then calls a function for each Badge, passing it this data. Each badge-specific function has its own function returns the set of BadgeInstances to be created and deleted."}),"\n"]}),"\n",(0,t.jsx)(r.p,{children:"Badge implementation also involves the creation of the Badges page. This page should provide a description of each Badge, the criteria required to obtain each level, and chips to indicate the Gardens (or Gardeners) that currently hold the badge."}),"\n",(0,t.jsx)(r.p,{children:'Badge implementation will also require updates to the Garden and Gardener entities and mutation processing in order to support the various "attestation" checkboxes. Each attestation can be implemented as an optional boolean field in the Garden or Gardener entity.'}),"\n",(0,t.jsx)(r.h2,{id:"garden-badges",children:"Garden badges"}),"\n",(0,t.jsx)(r.p,{children:"Here are proposals for the 1.0 release garden badges."}),"\n",(0,t.jsx)(r.h3,{id:"pesticide-free",children:"Pesticide free"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"No pesticides are used in this garden."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-1",children:"Observation tags"}),"\n",(0,t.jsx)(r.p,{children:"N/A"}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsxs)(r.td,{children:["a. The user has attested that the Garden is pesticide free. ",(0,t.jsx)("br",{})," b. There is Planting data for this garden for a single calendar year."]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsxs)(r.td,{children:["a. The user has attested that the Garden is pesticide free. ",(0,t.jsx)("br",{})," b. There is Planting data for this garden for exactly two calendar years."]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsxs)(r.td,{children:["a. The user has attested that the Garden is pesticide free. ",(0,t.jsx)("br",{})," b. There is Planting data for this garden for three or more calendar years."]})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Garden or Planting mutation."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.h3,{id:"pollinator-friendly",children:"Pollinator Friendly"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-1",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:'The garden has pollinator-friendly practices such as: (1) Using a wide variety of plants that bloom from early spring into late fall, (2) Avoiding modern hybrid flowers, especially those with "doubled" flowers, (3) Eliminating pesticides whenever possible, (4) Including larval host plants in your landscape, (5) Creating a damp salt lick for butterflies and bees, (6) Leaving dead trees, or at least an occasional dead limb, in order to provide essential nesting sites for native bees, and (7) Adding to nectar resources by providing a hummingbird feeder.'}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-2",children:"Observation tags"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#DitchChemicals"}),", ",(0,t.jsx)(r.code,{children:"#Habitat"}),", ",(0,t.jsx)(r.code,{children:"#Hummingbirds"}),", ",(0,t.jsx)(r.code,{children:"#LarvalHostPlants"}),", ",(0,t.jsx)(r.code,{children:"#NativeBees"}),", ",(0,t.jsx)(r.code,{children:"#NativePlants"}),", ",(0,t.jsx)(r.code,{children:"#PesticideFree"}),", ",(0,t.jsx)(r.code,{children:"#SaltLick"}),"."]}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices within a single calendar year."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices for two calendar years."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices for three or more calendar years."})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-1",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Observation mutation."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.h3,{id:"sustainable-soil",children:"Sustainable Soil"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-2",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"Garden soil has been improved by using sheet mulch, compost, and/or cover crops."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-3",children:"Observation tags"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#Compost"}),", ",(0,t.jsx)(r.code,{children:"#CoverCrops"}),", ",(0,t.jsx)(r.code,{children:"#SheetMulch"}),", ",(0,t.jsx)(r.code,{children:"#Mulch"}),", ",(0,t.jsx)(r.code,{children:"#CropRotation"})]}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices within a single calendar year."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices for two calendar years."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices for three or more calendar years."})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-2",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Observation mutation."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.h3,{id:"water-smart",children:"Water Smart"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-3",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The garden involves water conservation practices, including: (1) collecting and using rainwater; (2) drip irrigation or soaker hoses, or (3) timers to water during cooler parts of day to minimize water use."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-4",children:"Observation tags"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#DripIrrigation"}),", ",(0,t.jsx)(r.code,{children:"#Rainwater"}),", ",(0,t.jsx)(r.code,{children:"#WaterTimer"}),"."]}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices within a single calendar year."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for two calendar years."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for three or more calendar years."})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-3",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Observation mutation."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.h2,{id:"gardener-badges",children:"Gardener badges"}),"\n",(0,t.jsx)(r.p,{children:"Here are proposals for the 1.0 release Gardener badges."}),"\n",(0,t.jsx)(r.h3,{id:"community-cultivator",children:"Community Cultivator"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-4",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has demonstrated experience with community and/or school gardening."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-5",children:"Observation tags:"}),"\n",(0,t.jsx)(r.p,{children:"N/A"}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:'a. The gardener is associated with exactly one garden which has the "Community or School Garden" attestation.'})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:'a. The gardener is associated with exactly two gardens that have the "Community or School Garden" attestation.'})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:'a. The gardener is associated with three or more gardens that have the "Community or School Garden" attestation.'})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-4",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Garden and Gardener mutation."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithCoreData."}),"\n",(0,t.jsx)(r.h3,{id:"compost-champion",children:"Compost Champion"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-5",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has experience composting in a gardens."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-6",children:"Observation tags"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#Compost"}),", ",(0,t.jsx)(r.code,{children:"#CompostTea"}),", ",(0,t.jsx)(r.code,{children:"#Hugelkulture"}),", ",(0,t.jsx)(r.code,{children:"#Vermiculture"}),", ",(0,t.jsx)(r.code,{children:"#Worms"}),"."]}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. The gardener (owner or editor) has posted Observations indicating at least one of the practices for a single calendar year in a garden."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. The gardener (owner or editor) has posted Observations indicating at least one of the practices for two calendar years in a single garden."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. The gardener (owner or editor) has posted Observations indicating at least one of the practices for three or more calendar years in a single garden."})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-5",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Observation mutation."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.p,{children:'Note that the gardener cannot get to levels 2 or 3 by "switching" among different gardens. The postings must be from the same garden. This means WithGardenData is enough to evaluate the criteria.'}),"\n",(0,t.jsx)(r.p,{children:'Also, the gardener must make the Observations themselves. They can\'t "passively" obtain the badge because someone else in the Garden made Observations with the appropriate tags.'}),"\n",(0,t.jsx)(r.h3,{id:"crop-whisperer",children:"Crop Whisperer"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-6",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has demonstrated expertise in growing a specific crop in a single garden."}),"\n",(0,t.jsx)(r.admonition,{title:"Multiple Badge Alert!",type:"info",children:(0,t.jsx)(r.p,{children:'Unlike other badges, this badge is crop-specific, and so a gardener can earn multiple Crop Whisperer badges ("Bean Whisperer", "Cucumber Whisperer")'})}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-7",children:"Observation tags"}),"\n",(0,t.jsx)(r.p,{children:"N/A"}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsxs)(r.td,{children:["a. There are Plantings for exactly three different varieties of the same crop. ",(0,t.jsx)("br",{})," b. At least two outcomes were awarded at least three stars in at least one Planting."]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsxs)(r.td,{children:["a. There are Plantings for exactly four different varieties of the same crop. ",(0,t.jsx)("br",{})," b. At least two outcomes were awarded at least three stars in at least one Planting."]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsxs)(r.td,{children:["a. There are Plantings for at least five different varieties of the same crop. ",(0,t.jsx)("br",{})," b. At least two outcomes were awarded at least three stars in at least one Planting."]})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-6",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Planting mutation."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.h3,{id:"greenhouse-grower",children:"Greenhouse grower"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-7",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has experience growing plants successfully in a greenhouse."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-8",children:"Observation tags"}),"\n",(0,t.jsx)(r.p,{children:"N/A."}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. There is a single Planting in a single Garden that was started in a greenhouse that survived to harvest and was awarded at least three stars for at least one outcomes."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. There are two Plantings in a single Garden that were started in a greenhouse that survived to harvest and were awarded at least three stars for at least one outcomes."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. There are three Plantings in a single Garden that were started in a greenhouse that survived to harvest and were awarded at least three stars for at least one outcomes."})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-7",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Planting mutation."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.h3,{id:"permaculture-pro",children:"Permaculture Pro"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-8",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has completed a Permaculture workshop to learn about the philosophy of permaculture and is also associated with garden(s) that have achieved permaculture-related badges"}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-9",children:"Observation tags"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#PesticideFree"}),", ",(0,t.jsx)(r.code,{children:"#SustainableSoil"}),", ",(0,t.jsx)(r.code,{children:"#WaterSmart"}),", ",(0,t.jsx)(r.code,{children:"#PollinatorFriendly"})]}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsxs)(r.td,{children:["a. The gardener (owner or editor) has attested in their profile that they have completed a permaculture workshop. ",(0,t.jsx)("br",{})," b. There are Observations indicating at least one of the practices within a single calendar year."]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsxs)(r.td,{children:["a. The gardener (owner or editor) has attested in their profile that they have completed a permaculture workshop. ",(0,t.jsx)("br",{})," b. There are Observations indicating at least one of the practices for exactly two calendar years."]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsxs)(r.td,{children:["a. The gardener (owner or editor) has attested in their profile that they have completed a permaculture workshop. ",(0,t.jsx)("br",{})," b. There are Observations indicating at least one of the practices for three or more calendar years."]})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-8",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Garden and Observation mutations."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.h3,{id:"vermiculturalist",children:"Vermiculturalist"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-9",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has experience with vermiculture (the controlled growing of worms) and vermicomposting (the use of worms to produce compost)."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-10",children:"Observation tags:"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#CompostTea"}),", ",(0,t.jsx)(r.code,{children:"#Vermiculture"}),", ",(0,t.jsx)(r.code,{children:"#Worms"}),"."]}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices within a single calendar year in a single Garden."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for two calendar years in a single Garden."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for three or more calendar years in a single Garden."})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-9",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Observation mutations."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.h3,{id:"seed-saver",children:"Seed Saver"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-10",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has demonstrated experience with seed saving practices, including: (1) Harvesting seeds from plants, (2) Drying seeds, (3) Storing seeds, (4) Germinating seeds, (5) Providing seeds to other members of the community."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-11",children:"Observation tags:"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#SeedSaving"}),", ",(0,t.jsx)(r.code,{children:"#SeedSharing"})]}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices within a single calendar year in a single Garden."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for two calendar years in a single Garden."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for three or more calendar years in a single Garden."})]})]})]}),"\n",(0,t.jsx)(r.h4,{id:"implementation-notes-10",children:"Implementation notes"}),"\n",(0,t.jsx)(r.p,{children:"Triggered as part of Observation mutations."}),"\n",(0,t.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,t.jsx)(r.h2,{id:"post-10-badges",children:"Post-1.0 badges"}),"\n",(0,t.jsx)(r.p,{children:"Here are some proposals for badges that we could add after the 1.0 release. I have not edited these descriptions to conform to the latest design principles."}),"\n",(0,t.jsx)(r.h3,{id:"chapter-chair",children:"Chapter Chair"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-11",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener is serving as a Chair for the Chapter."}),"\n",(0,t.jsx)(r.p,{children:"Note that GGC System Admins are responsible to designating which member(s) of a Chapter are the Chair(s). When they do this designation, they set a flag in the member`s profile indicating that they are currently a Chapter Chair and what date they started being Chair."}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. The gardener is currently the Chapter Chair, and has served as a Chapter Chair for one or two years."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. The gardener is currently the Chapter Chair, and has served as a Chapter Chair for three or four years."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. The gardener is currently the Chapter Chair, and has served as a Chapter Chair for five or more years."})]})]})]}),"\n",(0,t.jsx)(r.h3,{id:"connected-community",children:"Connected Community"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-12",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The chapter has demonstrated a commitment to building a community of practice."}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. At least 100 gardeners in the chapter."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. At least 250 gardeners in the chapter."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. At least 500 gardeners in the chapter."})]})]})]}),"\n",(0,t.jsx)(r.h3,{id:"climate-victors",children:"Climate Victors"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-13",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The chapter has demonstrated a commitment to creating Climate Victory Gardens."}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. At least 50% of the chapter gardens have achieved the badge."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. At least 75% of the chapter gardens have achieved the badge."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. At least 90% of the chapter gardens have achieved the badge."})]})]})]}),"\n",(0,t.jsx)(r.h3,{id:"pesticide-resistors",children:"Pesticide Resistors"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-14",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The chapter has demonstrated a commitment to avoiding the use of pesticides in their gardens."}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. At least 50% of the chapter gardens have achieved the badge."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. At least 75% of the chapter gardens have achieved the badge."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. At least 90% of the chapter gardens have achieved the badge."})]})]})]}),"\n",(0,t.jsx)(r.h3,{id:"seed-sharers",children:"Seed Sharers"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-15",children:"General Criteria:"}),"\n",(0,t.jsx)(r.p,{children:"The chapter has demonstrated a commitment to seed sharing."}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. At least 50% of the chapter gardens have achieved the badge."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"a. At least 75% of the chapter gardens have achieved the badge."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"a. At least 90% of the chapter gardens have achieved the badge."})]})]})]}),"\n",(0,t.jsx)(r.h3,{id:"climate-victory",children:"Climate Victory"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-16",children:"General Criteria"}),"\n",(0,t.jsxs)(r.p,{children:["A Climate Victory Garden has been added to ",(0,t.jsx)(r.a,{href:"https://www.greenamerica.org/climate-victory-gardens",children:"Green America`s database"})," and the garden implements one or more of the following practices: (1) grow food, (2) cover soils, (3) compost, (4) ditch chemicals, and (5) encourage biodiversity."]}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-12",children:"Observation tags"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#Biodiversity"}),", ",(0,t.jsx)(r.code,{children:"#Compost"}),", ",(0,t.jsx)(r.code,{children:"#CoverCrops"}),",",(0,t.jsx)(r.code,{children:"#DitchChemicals"}),", ",(0,t.jsx)(r.code,{children:"#PesticideFree"}),", ",(0,t.jsx)(r.code,{children:"#PollinatorFriendly"}),", ",(0,t.jsx)(r.code,{children:"#SheetMulch"}),"."]}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsxs)(r.td,{children:["a. The user has attested that the Garden is in the Green America database. ",(0,t.jsx)("br",{})," b. There are Observations associated with this garden for at least two of the associated tags."]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsxs)(r.td,{children:["a. The user has attested that the Garden is in the Green America database. ",(0,t.jsx)("br",{})," b. There are Observations associated with this garden for at least five of the associated tags."]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsxs)(r.td,{children:["a. The user has attested that the Garden is in the Green America database. ",(0,t.jsx)("br",{})," b. There are Observations associated with this garden for at least five of the associated tags in at least two different calendar years."]})]})]})]}),"\n",(0,t.jsx)(r.h3,{id:"master-gardener",children:"Master gardener"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-17",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has completed a master gardener program."}),"\n",(0,t.jsx)(r.admonition,{title:"Shucks",type:"warning",children:(0,t.jsx)(r.p,{children:"I cannot think of a simple way to award more than one star. Ideas?"})}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-13",children:"Observation tags"}),"\n",(0,t.jsx)(r.p,{children:"N/A"}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Level"}),(0,t.jsx)(r.th,{children:"Verification"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"1"}),(0,t.jsx)(r.td,{children:"a. The gardener attests in their profile to having received a Master Gardener certification."})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"2"}),(0,t.jsx)(r.td,{children:"(Not yet available)"})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:"3"}),(0,t.jsx)(r.td,{children:"(Not yet available)"})]})]})]}),"\n",(0,t.jsx)(r.h3,{id:"bee-buddy",children:"Bee Buddy"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-18",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has experience caring for bees."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-14",children:"Observation tags"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#Beekeeping"}),", ",(0,t.jsx)(r.code,{children:"#Beekeeper"})]}),"\n",(0,t.jsx)(r.h3,{id:"aquaponics-ace",children:"Aquaponics Ace"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-19",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has demonstrated experience with aquaponics."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-15",children:"Observation tags"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#Aquaponics"}),", ",(0,t.jsx)(r.code,{children:"#FishAndPlants"}),","]}),"\n",(0,t.jsx)(r.h3,{id:"herbalist-hero",children:"Herbalist Hero"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-20",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has grown medicinal herbs and created remedies from them."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-16",children:"Observation tags:"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#Herbalist"}),", ",(0,t.jsx)(r.code,{children:"#HerbalRemedy"}),", ",(0,t.jsx)(r.code,{children:"#PlantMedicine"})]}),"\n",(0,t.jsx)(r.h3,{id:"educator-extraordinaire",children:"Educator Extraordinaire"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-21",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has provided educational experiences such as leading workshops, writing articles, or working as a garden educator in schools."}),"\n",(0,t.jsx)(r.h4,{id:"observation-tags-17",children:"Observation tags:"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"#InspireAndTeach"}),", ",(0,t.jsx)(r.code,{children:"#SkillSharing"}),", ",(0,t.jsx)(r.code,{children:"#CommunityWorkshop"})]}),"\n",(0,t.jsx)(r.h3,{id:"orchard-orchestrator",children:"Orchard Orchestrator"}),"\n",(0,t.jsx)(r.h4,{id:"general-criteria-22",children:"General Criteria"}),"\n",(0,t.jsx)(r.p,{children:"The gardener has demonstrated experience with orchard management."})]})}function c(e={}){const{wrapper:r}={...(0,a.a)(),...e.components};return r?(0,t.jsx)(r,{...e,children:(0,t.jsx)(o,{...e})}):o(e)}},1151:(e,r,i)=>{i.d(r,{Z:()=>d,a:()=>s});var t=i(7294);const a={},n=t.createContext(a);function s(e){const r=t.useContext(n);return t.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function d(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),t.createElement(n.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/3ad77611.dafd6774.js b/assets/js/3ad77611.dafd6774.js deleted file mode 100644 index 427739f00..000000000 --- a/assets/js/3ad77611.dafd6774.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[403],{9657:(e,r,i)=>{i.r(r),i.d(r,{assets:()=>l,contentTitle:()=>s,default:()=>c,frontMatter:()=>n,metadata:()=>d,toc:()=>h});var a=i(5893),t=i(1151);const n={hide_table_of_contents:!1},s="Badges",d={id:"develop/design/badges",title:"Badges",description:"Goals",source:"@site/docs/develop/design/badges.md",sourceDirName:"develop/design",slug:"/develop/design/badges",permalink:"/docs/develop/design/badges",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Data Model",permalink:"/docs/develop/design/data-model"},next:{title:"GGC Input Fields",permalink:"/docs/develop/design/input-fields"}},l={},h=[{value:"Goals",id:"goals",level:2},{value:"Design principles",id:"design-principles",level:2},{value:"Types",id:"types",level:3},{value:"Levels",id:"levels",level:3},{value:"Verification (i.e. badge processing)",id:"verification-ie-badge-processing",level:3},{value:"Observation tags",id:"observation-tags",level:3},{value:"Implementation hints",id:"implementation-hints",level:3},{value:"Garden badges",id:"garden-badges",level:2},{value:"Pesticide free",id:"pesticide-free",level:3},{value:"General Criteria",id:"general-criteria",level:4},{value:"Observation tags",id:"observation-tags-1",level:4},{value:"Implementation notes",id:"implementation-notes",level:4},{value:"Pollinator Friendly",id:"pollinator-friendly",level:3},{value:"General Criteria",id:"general-criteria-1",level:4},{value:"Observation tags",id:"observation-tags-2",level:4},{value:"Implementation notes",id:"implementation-notes-1",level:4},{value:"Sustainable Soil",id:"sustainable-soil",level:3},{value:"General Criteria",id:"general-criteria-2",level:4},{value:"Observation tags",id:"observation-tags-3",level:4},{value:"Implementation notes",id:"implementation-notes-2",level:4},{value:"Water Smart",id:"water-smart",level:3},{value:"General Criteria",id:"general-criteria-3",level:4},{value:"Observation tags",id:"observation-tags-4",level:4},{value:"Implementation notes",id:"implementation-notes-3",level:4},{value:"Gardener badges",id:"gardener-badges",level:2},{value:"Community Cultivator",id:"community-cultivator",level:3},{value:"General Criteria",id:"general-criteria-4",level:4},{value:"Observation tags:",id:"observation-tags-5",level:4},{value:"Implementation notes",id:"implementation-notes-4",level:4},{value:"Compost Champion",id:"compost-champion",level:3},{value:"General Criteria",id:"general-criteria-5",level:4},{value:"Observation tags",id:"observation-tags-6",level:4},{value:"Implementation notes",id:"implementation-notes-5",level:4},{value:"Crop Whisperer",id:"crop-whisperer",level:3},{value:"General Criteria",id:"general-criteria-6",level:4},{value:"Observation tags",id:"observation-tags-7",level:4},{value:"Implementation notes",id:"implementation-notes-6",level:4},{value:"Greenhouse grower",id:"greenhouse-grower",level:3},{value:"General Criteria",id:"general-criteria-7",level:4},{value:"Observation tags",id:"observation-tags-8",level:4},{value:"Implementation notes",id:"implementation-notes-7",level:4},{value:"Permaculture Pro",id:"permaculture-pro",level:3},{value:"General Criteria",id:"general-criteria-8",level:4},{value:"Observation tags",id:"observation-tags-9",level:4},{value:"Implementation notes",id:"implementation-notes-8",level:4},{value:"Vermiculturalist",id:"vermiculturalist",level:3},{value:"General Criteria",id:"general-criteria-9",level:4},{value:"Observation tags:",id:"observation-tags-10",level:4},{value:"Implementation notes",id:"implementation-notes-9",level:4},{value:"Seed Saver",id:"seed-saver",level:3},{value:"General Criteria",id:"general-criteria-10",level:4},{value:"Observation tags:",id:"observation-tags-11",level:4},{value:"Implementation notes",id:"implementation-notes-10",level:4},{value:"Post-1.0 badges",id:"post-10-badges",level:2},{value:"Chapter Chair",id:"chapter-chair",level:3},{value:"General Criteria",id:"general-criteria-11",level:4},{value:"Connected Community",id:"connected-community",level:3},{value:"General Criteria",id:"general-criteria-12",level:4},{value:"Climate Victors",id:"climate-victors",level:3},{value:"General Criteria",id:"general-criteria-13",level:4},{value:"Pesticide Resistors",id:"pesticide-resistors",level:3},{value:"General Criteria",id:"general-criteria-14",level:4},{value:"Seed Sharers",id:"seed-sharers",level:3},{value:"General Criteria:",id:"general-criteria-15",level:4},{value:"Climate Victory",id:"climate-victory",level:3},{value:"General Criteria",id:"general-criteria-16",level:4},{value:"Observation tags",id:"observation-tags-12",level:4},{value:"Master gardener",id:"master-gardener",level:3},{value:"General Criteria",id:"general-criteria-17",level:4},{value:"Observation tags",id:"observation-tags-13",level:4},{value:"Bee Buddy",id:"bee-buddy",level:3},{value:"General Criteria",id:"general-criteria-18",level:4},{value:"Observation tags",id:"observation-tags-14",level:4},{value:"Aquaponics Ace",id:"aquaponics-ace",level:3},{value:"General Criteria",id:"general-criteria-19",level:4},{value:"Observation tags",id:"observation-tags-15",level:4},{value:"Herbalist Hero",id:"herbalist-hero",level:3},{value:"General Criteria",id:"general-criteria-20",level:4},{value:"Observation tags:",id:"observation-tags-16",level:4},{value:"Educator Extraordinaire",id:"educator-extraordinaire",level:3},{value:"General Criteria",id:"general-criteria-21",level:4},{value:"Observation tags:",id:"observation-tags-17",level:4},{value:"Orchard Orchestrator",id:"orchard-orchestrator",level:3},{value:"General Criteria",id:"general-criteria-22",level:4}];function o(e){const r={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",ol:"ol",p:"p",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(r.header,{children:(0,a.jsx)(r.h1,{id:"badges",children:"Badges"})}),"\n",(0,a.jsx)(r.h2,{id:"goals",children:"Goals"}),"\n",(0,a.jsx)(r.p,{children:"The 1.0 release implements a badge system for gardens and gardeners. This badge system is designed to accomplish the following goals:"}),"\n",(0,a.jsxs)(r.ol,{children:["\n",(0,a.jsxs)(r.li,{children:[(0,a.jsx)(r.em,{children:"Foster user engagement and enjoyment through a game mechanic that publicizes achievements by gardens and gardeners."})," Gardeners should find it fun to accumulate badges that are associated with their profile and their garden(s)."]}),"\n",(0,a.jsxs)(r.li,{children:[(0,a.jsx)(r.em,{children:"Foster a community of practice by helping gardeners connect with others with similar interests and/or greater expertise with respect to a specific gardening topic."})," For example, if a user is interested in vermiculture, the badge system provides a mechanism for them to find other gardeners who already have experience in this area."]}),"\n",(0,a.jsxs)(r.li,{children:[(0,a.jsx)(r.em,{children:"Provide a useful, compact representation of garden and gardener characteristics."}),' The app provides "summary" cards for gardens and gardeners. Users should find the presence (and/or absence) of badges helpful in forming a high level understanding of these entities.']}),"\n",(0,a.jsxs)(r.li,{children:[(0,a.jsx)(r.em,{children:"Provide a mechanism that identifies ways to improve gardening practices."}),' The badge system makes visible the practices that are important to the GGC mission of food resiliency and sustainable gardening, such as seed saving, composting, and water conservation. This means that a simple heuristic for "getting better at gardening" is to simply "get more badges".']}),"\n"]}),"\n",(0,a.jsxs)(r.admonition,{title:"What about Chapters?",type:"info",children:[(0,a.jsx)(r.p,{children:"The 1.0 release will not implement Chapter-level badges for two reasons:"}),(0,a.jsxs)(r.ol,{children:["\n",(0,a.jsx)(r.li,{children:'The primary goals for Chapter-level badges are: (a) encouraging members of a Chapter via "peer pressure" to conform to certain best practices, and (b) making possible Chapter "leaderboards" so that Chapters can assess their capabilities relative to other chapters. Neither of these are important for the 1.0 release where we will focus on a single Chapter with a relatively small number of members.'}),"\n",(0,a.jsx)(r.li,{children:"We will implement garden and gardener-based badge processing within each client, which is simple, scalable, and (hopefully) efficient. Implementing a Chapter-level badge system at scale will require Firebase cloud functions. These functions require specialized knowledge to implement correctly."}),"\n"]})]}),"\n",(0,a.jsx)(r.h2,{id:"design-principles",children:"Design principles"}),"\n",(0,a.jsx)(r.h3,{id:"types",children:"Types"}),"\n",(0,a.jsx)(r.p,{children:"The 1.0 release will implement two types of badges: garden badges and gardener badges. Garden badges reflect the characteristics of a garden across one or more years. Gardener badges reflect characteristics of a gardener across all of the gardens with which they are associated."}),"\n",(0,a.jsx)(r.h3,{id:"levels",children:"Levels"}),"\n",(0,a.jsx)(r.p,{children:"Each badge can be achieved at three levels of increasing sophistication and/or expertise. Level 1 badges are relatively easy to achieve. Level 2 and Level 3 badges indicate increasing levels of expertise or accomplishment with respect to the badge subject."}),"\n",(0,a.jsx)(r.p,{children:"Levels will be visually represented by 1-3 stars along the left side of the badge. Here`s an example:"}),"\n",(0,a.jsx)("img",{style:{borderStyle:"solid"},width:"300px",src:"/img/develop/release-1.0/badges/badge-examples.png"}),"\n",(0,a.jsx)(r.h3,{id:"verification-ie-badge-processing",children:"Verification (i.e. badge processing)"}),"\n",(0,a.jsx)(r.p,{children:'Verification of badges can be done in the following ways: "via attestation", "via observation", or "via planting". Depending upon the badge and/or level, one or more of these verification approaches might be required.'}),"\n",(0,a.jsx)(r.p,{children:'"Via attestation" means that the Gardener (owner) has simply attested that they (or their garden) adheres to certain practices. This is implemented as an "Attestation" section in the Garden and Gardener forms. For example, when creating or updating a Garden, the gardener (owner) can simply check a box to attest that the garden is pesticide-free. Gardeners are on the honor system to attest only to practices that they believe to be true.'}),"\n",(0,a.jsx)(r.p,{children:'"Via Observation" requires the Gardener (owner or editor) to post one or more Observations with one or more badge-specific tags in a single Garden.'}),"\n",(0,a.jsx)(r.p,{children:'"Via Planting" requires the Gardener (owner) to have created Planting data in a single Garden that helps to satisfy the criteria for a badge.'}),"\n",(0,a.jsxs)(r.admonition,{title:"1.0 release badge processing is client-side only",type:"warning",children:[(0,a.jsx)(r.p,{children:"As the above indicates, for the 1.0 release, badge processing occurs on the client-side, and is triggered by updates to garden, gardener, observation, or planting documents."}),(0,a.jsx)(r.p,{children:'The current criteria are designed so that they can be assessed via either WithCoreData or WithGardenData. See the Implementation Notes section associated with each badge for an indication of which "With" widget can be used.'}),(0,a.jsx)(r.p,{children:"There are many ways we could define the criteria for a badge. The criteria we choose must align with the 1.0 release design constraints. If a criteria turns out to be too expensive to verify via client-side processing, then we should change the criteria, not change the design."})]}),"\n",(0,a.jsx)(r.h3,{id:"observation-tags",children:"Observation tags"}),"\n",(0,a.jsx)(r.p,{children:"Many badges require the posting of (public) Observations to provide evidence for a specific practice. To implement badge processing, the system needs to be able to identify Observations that are intended to support achievement of a particular badge. This will be done by the user attaching one or more pre-defined tags to an Observation. Some practices (i.e. cover crops) can help the user achieve multiple badges, so the tag labels are not designed to indicate any particular badge."}),"\n",(0,a.jsx)(r.p,{children:'The system will "take the user\'s word" for the appropriateness of the tags to the Observation. We hope that the public nature of these observations will prevent users from misusing this process.'}),"\n",(0,a.jsx)(r.h3,{id:"implementation-hints",children:"Implementation hints"}),"\n",(0,a.jsx)(r.p,{children:"Badge processing has the following general implementation characteristics:"}),"\n",(0,a.jsxs)(r.ul,{children:["\n",(0,a.jsx)(r.li,{children:'Triggered as part of "mutation" of Gardener, Garden, Planting, and Observation entities.'}),"\n",(0,a.jsx)(r.li,{children:"During submit() processing, the BadgeProcessor is called with the Garden, Chapter, and User collections, plus the entities about to be mutated. It then calls a function for each Badge, passing it this data. Each badge-specific function has its own function returns the set of BadgeInstances to be created and deleted."}),"\n"]}),"\n",(0,a.jsx)(r.p,{children:"Badge implementation also involves the creation of the Badges page. This page should provide a description of each Badge, the criteria required to obtain each level, and chips to indicate the Gardens (or Gardeners) that currently hold the badge."}),"\n",(0,a.jsx)(r.p,{children:'Badge implementation will also require updates to the Garden and Gardener entities and mutation processing in order to support the various "attestation" checkboxes. Each attestation can be implemented as an optional boolean field in the Garden or Gardener entity.'}),"\n",(0,a.jsx)(r.h2,{id:"garden-badges",children:"Garden badges"}),"\n",(0,a.jsx)(r.p,{children:"Here are proposals for the 1.0 release garden badges."}),"\n",(0,a.jsx)(r.h3,{id:"pesticide-free",children:"Pesticide free"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"No pesticides are used in this garden."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-1",children:"Observation tags"}),"\n",(0,a.jsx)(r.p,{children:"N/A"}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsxs)(r.td,{children:["a. The user has attested that the Garden is pesticide free. ",(0,a.jsx)("br",{})," b. There is Planting data for this garden for a single calendar year."]})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsxs)(r.td,{children:["a. The user has attested that the Garden is pesticide free. ",(0,a.jsx)("br",{})," b. There is Planting data for this garden for exactly two calendar years."]})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsxs)(r.td,{children:["a. The user has attested that the Garden is pesticide free. ",(0,a.jsx)("br",{})," b. There is Planting data for this garden for three or more calendar years."]})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Garden or Planting mutation."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.h3,{id:"pollinator-friendly",children:"Pollinator Friendly"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-1",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:'The garden has pollinator-friendly practices such as: (1) Using a wide variety of plants that bloom from early spring into late fall, (2) Avoiding modern hybrid flowers, especially those with "doubled" flowers, (3) Eliminating pesticides whenever possible, (4) Including larval host plants in your landscape, (5) Creating a damp salt lick for butterflies and bees, (6) Leaving dead trees, or at least an occasional dead limb, in order to provide essential nesting sites for native bees, and (7) Adding to nectar resources by providing a hummingbird feeder.'}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-2",children:"Observation tags"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#DitchChemicals"}),", ",(0,a.jsx)(r.code,{children:"#Habitat"}),", ",(0,a.jsx)(r.code,{children:"#Hummingbirds"}),", ",(0,a.jsx)(r.code,{children:"#LarvalHostPlants"}),", ",(0,a.jsx)(r.code,{children:"#NativeBees"}),", ",(0,a.jsx)(r.code,{children:"#NativePlants"}),", ",(0,a.jsx)(r.code,{children:"#PesticideFree"}),", ",(0,a.jsx)(r.code,{children:"#SaltLick"}),"."]}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices within a single calendar year."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices for two calendar years."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices for three or more calendar years."})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-1",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Observation mutation."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.h3,{id:"sustainable-soil",children:"Sustainable Soil"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-2",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"Garden soil has been improved by using sheet mulch, compost, and/or cover crops."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-3",children:"Observation tags"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#Compost"}),", ",(0,a.jsx)(r.code,{children:"#CoverCrops"}),", ",(0,a.jsx)(r.code,{children:"#SheetMulch"}),", ",(0,a.jsx)(r.code,{children:"#Mulch"}),", ",(0,a.jsx)(r.code,{children:"#CropRotation"})]}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices within a single calendar year."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices for two calendar years."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least three of the practices for three or more calendar years."})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-2",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Observation mutation."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.h3,{id:"water-smart",children:"Water Smart"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-3",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The garden involves water conservation practices, including: (1) collecting and using rainwater; (2) drip irrigation or soaker hoses, or (3) timers to water during cooler parts of day to minimize water use."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-4",children:"Observation tags"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#DripIrrigation"}),", ",(0,a.jsx)(r.code,{children:"#Rainwater"}),", ",(0,a.jsx)(r.code,{children:"#WaterTimer"}),"."]}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices within a single calendar year."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for two calendar years."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for three or more calendar years."})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-3",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Observation mutation."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.h2,{id:"gardener-badges",children:"Gardener badges"}),"\n",(0,a.jsx)(r.p,{children:"Here are proposals for the 1.0 release Gardener badges."}),"\n",(0,a.jsx)(r.h3,{id:"community-cultivator",children:"Community Cultivator"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-4",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has demonstrated experience with community and/or school gardening."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-5",children:"Observation tags:"}),"\n",(0,a.jsx)(r.p,{children:"N/A"}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:'a. The gardener is associated with exactly one garden which has the "Community or School Garden" attestation.'})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:'a. The gardener is associated with exactly two gardens that have the "Community or School Garden" attestation.'})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:'a. The gardener is associated with three or more gardens that have the "Community or School Garden" attestation.'})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-4",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Garden and Gardener mutation."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithCoreData."}),"\n",(0,a.jsx)(r.h3,{id:"compost-champion",children:"Compost Champion"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-5",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has experience composting in a gardens."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-6",children:"Observation tags"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#Compost"}),", ",(0,a.jsx)(r.code,{children:"#CompostTea"}),", ",(0,a.jsx)(r.code,{children:"#Hugelkulture"}),", ",(0,a.jsx)(r.code,{children:"#Vermiculture"}),", ",(0,a.jsx)(r.code,{children:"#Worms"}),"."]}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. The gardener (owner or editor) has posted Observations indicating at least one of the practices for a single calendar year in a garden."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. The gardener (owner or editor) has posted Observations indicating at least one of the practices for two calendar years in a single garden."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. The gardener (owner or editor) has posted Observations indicating at least one of the practices for three or more calendar years in a single garden."})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-5",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Observation mutation."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.p,{children:'Note that the gardener cannot get to levels 2 or 3 by "switching" among different gardens. The postings must be from the same garden. This means WithGardenData is enough to evaluate the criteria.'}),"\n",(0,a.jsx)(r.p,{children:'Also, the gardener must make the Observations themselves. They can\'t "passively" obtain the badge because someone else in the Garden made Observations with the appropriate tags.'}),"\n",(0,a.jsx)(r.h3,{id:"crop-whisperer",children:"Crop Whisperer"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-6",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has demonstrated expertise in growing a specific crop in a single garden."}),"\n",(0,a.jsx)(r.admonition,{title:"Multiple Badge Alert!",type:"info",children:(0,a.jsx)(r.p,{children:'Unlike other badges, this badge is crop-specific, and so a gardener can earn multiple Crop Whisperer badges ("Bean Whisperer", "Cucumber Whisperer")'})}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-7",children:"Observation tags"}),"\n",(0,a.jsx)(r.p,{children:"N/A"}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsxs)(r.td,{children:["a. There are Plantings for exactly three different varieties of the same crop. ",(0,a.jsx)("br",{})," b. At least two outcomes were awarded at least three stars in at least one Planting."]})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsxs)(r.td,{children:["a. There are Plantings for exactly four different varieties of the same crop. ",(0,a.jsx)("br",{})," b. At least two outcomes were awarded at least three stars in at least one Planting."]})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsxs)(r.td,{children:["a. There are Plantings for at least five different varieties of the same crop. ",(0,a.jsx)("br",{})," b. At least two outcomes were awarded at least three stars in at least one Planting."]})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-6",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Planting mutation."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.h3,{id:"greenhouse-grower",children:"Greenhouse grower"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-7",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has experience growing plants successfully in a greenhouse."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-8",children:"Observation tags"}),"\n",(0,a.jsx)(r.p,{children:"N/A."}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. There is a single Planting in a single Garden that was started in a greenhouse that survived to harvest and was awarded at least three stars for at least one outcomes."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. There are two Plantings in a single Garden that were started in a greenhouse that survived to harvest and were awarded at least three stars for at least one outcomes."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. There are three Plantings in a single Garden that were started in a greenhouse that survived to harvest and were awarded at least three stars for at least one outcomes."})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-7",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Planting mutation."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.h3,{id:"permaculture-pro",children:"Permaculture Pro"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-8",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has completed a Permaculture workshop to learn about the philosophy of permaculture and is also associated with garden(s) that have achieved permaculture-related badges"}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-9",children:"Observation tags"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#PesticideFree"}),", ",(0,a.jsx)(r.code,{children:"#SustainableSoil"}),", ",(0,a.jsx)(r.code,{children:"#WaterSmart"}),", ",(0,a.jsx)(r.code,{children:"#PollinatorFriendly"})]}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsxs)(r.td,{children:["a. The gardener (owner or editor) has attested in their profile that they have completed a permaculture workshop. ",(0,a.jsx)("br",{})," b. There are Observations indicating at least one of the practices within a single calendar year."]})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsxs)(r.td,{children:["a. The gardener (owner or editor) has attested in their profile that they have completed a permaculture workshop. ",(0,a.jsx)("br",{})," b. There are Observations indicating at least one of the practices for exactly two calendar years."]})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsxs)(r.td,{children:["a. The gardener (owner or editor) has attested in their profile that they have completed a permaculture workshop. ",(0,a.jsx)("br",{})," b. There are Observations indicating at least one of the practices for three or more calendar years."]})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-8",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Garden and Observation mutations."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.h3,{id:"vermiculturalist",children:"Vermiculturalist"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-9",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has experience with vermiculture (the controlled growing of worms) and vermicomposting (the use of worms to produce compost)."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-10",children:"Observation tags:"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#CompostTea"}),", ",(0,a.jsx)(r.code,{children:"#Vermiculture"}),", ",(0,a.jsx)(r.code,{children:"#Worms"}),"."]}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices within a single calendar year in a single Garden."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for two calendar years in a single Garden."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for three or more calendar years in a single Garden."})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-9",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Observation mutations."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.h3,{id:"seed-saver",children:"Seed Saver"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-10",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has demonstrated experience with seed saving practices, including: (1) Harvesting seeds from plants, (2) Drying seeds, (3) Storing seeds, (4) Germinating seeds, (5) Providing seeds to other members of the community."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-11",children:"Observation tags:"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#SeedSaving"}),", ",(0,a.jsx)(r.code,{children:"#SeedSharing"})]}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices within a single calendar year in a single Garden."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for two calendar years in a single Garden."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. There are Observations indicating at least one of the practices for three or more calendar years in a single Garden."})]})]})]}),"\n",(0,a.jsx)(r.h4,{id:"implementation-notes-10",children:"Implementation notes"}),"\n",(0,a.jsx)(r.p,{children:"Triggered as part of Observation mutations."}),"\n",(0,a.jsx)(r.p,{children:"Requires WithGardenData."}),"\n",(0,a.jsx)(r.h2,{id:"post-10-badges",children:"Post-1.0 badges"}),"\n",(0,a.jsx)(r.p,{children:"Here are some proposals for badges that we could add after the 1.0 release. I have not edited these descriptions to conform to the latest design principles."}),"\n",(0,a.jsx)(r.h3,{id:"chapter-chair",children:"Chapter Chair"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-11",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener is serving as a Chair for the Chapter."}),"\n",(0,a.jsx)(r.p,{children:"Note that GGC System Admins are responsible to designating which member(s) of a Chapter are the Chair(s). When they do this designation, they set a flag in the member`s profile indicating that they are currently a Chapter Chair and what date they started being Chair."}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. The gardener is currently the Chapter Chair, and has served as a Chapter Chair for one or two years."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. The gardener is currently the Chapter Chair, and has served as a Chapter Chair for three or four years."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. The gardener is currently the Chapter Chair, and has served as a Chapter Chair for five or more years."})]})]})]}),"\n",(0,a.jsx)(r.h3,{id:"connected-community",children:"Connected Community"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-12",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The chapter has demonstrated a commitment to building a community of practice."}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. At least 100 gardeners in the chapter."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. At least 250 gardeners in the chapter."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. At least 500 gardeners in the chapter."})]})]})]}),"\n",(0,a.jsx)(r.h3,{id:"climate-victors",children:"Climate Victors"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-13",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The chapter has demonstrated a commitment to creating Climate Victory Gardens."}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. At least 50% of the chapter gardens have achieved the badge."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. At least 75% of the chapter gardens have achieved the badge."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. At least 90% of the chapter gardens have achieved the badge."})]})]})]}),"\n",(0,a.jsx)(r.h3,{id:"pesticide-resistors",children:"Pesticide Resistors"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-14",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The chapter has demonstrated a commitment to avoiding the use of pesticides in their gardens."}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. At least 50% of the chapter gardens have achieved the badge."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. At least 75% of the chapter gardens have achieved the badge."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. At least 90% of the chapter gardens have achieved the badge."})]})]})]}),"\n",(0,a.jsx)(r.h3,{id:"seed-sharers",children:"Seed Sharers"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-15",children:"General Criteria:"}),"\n",(0,a.jsx)(r.p,{children:"The chapter has demonstrated a commitment to seed sharing."}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. At least 50% of the chapter gardens have achieved the badge."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"a. At least 75% of the chapter gardens have achieved the badge."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"a. At least 90% of the chapter gardens have achieved the badge."})]})]})]}),"\n",(0,a.jsx)(r.h3,{id:"climate-victory",children:"Climate Victory"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-16",children:"General Criteria"}),"\n",(0,a.jsxs)(r.p,{children:["A Climate Victory Garden has been added to ",(0,a.jsx)(r.a,{href:"https://www.greenamerica.org/climate-victory-gardens",children:"Green America`s database"})," and the garden implements one or more of the following practices: (1) grow food, (2) cover soils, (3) compost, (4) ditch chemicals, and (5) encourage biodiversity."]}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-12",children:"Observation tags"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#Biodiversity"}),", ",(0,a.jsx)(r.code,{children:"#Compost"}),", ",(0,a.jsx)(r.code,{children:"#CoverCrops"}),",",(0,a.jsx)(r.code,{children:"#DitchChemicals"}),", ",(0,a.jsx)(r.code,{children:"#PesticideFree"}),", ",(0,a.jsx)(r.code,{children:"#PollinatorFriendly"}),", ",(0,a.jsx)(r.code,{children:"#SheetMulch"}),"."]}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsxs)(r.td,{children:["a. The user has attested that the Garden is in the Green America database. ",(0,a.jsx)("br",{})," b. There are Observations associated with this garden for at least two of the associated tags."]})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsxs)(r.td,{children:["a. The user has attested that the Garden is in the Green America database. ",(0,a.jsx)("br",{})," b. There are Observations associated with this garden for at least five of the associated tags."]})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsxs)(r.td,{children:["a. The user has attested that the Garden is in the Green America database. ",(0,a.jsx)("br",{})," b. There are Observations associated with this garden for at least five of the associated tags in at least two different calendar years."]})]})]})]}),"\n",(0,a.jsx)(r.h3,{id:"master-gardener",children:"Master gardener"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-17",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has completed a master gardener program."}),"\n",(0,a.jsx)(r.admonition,{title:"Shucks",type:"warning",children:(0,a.jsx)(r.p,{children:"I cannot think of a simple way to award more than one star. Ideas?"})}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-13",children:"Observation tags"}),"\n",(0,a.jsx)(r.p,{children:"N/A"}),"\n",(0,a.jsxs)(r.table,{children:[(0,a.jsx)(r.thead,{children:(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.th,{children:"Level"}),(0,a.jsx)(r.th,{children:"Verification"})]})}),(0,a.jsxs)(r.tbody,{children:[(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"1"}),(0,a.jsx)(r.td,{children:"a. The gardener attests in their profile to having received a Master Gardener certification."})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"2"}),(0,a.jsx)(r.td,{children:"(Not yet available)"})]}),(0,a.jsxs)(r.tr,{children:[(0,a.jsx)(r.td,{children:"3"}),(0,a.jsx)(r.td,{children:"(Not yet available)"})]})]})]}),"\n",(0,a.jsx)(r.h3,{id:"bee-buddy",children:"Bee Buddy"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-18",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has experience caring for bees."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-14",children:"Observation tags"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#Beekeeping"}),", ",(0,a.jsx)(r.code,{children:"#Beekeeper"})]}),"\n",(0,a.jsx)(r.h3,{id:"aquaponics-ace",children:"Aquaponics Ace"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-19",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has demonstrated experience with aquaponics."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-15",children:"Observation tags"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#Aquaponics"}),", ",(0,a.jsx)(r.code,{children:"#FishAndPlants"}),","]}),"\n",(0,a.jsx)(r.h3,{id:"herbalist-hero",children:"Herbalist Hero"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-20",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has grown medicinal herbs and created remedies from them."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-16",children:"Observation tags:"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#Herbalist"}),", ",(0,a.jsx)(r.code,{children:"#HerbalRemedy"}),", ",(0,a.jsx)(r.code,{children:"#PlantMedicine"})]}),"\n",(0,a.jsx)(r.h3,{id:"educator-extraordinaire",children:"Educator Extraordinaire"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-21",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has provided educational experiences such as leading workshops, writing articles, or working as a garden educator in schools."}),"\n",(0,a.jsx)(r.h4,{id:"observation-tags-17",children:"Observation tags:"}),"\n",(0,a.jsxs)(r.p,{children:[(0,a.jsx)(r.code,{children:"#InspireAndTeach"}),", ",(0,a.jsx)(r.code,{children:"#SkillSharing"}),", ",(0,a.jsx)(r.code,{children:"#CommunityWorkshop"})]}),"\n",(0,a.jsx)(r.h3,{id:"orchard-orchestrator",children:"Orchard Orchestrator"}),"\n",(0,a.jsx)(r.h4,{id:"general-criteria-22",children:"General Criteria"}),"\n",(0,a.jsx)(r.p,{children:"The gardener has demonstrated experience with orchard management."})]})}function c(e={}){const{wrapper:r}={...(0,t.a)(),...e.components};return r?(0,a.jsx)(r,{...e,children:(0,a.jsx)(o,{...e})}):o(e)}},1151:(e,r,i)=>{i.d(r,{Z:()=>d,a:()=>s});var a=i(7294);const t={},n=a.createContext(t);function s(e){const r=a.useContext(n);return a.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function d(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),a.createElement(n.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7d1225b6.44565266.js b/assets/js/7d1225b6.44565266.js new file mode 100644 index 000000000..0a8e57c76 --- /dev/null +++ b/assets/js/7d1225b6.44565266.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4076],{7576:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>r,contentTitle:()=>l,default:()=>p,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var n=o(5893),a=o(1151);const s={hide_table_of_contents:!0},l="Backups",i={id:"develop/backups",title:"Backups",description:"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn't lose too much work.",source:"@site/docs/develop/backups.md",sourceDirName:"develop",slug:"/develop/backups",permalink:"/docs/develop/backups",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!0},sidebar:"developSidebar",previous:{title:"Deployment",permalink:"/docs/develop/deployment"},next:{title:"Features",permalink:"/docs/develop/design/features"}},r={},c=[{value:"1. Download and install Firefoo",id:"1-download-and-install-firefoo",level:2},{value:"2. Export a JSON file containing all collections",id:"2-export-a-json-file-containing-all-collections",level:2},{value:"3. Rename, compress, and upload the backup",id:"3-rename-compress-and-upload-the-backup",level:2},{value:"4. Restoring from backup",id:"4-restoring-from-backup",level:2}];function d(e){const t={a:"a",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",...(0,a.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"backups",children:"Backups"})}),"\n",(0,n.jsx)(t.p,{children:"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn't lose too much work."}),"\n",(0,n.jsx)(t.p,{children:"Here are the steps."}),"\n",(0,n.jsx)(t.h2,{id:"1-download-and-install-firefoo",children:"1. Download and install Firefoo"}),"\n",(0,n.jsxs)(t.p,{children:["Firefoo is located ",(0,n.jsx)(t.a,{href:"https://www.firefoo.app/",children:"here"}),"."]}),"\n",(0,n.jsx)(t.p,{children:"Once installed, you need to load the GGC Firestore database. When successful, the Firefoo screen should look something like this:"}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-0.png"}),"\n",(0,n.jsx)(t.h2,{id:"2-export-a-json-file-containing-all-collections",children:"2. Export a JSON file containing all collections"}),"\n",(0,n.jsx)(t.p,{children:'Right click on "ggc-app" in the left side-bar, and then select "Export All Collections..."'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-1.png"}),"\n",(0,n.jsx)(t.p,{children:'Select "Newline-delimited JSON" as the format, then click "Export" to export a file containing a separate line for each document in the database.'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-2.png"}),"\n",(0,n.jsx)(t.p,{children:'This will create a file named something like "ggc-app-2de7b-1711139889.jsonl".'}),"\n",(0,n.jsx)(t.h2,{id:"3-rename-compress-and-upload-the-backup",children:"3. Rename, compress, and upload the backup"}),"\n",(0,n.jsx)(t.p,{children:'Next, rename the file with the format "ggc-backup-YYYY-MM-DD.jsonl" so that the file name contains the date that the backup was made.'}),"\n",(0,n.jsx)(t.p,{children:'Next, compress the file. The compressed file will have the name "ggc-backup-YYYY-MM-DD.jsonl.zip" and will be much, much smaller, typically 10% or less. For example, as of May 2024, the uncompressed file was around 1 MB, and the compressed file was 87 KB.'}),"\n",(0,n.jsx)(t.p,{children:'Finally, upload the file to the geogardenclub "backups" repo. When done, the repo will look something like this:'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-3.png"}),"\n",(0,n.jsx)(t.h2,{id:"4-restoring-from-backup",children:"4. Restoring from backup"}),"\n",(0,n.jsx)(t.p,{children:"This should involve the following steps:"}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsx)(t.li,{children:"Download the latest zip file from the backups repository and uncompress."}),"\n",(0,n.jsx)(t.li,{children:'Right-click on the ggc-app and select "Import collections".'}),"\n",(0,n.jsx)(t.li,{children:'Select the backup file and click "Import".'}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"I have never done this, so I cannot verify that this will work."})]})}function p(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1151:(e,t,o)=>{o.d(t,{Z:()=>i,a:()=>l});var n=o(7294);const a={},s=n.createContext(a);function l(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:l(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7d1225b6.c945d809.js b/assets/js/7d1225b6.c945d809.js deleted file mode 100644 index f0b18e59a..000000000 --- a/assets/js/7d1225b6.c945d809.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[4076],{7576:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>r,contentTitle:()=>l,default:()=>p,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var n=o(5893),a=o(1151);const s={hide_table_of_contents:!0},l="Backups",i={id:"develop/backups",title:"Backups",description:"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn't lose too much work.",source:"@site/docs/develop/backups.md",sourceDirName:"develop",slug:"/develop/backups",permalink:"/docs/develop/backups",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!0},sidebar:"developSidebar",previous:{title:"Deployment",permalink:"/docs/develop/deployment"},next:{title:"Anatomy of a feature",permalink:"/docs/develop/design/features"}},r={},c=[{value:"1. Download and install Firefoo",id:"1-download-and-install-firefoo",level:2},{value:"2. Export a JSON file containing all collections",id:"2-export-a-json-file-containing-all-collections",level:2},{value:"3. Rename, compress, and upload the backup",id:"3-rename-compress-and-upload-the-backup",level:2},{value:"4. Restoring from backup",id:"4-restoring-from-backup",level:2}];function d(e){const t={a:"a",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",...(0,a.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"backups",children:"Backups"})}),"\n",(0,n.jsx)(t.p,{children:"Our current backup approach is to use Firefoo to create a JSON file containing all of the documents in the GGC Firestore database, compress this file, and upload it to the geogardenclub/backups repository. The goal is to do this every week or two, so that in the event of catastrophe, we can restore the database to a state that doesn't lose too much work."}),"\n",(0,n.jsx)(t.p,{children:"Here are the steps."}),"\n",(0,n.jsx)(t.h2,{id:"1-download-and-install-firefoo",children:"1. Download and install Firefoo"}),"\n",(0,n.jsxs)(t.p,{children:["Firefoo is located ",(0,n.jsx)(t.a,{href:"https://www.firefoo.app/",children:"here"}),"."]}),"\n",(0,n.jsx)(t.p,{children:"Once installed, you need to load the GGC Firestore database. When successful, the Firefoo screen should look something like this:"}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-0.png"}),"\n",(0,n.jsx)(t.h2,{id:"2-export-a-json-file-containing-all-collections",children:"2. Export a JSON file containing all collections"}),"\n",(0,n.jsx)(t.p,{children:'Right click on "ggc-app" in the left side-bar, and then select "Export All Collections..."'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-1.png"}),"\n",(0,n.jsx)(t.p,{children:'Select "Newline-delimited JSON" as the format, then click "Export" to export a file containing a separate line for each document in the database.'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-2.png"}),"\n",(0,n.jsx)(t.p,{children:'This will create a file named something like "ggc-app-2de7b-1711139889.jsonl".'}),"\n",(0,n.jsx)(t.h2,{id:"3-rename-compress-and-upload-the-backup",children:"3. Rename, compress, and upload the backup"}),"\n",(0,n.jsx)(t.p,{children:'Next, rename the file with the format "ggc-backup-YYYY-MM-DD.jsonl" so that the file name contains the date that the backup was made.'}),"\n",(0,n.jsx)(t.p,{children:'Next, compress the file. The compressed file will have the name "ggc-backup-YYYY-MM-DD.jsonl.zip" and will be much, much smaller, typically 10% or less. For example, as of May 2024, the uncompressed file was around 1 MB, and the compressed file was 87 KB.'}),"\n",(0,n.jsx)(t.p,{children:'Finally, upload the file to the geogardenclub "backups" repo. When done, the repo will look something like this:'}),"\n",(0,n.jsx)("img",{src:"/img/develop/release-1.0/backup-3.png"}),"\n",(0,n.jsx)(t.h2,{id:"4-restoring-from-backup",children:"4. Restoring from backup"}),"\n",(0,n.jsx)(t.p,{children:"This should involve the following steps:"}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsx)(t.li,{children:"Download the latest zip file from the backups repository and uncompress."}),"\n",(0,n.jsx)(t.li,{children:'Right-click on the ggc-app and select "Import collections".'}),"\n",(0,n.jsx)(t.li,{children:'Select the backup file and click "Import".'}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"I have never done this, so I cannot verify that this will work."})]})}function p(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1151:(e,t,o)=>{o.d(t,{Z:()=>i,a:()=>l});var n=o(7294);const a={},s=n.createContext(a);function l(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:l(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/a1e7621f.aba6ed83.js b/assets/js/a1e7621f.aba6ed83.js deleted file mode 100644 index 69d6f6d94..000000000 --- a/assets/js/a1e7621f.aba6ed83.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[2889],{7920:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>s,default:()=>c,frontMatter:()=>r,metadata:()=>o,toc:()=>l});var a=n(5893),i=n(1151);const r={hide_table_of_contents:!1},s="Data Model",o={id:"develop/design/data-model",title:"Data Model",description:"This page explains the data model (i.e. the set of entities and their relationships) for GGC, along with a rationale for the design decisions that we've made along the way.",source:"@site/docs/develop/design/data-model.md",sourceDirName:"develop/design",slug:"/develop/design/data-model",permalink:"/docs/develop/design/data-model",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Anatomy of a feature",permalink:"/docs/develop/design/features"},next:{title:"Badges",permalink:"/docs/develop/design/badges"}},d={},l=[{value:"Entities",id:"entities",level:2},{value:"Entity Hierarchy",id:"entity-hierarchy",level:4},{value:"Entity dependencies",id:"entity-dependencies",level:4},{value:"Chapter",id:"chapter",level:3},{value:"ChapterID management",id:"chapterid-management",level:4},{value:"User registration and chapter assignment",id:"user-registration-and-chapter-assignment",level:4},{value:"ChapterID as Firebase index",id:"chapterid-as-firebase-index",level:4},{value:"Chapter entity representation",id:"chapter-entity-representation",level:4},{value:"Projected Release 2.0 changes",id:"projected-release-20-changes",level:4},{value:"User",id:"user",level:3},{value:"Users vs Gardeners",id:"users-vs-gardeners",level:4},{value:"UserID management",id:"userid-management",level:4},{value:"User onboarding",id:"user-onboarding",level:4},{value:"User entity representation",id:"user-entity-representation",level:4},{value:"Gardener",id:"gardener",level:3},{value:"Chapter members vs Vendors",id:"chapter-members-vs-vendors",level:4},{value:"Cached values",id:"cached-values",level:4},{value:"Badge attestations",id:"badge-attestations",level:4},{value:"GardenerID management",id:"gardenerid-management",level:4},{value:"Gardener entity representation",id:"gardener-entity-representation",level:4},{value:"Garden",id:"garden",level:3},{value:"GardenID management",id:"gardenid-management",level:4},{value:"Field Notes",id:"field-notes",level:4},{value:"Cached values",id:"cached-values-1",level:4},{value:"Badge attestations",id:"badge-attestations-1",level:4},{value:"Garden entity representation",id:"garden-entity-representation",level:4},{value:"Editor",id:"editor",level:3},{value:"EditorID management",id:"editorid-management",level:4},{value:"Editor entity representation",id:"editor-entity-representation",level:4},{value:"Bed",id:"bed",level:3},{value:"BedID management",id:"bedid-management",level:4},{value:"Bed entity representation",id:"bed-entity-representation",level:4},{value:"Family",id:"family",level:3},{value:"FamilyID management",id:"familyid-management",level:4},{value:"Family entity representation",id:"family-entity-representation",level:4},{value:"Crop",id:"crop",level:3},{value:"CropID management",id:"cropid-management",level:4},{value:"Crop entity representation",id:"crop-entity-representation",level:4},{value:"Variety",id:"variety",level:3},{value:"VarietyID management",id:"varietyid-management",level:4},{value:"Field notes",id:"field-notes-1",level:4},{value:"Variety entity representation",id:"variety-entity-representation",level:4},{value:"Planting",id:"planting",level:3},{value:"Plantings and seeds",id:"plantings-and-seeds",level:4},{value:"PlantingID management",id:"plantingid-management",level:4},{value:"Field notes",id:"field-notes-2",level:4},{value:"Planting entity representation",id:"planting-entity-representation",level:4},{value:"Outcome",id:"outcome",level:3},{value:"OutcomeID management",id:"outcomeid-management",level:4},{value:"Field notes",id:"field-notes-3",level:4},{value:"Outcome entity representation",id:"outcome-entity-representation",level:4},{value:"Seed",id:"seed",level:3},{value:"SeedID management",id:"seedid-management",level:4},{value:"Field notes",id:"field-notes-4",level:4},{value:"Seed entity representation",id:"seed-entity-representation",level:4},{value:"Seed caveats",id:"seed-caveats",level:4},{value:"Observation",id:"observation",level:3},{value:"ObservationID management",id:"observationid-management",level:4},{value:"Field notes",id:"field-notes-5",level:4},{value:"Observation entity representation",id:"observation-entity-representation",level:4},{value:"Observation Comments",id:"observation-comments",level:4},{value:"Tag",id:"tag",level:3},{value:"TagID management",id:"tagid-management",level:4},{value:"Tag entity representation",id:"tag-entity-representation",level:4},{value:"Task",id:"task",level:3},{value:"TaskID management",id:"taskid-management",level:4},{value:"Task Types",id:"task-types",level:4},{value:"Task titles and descriptions",id:"task-titles-and-descriptions",level:4},{value:"Task entity representation",id:"task-entity-representation",level:4},{value:"Badge",id:"badge",level:3},{value:"BadgeID and BadgeInstanceID management",id:"badgeid-and-badgeinstanceid-management",level:4},{value:"Badge entity representation",id:"badge-entity-representation",level:4},{value:"Collections and business logic",id:"collections-and-business-logic",level:2},{value:"Privacy",id:"privacy",level:2},{value:"IDs",id:"ids",level:2},{value:"Normalization and caching",id:"normalization-and-caching",level:2},{value:"Root collections vs subcollections",id:"root-collections-vs-subcollections",level:2},{value:"Chat rooms",id:"chat-rooms",level:2}];function h(e){const t={a:"a",admonition:"admonition",br:"br",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.header,{children:(0,a.jsx)(t.h1,{id:"data-model",children:"Data Model"})}),"\n",(0,a.jsx)(t.p,{children:"This page explains the data model (i.e. the set of entities and their relationships) for GGC, along with a rationale for the design decisions that we've made along the way."}),"\n",(0,a.jsx)(t.h2,{id:"entities",children:"Entities"}),"\n",(0,a.jsx)(t.p,{children:'In GGC, "entity" refers to the fundamental forms of persistent data objects. Examples of entities are: "Chapter", "Garden", "Gardener", "Observation", etc.'}),"\n",(0,a.jsx)(t.p,{children:"Each entity is defined as a set of typed fields."}),"\n",(0,a.jsx)(t.p,{children:"Entities are persisted through a set of Firebase collections. In general, each entity is a document that is stored in a corresponding collection: all of the Chapter entity documents are stored in a Firebase collection called Chapters, all of the Gardener entity documents are stored in a Firebase collection called Gardeners."}),"\n",(0,a.jsx)(t.p,{children:'The GGC app implements a set of Dart "domain" classes that mirror these Firebase collections, so (for example) there is a Dart class called "Chapter" (that defines the structure of a Chapter entity), and a Dart class called "ChapterCollection" (which holds a list of Chapter entity instances and provides operations upon them).'}),"\n",(0,a.jsx)(t.h4,{id:"entity-hierarchy",children:"Entity Hierarchy"}),"\n",(0,a.jsx)(t.p,{children:"The following diagram provides a high-level overview of the entities in the data model organized into a three level hierarchy:"}),"\n",(0,a.jsx)("img",{style:{borderStyle:"solid"},src:"/img/develop/release-1.0/data-model/entity-overview.png"}),"\n",(0,a.jsx)(t.p,{children:"The diagram separates the entities into three categories:"}),"\n",(0,a.jsxs)(t.ol,{children:["\n",(0,a.jsx)(t.li,{children:'"Global-level" entities. These entities are defined at the "system-level". In other words, they can only be changed by GGC developers, and changes to these entities might involve changes to the source code, the database, and possibly redeployment of the app.'}),"\n",(0,a.jsx)(t.li,{children:'"Chapter-level" entities. These are "top-level" entities for any given chapter. These entities all include a chapterID field. Each user is always associated with a single Chapter, and thus can only "see" the entities with a matching chapterID. The Chapter-level entities are visible only to the entities in their Chapter. They are normally downloaded and cached in the client application upon login.'}),"\n",(0,a.jsx)(t.li,{children:'"Garden-level" entities. These entities are all specific to a single Garden, and include both a chapterID and a gardenID. Garden-level entities are only visible to entities within their Chapter. In addition, Garden-level entities are only downloaded and cached in the client application when the user explicitly navigates to a Garden\'s details page.'}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'This diagram can also be used to understand the relative numbers of entities that a given client must manipulate. Each of the "Global-level" entity will have dozens to hundreds of instances, and so it is practical for the client application to cache them locally without a large performance impact.'}),"\n",(0,a.jsx)(t.p,{children:'Since each User is associated with a single Chapter, the number of "Chapter-level" entity instances visible to a User is not expected to exceed several hundred to a thousand. This means it is practical for the client application to cache all "visible" Chapter-level entities locally.'}),"\n",(0,a.jsx)(t.p,{children:'We expect each User to be associated with one to a dozen Gardens. Each Garden might have hundreds to thousands of Plantings. This means it is practical for the client application to cache the "Garden-level" entities that they are associated with.'}),"\n",(0,a.jsxs)(t.p,{children:['The goal of this design is to create "chapter-level" and "garden-level" namespaces, such that GeoGardenClub can scale to hundreds of Chapters, where each Chapter contains hundreds of gardens, and where each Garden contains hundreds of Plantings (and other Garden-specific entities), all while providing a fast, intuitive, and responsive application for each user. Our design means that the GGC database can grow to millions of documents while individual client apps require access to only thousands of documents.',(0,a.jsx)(t.br,{}),"\n","This design does have a potential problem: what if a Chapter becomes wildly popular and grows to many hundreds of members? It is possible that the performance of the client application can degrade if the number of members (and thus gardens) in a single Chapter becomes too large."]}),"\n",(0,a.jsx)(t.p,{children:'To address this potential problem, the data model is designed to facilitate partitioning of large Chapters into multiple smaller Chapters in the event that the number of members becomes too large. For example, the initial definition of a Chapter may comprise 8 postal (zip) codes, corresponding to all the postal codes in that country. But if that Chapter becomes too large, we could split it into two Chapters, each defined with 4 postal codes (or one with 3 postal codes and one with 5 postal codes, depending upon the concentration of members in each postal code). Our data model does not currently allow Chapter definition "below" the level of a postal code, so the smallest possible Chapter in GeoGardenClub would be one defined by a single postal code.'}),"\n",(0,a.jsx)(t.p,{children:"We foresee an annual end-of-year review, where we see if any Chapters are reaching a size where it would be appropriate to split them up into smaller Chapters. By doing it in Winter (at least for the Northern Hemisphere), such Chapter reorganization should have less impact on the Gardeners."}),"\n",(0,a.jsx)(t.p,{children:"To facilitate Chapter splitting, the IDs associated with Garden-level entities do not encode the chapterID, but instead the two character (alpha2) country code and the postal code. This allows Garden-level data to more easily migrate to new Chapters without needing to change their entity IDs."}),"\n",(0,a.jsx)(t.h4,{id:"entity-dependencies",children:"Entity dependencies"}),"\n",(0,a.jsx)(t.p,{children:"The following diagram presents an alternative perspective on the entities. In this case, there is a line between two entities when there is a relationship between them; in other words, one of the entities refers to the other with a foreign key (i.e. ID) field."}),"\n",(0,a.jsx)("img",{style:{borderStyle:"solid"},src:"/img/develop/release-1.0/data-model/entity-dependencies.png"}),"\n",(0,a.jsx)(t.p,{children:"The primary goal of this diagram is to make it clear that there is a fairly rich set of dependencies among the entities in this data model."}),"\n",(0,a.jsx)(t.p,{children:'This is a positive thing, because it means that there are many different and interesting ways to "slice and dice" the data.'}),"\n",(0,a.jsx)(t.p,{children:"It also illustrates why we have chosen to implement the data model as a set of top-level collections. The many different relationships argue against the use of subcollections."}),"\n",(0,a.jsx)(t.p,{children:"Let's now turn to a more detailed description of the entities in the data model."}),"\n",(0,a.jsx)(t.h3,{id:"chapter",children:"Chapter"}),"\n",(0,a.jsx)(t.p,{children:"The Chapter entity defines a geographic region based on a country (represented as a two character (alpha-2) country code), and a set of one or more postal (zip) codes. GGC ensures that Chapter instances partition the world: every pair of (country code, postal code) is mapped to exactly one Chapter."}),"\n",(0,a.jsx)(t.h4,{id:"chapterid-management",children:"ChapterID management"}),"\n",(0,a.jsx)(t.p,{children:"A Firebase collection called ChapterZipMap will provide a default mapping of US postal (i.e. zip) codes to chapterIDs. This mapping initially defines a one-to-one correspondence between US counties and GGC Chapters."}),"\n",(0,a.jsx)(t.p,{children:"Outside of the US, each (country code, postal code) pair will be its own Chapter. This is not optimal but it provides a way to make GGC immediately available to users outside the US without constructing a world-wide ChapterPostalCodeMap. We can add this later without any change to the data model."}),"\n",(0,a.jsxs)(t.p,{children:["Unlike most other entity IDs, the complete set of chapterIDs is defined in advance in GGC. In other words, we can compute all of the chapterIDs on earth, and they do not depend upon the number of users or their behavior. In contrast, there is no ",(0,a.jsx)(t.em,{children:"a priori"})," limit to the number of (say) Planting IDs."]}),"\n",(0,a.jsxs)(t.p,{children:["While chapterIDs are finite, they are not necessarily ",(0,a.jsx)(t.em,{children:"fixed"})," in terms of their numbers and the geographic regions that they encompass. For US Chapters, we can change the set of chapters by changing the entries in the ChapterZipMap. For example, while our initial approach is to implement a one-to-one correspondence between US chapters and US counties, we could in future change the ChapterZipMap so that a single US county could have multiple Chapters, or multiple counties could be combined into a single Chapter, or some other approach. (Changing chapter geographic boundaries requires more than just changing the ChapterZipMap; the point here is that our representation does not lock us in to our initial definition for Chapters.) The only hard constraint is that each postal code is assigned to one and only one Chapter."]}),"\n",(0,a.jsxs)(t.p,{children:["ChapterIDs have the format ",(0,a.jsx)(t.code,{children:"chapter--"}),'. In the case of a US Chapter, an example Chapter ID is: "chapter-US-001". In the case of a non-US Chapter, an example Chapter ID is "chapter-CA-V6K1G8".']}),"\n",(0,a.jsx)(t.p,{children:"To support readability in this document, we will use US chapters and the chapterCodes will be numeric."}),"\n",(0,a.jsx)(t.h4,{id:"user-registration-and-chapter-assignment",children:"User registration and chapter assignment"}),"\n",(0,a.jsx)(t.p,{children:'New user registration works as follows. If they supply "US" as their country code, then the system will query the ChapterZipMap collection to determine their chapterID based on the postal (zip) code that they also supply. If no Chapter entity exists yet with that chapterID, it will be created with the chapterID provided by the ChapterZipMap collection.'}),"\n",(0,a.jsxs)(t.p,{children:["If the new user supplies a non-US country code, then the ChapterZipMap is not consulted. Instead, the chapterID is defined as ",(0,a.jsx)(t.code,{children:"chapter--"}),". If no Chapter entity exists yet corresponding to that ChapterID, then it will be created."]}),"\n",(0,a.jsxs)(t.p,{children:["Note that ",(0,a.jsx)(t.a,{href:"https://tosbourn.com/list-of-countries-without-a-postcode/",children:"some countries do not have a postal code"}),'. In this case, we will create a default postal code (i.e. "000") for those countries and not request it from the user if they select one of those countries. This implies that for those countries, there will be only one chapter for the entire country. Since most of those countries are pretty small, that seems like a reasonable design decision.']}),"\n",(0,a.jsx)(t.admonition,{title:"The 1.0 release works differently",type:"info",children:(0,a.jsx)(t.p,{children:"The 1.0 release will only be distributed to users in Whatcom Country, WA, and so the registration mechanism will be simplified. See below for details."})}),"\n",(0,a.jsx)(t.h4,{id:"chapterid-as-firebase-index",children:"ChapterID as Firebase index"}),"\n",(0,a.jsx)(t.p,{children:"As will be seen, many entities contain a chapterID field. When a client retrieves data from Firebase, it will normally request all of the documents where the chapterID field is the one associated with their chapter. This is the primary way in which GGC can scale. For this to work effectively, we must define an index on the chapterID field for all collections in which the entities have that field."}),"\n",(0,a.jsx)(t.h4,{id:"chapter-entity-representation",children:"Chapter entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Chapter(\n {required String chapterID, // 'chapter-US-001', or 'chapter-CA-V6K1G8'\n required String name, // 'Whatcom-WA', or 'CA-V6K1G8'\n required String countryCode, // 'US', 'CA'\n required List postalCodes} // ['98225', '98226'], or ['V6K1GB']\n)\n"})}),"\n",(0,a.jsx)(t.h4,{id:"projected-release-20-changes",children:"Projected Release 2.0 changes"}),"\n",(0,a.jsx)(t.p,{children:'In Release 2.0, users will be able to see information about Chapters other than their own. To implement this, we will expand the representation of the Chapter entity with "cached" information, perhaps the number of gardeners, the Chapter badges awarded to that chapter, and so forth. Release 2.0 might also include climate-related features, which might result in associating a list of hardiness zones with each Chapter entity.'}),"\n",(0,a.jsx)(t.h3,{id:"user",children:"User"}),"\n",(0,a.jsx)(t.p,{children:"A User entity is created for all of the people who have created an account with the system."}),"\n",(0,a.jsx)(t.h4,{id:"users-vs-gardeners",children:"Users vs Gardeners"}),"\n",(0,a.jsx)(t.p,{children:"Note that all User entities will also have a Gardener entity, but not vice-versa: not all Gardener entities have a corresponding User entity. This is because commercial seed vendors won't generally have an account on the system, but they are represented within the system as Gardener entities."}),"\n",(0,a.jsx)(t.p,{children:"Every User is associated with a unique email address, which is their UserID. (Their email is also used for their gardenerID.)"}),"\n",(0,a.jsx)(t.h4,{id:"userid-management",children:"UserID management"}),"\n",(0,a.jsx)(t.p,{children:"UserIDs are the email addresses of the user. We obtain the email as part of registration."}),"\n",(0,a.jsx)(t.h4,{id:"user-onboarding",children:"User onboarding"}),"\n",(0,a.jsx)(t.p,{children:"After a user successfully registers with the system using the Firebase authentication procedures, they are logged in. Whenever a user logs in, the system checks to see if there is a User document associated with the email address of the currently logged in user. If there is no User document for that email, then the system displays an Onboarding screen."}),"\n",(0,a.jsx)(t.p,{children:"The onboarding screen is essentially a form that must be successfully filled out in order for the logged in user to proceed to their home page (as well as to any other areas of the application)."}),"\n",(0,a.jsx)(t.p,{children:"The form provides fields for the user's:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"Name"}),"\n",(0,a.jsx)(t.li,{children:"Username"}),"\n",(0,a.jsx)(t.li,{children:"Country"}),"\n",(0,a.jsx)(t.li,{children:"Postal (Zip) code"}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"In addition, the user can provide a picture at this time if they want."}),"\n",(0,a.jsxs)(t.admonition,{title:"1.0 Release modifications",type:"info",children:[(0,a.jsx)(t.p,{children:"For the initial 1.0 release:"}),(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'The country field will be a read-only drop-down and "United States" will be selected. It returns the alpha2 code for the United States (i.e. "US")'}),"\n",(0,a.jsx)(t.li,{children:"The Postal (Zip) Code input field will be a pull-down list of postal codes associated with Whatcom, Washington."}),"\n"]}),(0,a.jsx)(t.p,{children:"These modifications to the Onboarding screen guarantee that 1.0 test users will be associated with the Whatcom-WA Chapter, and allow us to avoid the need to design and implement the ChapterZipMap and associated processing."})]}),"\n",(0,a.jsx)(t.p,{children:"Once the form is successfully filled out, a User and Gardener document is created for that email address. If those documents are created successfully, then the application displays the Home screen for that User."}),"\n",(0,a.jsx)(t.h4,{id:"user-entity-representation",children:"User entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory User(\n {required String userID, // 'johnson@hawaii.edu'\n required String chapterID, // 'chapter-US-001'\n required String name, // 'Philip Johnson'\n required String username, // '@fiveoclockphil'\n required String country, // 'US'\n required String postalCode, // '98225'\n required String uid, // '22e9fe1b-445c-4523-89c2-4450244f1959'\n String? pictureURL} // null, or 'https://firebasestorage.googleapis.com/v0/...'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"gardener",children:"Gardener"}),"\n",(0,a.jsx)(t.p,{children:'There is one Gardener entity for each Chapter member and vendor in GGC. This entity is designed to represent two distinct classes of gardeners: (1) "normal" home gardeners (who are Chapter members) and (2) commercial seed vendors (who are not (normally) Chapter members).'}),"\n",(0,a.jsx)(t.h4,{id:"chapter-members-vs-vendors",children:"Chapter members vs Vendors"}),"\n",(0,a.jsx)(t.p,{children:'The benefit of having the Gardener entity represent both Chapter members as well as commercial seed vendors is that it results in a uniform mechanism in the app to support "seed providers". Any Gardener (which can either be a normal home gardener or a commercial seed vendor) owns a Garden which contains Plantings which (may or may not) produce seeds that are available within the Chapter.'}),"\n",(0,a.jsx)(t.p,{children:'This does create some UI complexity, in that commercial seed vendors do not appear in the list of "Gardeners" and instead appear in the UI as "Vendors". Underneath, however, commercial seed vendors will (like Chapter members) have a Gardener entity, a Garden entity, and for each seed that someone in the Chapter uses, there will be a Seed entity and a Planting entity. (To as great an extent as possible, all of this Vendor entity management is managed internally and hidden from the UI.)'}),"\n",(0,a.jsx)(t.p,{children:"The Gardener entity indicates that it is representing a Vendor by setting the isVendor flag to true. If that flag is true, then the vendorName, vendorShortName, and vendorUrl fields must be non-null."}),"\n",(0,a.jsx)(t.p,{children:"The Vendors in a Chapter are crowd-sourced, which means any Chapter member can create a new Vendor. When a Vendor is created, they are given the country and postal code of the member who defined them. This is necessary so that their implicitly defined Garden and Plantings can have Chapter-appropriate ID strings."}),"\n",(0,a.jsx)(t.h4,{id:"cached-values",children:"Cached values"}),"\n",(0,a.jsx)(t.p,{children:'We want to provide information about Gardeners such as the crops and varieties that they are growing in the Index screens, and for performance reasons, we want to provide this information without having to retrieve all of the Planting instances associated with their gardens. To do this, we "cache" the cropIDs and varietyIDs associated with this gardener in this entity.'}),"\n",(0,a.jsx)(t.p,{children:'By "associated", we mean the crops and varieties in the garden(s) for which this gardener is an owner.'}),"\n",(0,a.jsx)(t.h4,{id:"badge-attestations",children:"Badge attestations"}),"\n",(0,a.jsx)(t.p,{children:'Certain badges require Gardeners to "attest" to having performed activities. The Gardener entity contains an attestations field that holds strings indicating what has been attested to.'}),"\n",(0,a.jsx)(t.h4,{id:"gardenerid-management",children:"GardenerID management"}),"\n",(0,a.jsxs)(t.p,{children:["GardenerIDs are the email addresses of the gardener. In the case of registered users, the UserID is the same as the GardenerID. In the case of Vendors, the GardenerID is the contact email for the vendor company (for example, ",(0,a.jsx)(t.a,{href:"mailto:info@johnnyseeds.com",children:"info@johnnyseeds.com"}),")."]}),"\n",(0,a.jsx)(t.h4,{id:"gardener-entity-representation",children:"Gardener entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Gardener(\n {required String gardenerID, // 'johnson@hawaii.edu'\n required String chapterID, // 'chapter-US-001'\n required List cachedCropIDs, // ['crop-US-001-203-9987']\n required List cachedVarietyIDs, // ['variety-US-001-305-8765']\n required String country, // 'US'\n required String postalCode, // '98225'\n required List attestations, // ['PermacultureWorkshop']\n @Default(false) bool isVendor, // true, or false\n String? vendorName, // null, or 'Johnnys Seeds and Supplies'\n String? vendorShortName, // null, or 'Johnnys'\n String? vendorURL} // true, or false\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"garden",children:"Garden"}),"\n",(0,a.jsx)(t.p,{children:"The Garden entity represents a plot of land (or maybe even just some pots) that can hold Plantings over one or more years."}),"\n",(0,a.jsx)(t.h4,{id:"gardenid-management",children:"GardenID management"}),"\n",(0,a.jsx)(t.p,{children:"GardenIDs are generated dynamically when a Chapter member defines a new Garden or when a Chapter member defines a new Vendor (which implicitly results in the creation of a new Garden)."}),"\n",(0,a.jsxs)(t.p,{children:["GardenIDs have the format ",(0,a.jsx)(t.code,{children:"garden----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"The GardenID embeds the country code and postal code associated with the ownerID. Note that this might not be the same postal code as the one associated with the physical location of the garden! We do this in order to ensure that if a Chapter's set of postal codes is reorganized, then the Gardens owned by a Gardener will always end up in the same Chapter as their owner."}),"\n",(0,a.jsx)(t.p,{children:'To support readability in this document and initial development, the gardenNum starts at "101" for each chapter.'}),"\n",(0,a.jsx)(t.h4,{id:"field-notes",children:"Field Notes"}),"\n",(0,a.jsxs)(t.p,{children:["The form field for vendor name entry imposes validation criteria. See ",(0,a.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/lib/features/common/input-fields/validators.dart",children:"validators.dart"})," for details."]}),"\n",(0,a.jsx)(t.p,{children:"The Garden name must be unique within a Chapter."}),"\n",(0,a.jsx)(t.p,{children:"The cachedYears value is based on the StartDate for the Plantings associated with the Garden."}),"\n",(0,a.jsx)(t.h4,{id:"cached-values-1",children:"Cached values"}),"\n",(0,a.jsx)(t.p,{children:"Each Garden entity caches the CropIDs, VarietyIDs, years, and the number of Plantings. This allows the Index screens to show this information about Gardens without needing to retrieve and process Plantings."}),"\n",(0,a.jsx)(t.p,{children:"In addition, whenever there is a change to the Plantings associated with this Garden, the lastUpdated field is set to the current time. This allows the community to see which Gardens in their Chapter are active."}),"\n",(0,a.jsx)(t.h4,{id:"badge-attestations-1",children:"Badge attestations"}),"\n",(0,a.jsx)(t.p,{children:'Certain badges require Gardeners to "attest" to their Garden having certain properties. The Garden entity contains an attestations field with strings indicating the properties that they have attested to.'}),"\n",(0,a.jsx)(t.h4,{id:"garden-entity-representation",children:"Garden entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Garden(\n {required String gardenID, // 'garden-US-98225-101-4567'\n required String chapterID, // 'chapter-US-001'\n required String name, // 'Kale is for Kids'\n required String ownerID, // 'jessie@gmail.com'\n required List cachedCropIDs, // ['crop-US-001-201-9876']\n required List cachedVarietyIDs, // ['variety-US-001-302-7865']\n required List cachedYears, // [2023, 2022]\n required int cachedNumPlantings, // 231\n required List attestations, // ['ClimateVictory', 'PesticideFree', 'CommunityOrSchool']\n String? pictureURL, // null, 'https://firebasestorage.googleapis.com/v0/...'\n String? plotPlanURL, // null, 'https://firebasestorage.googleapis.com/v0/...' \n DateTime? lastUpdate, // null (for vendors), '2023-03-19T12:19:14.164090' \n @Default(false) bool isVendor} // true, false\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"editor",children:"Editor"}),"\n",(0,a.jsx)(t.p,{children:'The owner of a Garden can add other Chapter members as "editors", which enables those users to edit the Plantings and other information associated with a Garden.'}),"\n",(0,a.jsx)(t.p,{children:"There are some things Editors cannot do. For example, they cannot delete the garden. Only the owner can do that."}),"\n",(0,a.jsx)(t.p,{children:"To earn a Gardener Badge, only the data associated with Gardens that you own is used. Being an Editor on a Garden does not support Badge processing."}),"\n",(0,a.jsx)(t.p,{children:"In addition, when displaying the Crops and Varieties associated with a Gardener, only those Crops and Varieties for the Gardens that you own are displayed. The Crops and Varieties for Gardens for which you are an Editor are not included."}),"\n",(0,a.jsx)(t.h4,{id:"editorid-management",children:"EditorID management"}),"\n",(0,a.jsx)(t.p,{children:"Editor entities are created or deleted when the owner of a Garden edits the Editor field of the Garden Details form."}),"\n",(0,a.jsxs)(t.p,{children:["EditorIDs have the format ",(0,a.jsx)(t.code,{children:"editor-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"EditorNums start at 001 for each garden."}),"\n",(0,a.jsx)(t.h4,{id:"editor-entity-representation",children:"Editor entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Editor(\n {required String editorID, // 'editor-US-98225-102-001-5231'\n required String gardenID, // 'garden-US-98225-102-6789'\n required String chapterID, // 'chapter-US-001'\n required String gardenerID} // 'johnson@hawaii.edu'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"bed",children:"Bed"}),"\n",(0,a.jsx)(t.p,{children:"Each Garden consists of a number of Beds. An owner can edit the name of an existing Bed, and can add a new Bed to a Garden, but cannot delete a Bed if there are any Plantings associated with it."}),"\n",(0,a.jsx)(t.h4,{id:"bedid-management",children:"BedID management"}),"\n",(0,a.jsxs)(t.p,{children:["BedIDs have the format ",(0,a.jsx)(t.code,{children:"bed-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"BedNums start at 001 for each garden."}),"\n",(0,a.jsx)(t.h4,{id:"bed-entity-representation",children:"Bed entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" const factory Bed(\n {required String bedID, // 'bed-US-98225-101-001-5634'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-101-6789'\n required String name, // '02'\n String? gardenerID, // The owner of the garden, i.e. 'johnson@hawaii.edu'.\n } \n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"family",children:"Family"}),"\n",(0,a.jsx)(t.p,{children:'The Family entity specifies the botanical family associated with one or more Crops (and implicitly, Varieties). For example, the "Nightshade" family groups together Tomatoes, Potatoes, and Peppers. Each Crop is associated with exactly one Family.'}),"\n",(0,a.jsx)(t.p,{children:"Family data is useful to facilitate planning issues including crop rotation and companion planting. However, in Release 1.0, we do not provide any explicit support for rotation or companion planning."}),"\n",(0,a.jsx)(t.p,{children:'The Family entity is a "global" collection in GGC. In other words, it does not include a ChapterID; every Chapter will download this collection, and it cannot be edited except by developers.'}),"\n",(0,a.jsx)(t.h4,{id:"familyid-management",children:"FamilyID management"}),"\n",(0,a.jsxs)(t.p,{children:["FamilyIDs have the format ",(0,a.jsx)(t.code,{children:"family-"}),". The set of Family entity documents is defined in advance by GGC developers, and editing this collection requires direct interaction with the database."]}),"\n",(0,a.jsx)(t.p,{children:"FamilyNums start at 001."}),"\n",(0,a.jsx)(t.h4,{id:"family-entity-representation",children:"Family entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Family(\n {required String familyID, // 'family-001'\n required String formal, // 'Amryllidaceae'\n required String common, // 'Allium'\n required String examples} // 'onion, leek, garlic, shallot'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"crop",children:"Crop"}),"\n",(0,a.jsx)(t.p,{children:'The Crop entity specifies a type of plant independent of its Variety. For example, "Tomato" is a Crop, while "Big Boy Tomato" is a specific Variety of Tomato.'}),"\n",(0,a.jsx)(t.p,{children:"Each Crop is associated with exactly one Family entity. A Crop can be associated with many Varieties."}),"\n",(0,a.jsx)(t.p,{children:'Each Chapter is responsible for "crowd-sourcing" the set of Crop entities. This puts on burden on early Chapter members to define Crops. We estimate that most chapters will need to define between 50 and 100 Crop entities.'}),"\n",(0,a.jsxs)(t.p,{children:["The reason we do not provide a global collection of Crops is because a single collection containing all the crops grown world-wide would have several hundred entities, many of which would not be relevant to the Chapter. We want each Chapter's UI to show only the Crops (and Varieties, and Seeds) that are ",(0,a.jsx)(t.em,{children:"actually being grown"})," in that Chapter. We hypothesize that the benefits of focusing on what is actually being grown outweigh the cost of crowd-sourced management."]}),"\n",(0,a.jsx)(t.h4,{id:"cropid-management",children:"CropID management"}),"\n",(0,a.jsxs)(t.p,{children:["CropIDs have the format ",(0,a.jsx)(t.code,{children:"crop----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"CropIDs embed the chapter's country code and chapterCode. (ChapterCodes could be a number like '001' in the case of a US Chapter, or a postal code like 'VNZ76T' in the case of a non-US chapter.)"}),"\n",(0,a.jsx)(t.p,{children:"In the event that a Chapter is divided into two or more smaller chapters, each of the new Chapters needs a copy of the Crop collection where the IDs have been changed to embed the new chapterCode. This will require a pass through all of the Garden-level entities to update the value of their cropID fields to the new string value."}),"\n",(0,a.jsx)(t.p,{children:"CropNums start at 201 for each chapter."}),"\n",(0,a.jsx)(t.h4,{id:"crop-entity-representation",children:"Crop entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Crop(\n {required String cropID, // 'crop-US-001-201-3452'\n required String chapterID, // 'chapter-US-001'\n required String familyID, // 'family-001'\n required String name} // 'Tomato'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"variety",children:"Variety"}),"\n",(0,a.jsx)(t.p,{children:'Variety is a specific kind of Crop which can actually be grown, i.e. it has seeds. For example, a seed packet such as "Tomato (Sun Gold)" specifies the crop ("Tomato") and the Variety ("Sun Gold").'}),"\n",(0,a.jsx)(t.p,{children:'In some cases, the Variety associated with a given seed might not be known. In those cases, by convention, the Variety name can be specified as "Unknown". (It is not, however, appropriate to create a Crop called "Unknown". If you plant some seeds that you know absolutely nothing about, you should wait until they germinate and you can identify their Crop before you can enter data about it into GGC!)'}),"\n",(0,a.jsx)(t.p,{children:"Note that it is possible (and common) for multiple gardeners (either home or commercial vendors) to produce seeds of the same Variety."}),"\n",(0,a.jsx)(t.h4,{id:"varietyid-management",children:"VarietyID management"}),"\n",(0,a.jsxs)(t.p,{children:["VarietyIDs have the format ",(0,a.jsx)(t.code,{children:"variety----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"Like CropIDs, VarietyIDs embed the country code and chapterCode. (Like CropIDs, ChapterCodes could be a number like '001' in the case of a US Chapter, or a postal code like 'VNZ76T' in the case of a non-US chapter.)"}),"\n",(0,a.jsx)(t.p,{children:"In the event that a Chapter is divided into two or more smaller chapters, each of the new Chapters needs a copy of the Variety collection with the updated chapterCode. This will require a pass through all of the Garden-level entities to update their varietyID fields to the new string value."}),"\n",(0,a.jsx)(t.p,{children:"VarietyNums start at 301 for each chapter."}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-1",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Note that we cache the Crop Name because it will rarely, if ever, change and it is useful to have it in the Variety document so that we can return the full name without needing the Crop collection."}),"\n",(0,a.jsx)(t.p,{children:"That implies, however, that if the name of a Crop is ever changed, then we must find all of the Variety documents associated with that cropID and update the cachedCropName field. This is an acceptable trade-off."}),"\n",(0,a.jsx)(t.h4,{id:"variety-entity-representation",children:"Variety entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Variety(\n {required String varietyID, // 'variety-US-001-302-7654'\n required String chapterID, // 'chapter-US-001'\n required String cropID, // 'crop-US-001-203-2354'\n required String cachedCropName, // 'Asparagus'\n bool? isGold, // If present and set to true, the variety has \"gold\" status.\n required String name} // 'Jersey Knight' \n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"planting",children:"Planting"}),"\n",(0,a.jsx)(t.p,{children:"A Planting represents a set of plants of the same variety (or crop), planted in a single bed, all with the same approximate timings (i.e. sow date, transplant date, first harvest date, etc.)."}),"\n",(0,a.jsx)(t.p,{children:"If the same variety (or crop) is planted in two different beds, then this must be represented by two Planting instances."}),"\n",(0,a.jsx)(t.p,{children:'It is common during the garden planning process to first design the garden at the "crop" level, and then later refine the plan by specifying the specific variety to be planted. To support this incremental planning process, you can create a Planting instance and specify only the Crop, not the Variety.'}),"\n",(0,a.jsx)(t.h4,{id:"plantings-and-seeds",children:"Plantings and seeds"}),"\n",(0,a.jsx)(t.p,{children:"One innovative feature of GGC is that we provide an explicit representation of the seeds grown by a Planting. Here is how it manifests in the Planting entity."}),"\n",(0,a.jsx)(t.p,{children:"In each Planting document, we two optional fields called sowSeedID and harvestSeedID. The sowSeedID represents the seeds from which this Planting was grown (if known), and the harvestSeedID represents the seeds produced by this Planting (if any were produced)."}),"\n",(0,a.jsx)(t.p,{children:"Finally, there is a boolean field called seedsAvailable. If true, this means not only that the Planting grew seeds (and thus there is a harvestSeedID), but that this gardener is willing to share these seeds with others in the Chapter. When seedsAvailable is true, then other Gardeners looking at the Variety associated with this planting will see that they can contact the owner of this Garden to request seeds from this Planting. They might also be able to see the Outcome data for this Planting, which provides some evidence for the future success of these seeds when grown."}),"\n",(0,a.jsx)(t.h4,{id:"plantingid-management",children:"PlantingID management"}),"\n",(0,a.jsxs)(t.p,{children:["PlantingIDs have the format ",(0,a.jsx)(t.code,{children:"planting-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"The country and postal code fields in the ID must match those fields in the gardenID associated with this Planting."}),"\n",(0,a.jsx)(t.p,{children:"Since, over a period of years, a single garden can result in over a thousand plantings, we generally use a four digit number for the plantingNum."}),"\n",(0,a.jsx)(t.p,{children:"PlantingNums start at 1001 for each garden."}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-2",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Validators should guarantee that startDate < transplantDate < firstHarvestDate < endHarvestDate < pullDate."}),"\n",(0,a.jsx)(t.p,{children:"All Plantings must have a startDate, a pullDate, and a bedID. These values are required so that the Planting can be displayed as a horizontal bar in the Garden details view."}),"\n",(0,a.jsx)(t.p,{children:"If a Gardener wants to indicate that seeds are available, they must provide the Variety for this Planting."}),"\n",(0,a.jsx)(t.p,{children:"If the gardener sets usedGreenhouse to true, then they should (eventually) record a transplantDate, although this is not mandatory."}),"\n",(0,a.jsx)(t.p,{children:'Note that if both a cropID and varietyID is provided, then the varietyID must "match" the cropID. Put another way, the associated Variety\'s cropID field should match the Planting\'s cropID field. (Put yet another way, this would be illegal: a Planting in which the Crop is "Corn" but the Variety is "Big Boy (Tomato)"). The UI for defining and managing Planting entities will enforce this by only showing the Varieties associated with the currently selected Crop.'}),"\n",(0,a.jsx)(t.h4,{id:"planting-entity-representation",children:"Planting entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"factory Planting(\n {required String plantingID, // 'planting-US-98225-102-1001-7645'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-102-5678'\n required String cropID, // 'crop-US-001-202-9432'\n required String cachedCropName,// 'Bean'\n required String bedID, // 'bed-US-98225-102-003-4823'\n required String cachedBedName, // '02'\n required DateTime startDate, // '2023-03-19T12:19:14.164090'\n required DateTime pullDate, // '2023-07-19T12:19:14.164090'\n String? varietyID, // null, 'variety-US-001-310-7645'\n String? cachedVarietyName, // null, 'Big Boy'\n String? outcomeID, // null, 'outcome-US-98225-102-1001-3472'\n DateTime? transplantDate, // null, '2023-04-19T12:19:14.164090'\n DateTime? firstHarvestDate, // null, '2023-05-19T12:19:14.164090'\n DateTime? endHarvestDate, // null, '2023-06-19T12:19:14.164090'\n String? sowSeedID, // null, 'seed-US-98225-102-001-3563'\n String? harvestSeedID, // null, 'seed-US-98225-102-005-2185'\n @Default(false) bool usedGreenhouse, // true, false \n @Default(false) bool isVendor, // true, false\n @Default(false) bool seedsAvailable} // true, false\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"outcome",children:"Outcome"}),"\n",(0,a.jsx)(t.p,{children:"Outcome data is gardener-supplied information about the result of a single Planting. We want to specify planting results in a way that:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"Is useful and actionable for gardeners,"}),"\n",(0,a.jsx)(t.li,{children:"Captures important properties of a planting,"}),"\n",(0,a.jsx)(t.li,{children:"Is relatively easy to provide,"}),"\n",(0,a.jsx)(t.li,{children:"Is interpreted in a relatively consistent manner by different gardeners,"}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'To support these requirements, we define five outcome types: germination, yield, flavor, pest and disease resistance, and appearance. Each planting can receive a "grade" for each of these outcome types on a five point scale. The following table presents the definitions for each scale value for each outcome type.'}),"\n",(0,a.jsxs)(t.table,{children:[(0,a.jsx)(t.thead,{children:(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.th,{}),(0,a.jsx)(t.th,{children:"1"}),(0,a.jsx)(t.th,{children:"2"}),(0,a.jsx)(t.th,{children:"3"}),(0,a.jsx)(t.th,{children:"4"}),(0,a.jsx)(t.th,{children:"5"})]})}),(0,a.jsxs)(t.tbody,{children:[(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Germination"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"None."})," No germination."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," ~25% germination."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," ~50% germination."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," ~75% germination."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," >90% germination.."]})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Yield"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"None."})," Died and/or no food"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," Less food than expected"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," Expected amount of food"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," More food than expected"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," TWay more food than expected"]})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Flavor"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Bad."})," Unappealing flavor"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," Bland flavor"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," Expected flavor."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," Enjoyable flavor"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," Awesome flavor."]})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Pest and disease resistance"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Very poor."})," >90% damaged"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," ~50% damaged"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," < 25% damaged"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," Very few damaged"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," No damage."]})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Appearance"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Very poor."})," >90% ugly"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," ~60% ugly"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," ~60% not ugly"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," ~60% beautiful"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," >90% beautiful"]})]})]})]}),"\n",(0,a.jsx)(t.p,{children:'In addition, an Outcome type can have a value of "0", which means there is no data regarding that type of outcome.'}),"\n",(0,a.jsx)(t.h4,{id:"outcomeid-management",children:"OutcomeID management"}),"\n",(0,a.jsxs)(t.p,{children:["OutcomeIDs have the format ",(0,a.jsx)(t.code,{children:"outcome-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"Each Outcome entity is associated with exactly one Planting entity. (Note that the converse is not true: a Planting entity need not be associated with an Outcome entity, since the Gardener might not choose to record any Outcome data.)"}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-3",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Outcomes cache the cropID and varietyID associated with their Planting. This is to allow Index and View widgets to display Outcome data without having to retrieve Plantings from the database."}),"\n",(0,a.jsx)(t.p,{children:"Outcome value must be integers between 0 (indicating no data) and 5 (indicating Excellent)."}),"\n",(0,a.jsx)(t.h4,{id:"outcome-entity-representation",children:"Outcome entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Outcome(\n {required String outcomeID, // 'outcome-US-98225-102-1001-5218'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-102-6789'\n required String plantingID, // 'planting-US-98225-102-1001-9213'\n required String cachedCropID, // 'crop-US-001-245-4376'\n required String cachedVarietyID, // 'variety-US-001-321-3214'\n @Default(0) int germination, // 0-5\n @Default(0) int yieldd, // 0-5 (yield is a reserved word)\n @Default(0) int flavor, // 0-5\n @Default(0) int resistance, // 0-5\n @Default(0) int appearance} // 0-5\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"seed",children:"Seed"}),"\n",(0,a.jsx)(t.p,{children:"The ability to save and share seeds within a Chapter is a significant core value proposition for GGC."}),"\n",(0,a.jsx)(t.p,{children:'By "seed", we don\'t mean each individual, tiny seed. We mean the set of all seeds harvested from a planting in a garden in a particular season, or the set of seeds in a seed packet from a commercial vendor.'}),"\n",(0,a.jsxs)(t.p,{children:["Our data model enables us to represent both seeds that are locally produced by gardeners as well as seeds that are produced by vendors. Because a Planting can represent both the seeds that were used to grow it (in the field ",(0,a.jsx)(t.code,{children:"sowSeedID"}),") as well as the seeds that it produced and could be used to grow a new Planting in a subsequent season (in the field ",(0,a.jsx)(t.code,{children:"harvestSeedID"}),'), we get the ability to track the "provenance" of a seed:']}),"\n",(0,a.jsx)("img",{style:{borderStyle:"solid"},src:"/img/develop/release-1.0/data-model/seed-provenance.png"}),"\n",(0,a.jsx)(t.h4,{id:"seedid-management",children:"SeedID management"}),"\n",(0,a.jsxs)(t.p,{children:["SeedIDs have the format ",(0,a.jsx)(t.code,{children:"seed-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"SeedNums is typically taken from the plantingNum of the Planting from which the Seed was harvested."}),"\n",(0,a.jsx)(t.p,{children:"The country and postal code fields are taken from the Planting that this seed was harvested from. This is to ensure that if a Chapter is reorganized, the Seed will move with the Planting it was harvested from."}),"\n",(0,a.jsx)(t.admonition,{title:"Chapter reorganization and seeds",type:"warning",children:(0,a.jsx)(t.p,{children:'Note that Seeds harvested from one postal code in a Chapter can be sowed in another postal code in a Chapter. This means that if a Chapter is split up into two sub-Chapters, there is the possibility that the original Seed will need to be "cloned" into the two sub-Chapters.'})}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-4",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Seed instances cache the gardenerID, cropID, varietyID, cropName, and seedsAvailable field values from the Planting from which they were harvested."}),"\n",(0,a.jsx)(t.p,{children:"A Seed instance is always associated with a single Planting. Each Seed has a plantingID field indicating the Planting from which the Seed was harvested. In addition, that Planting has a harvestSeedID field which points to this Seed. So, there is a bi-directional mapping."}),"\n",(0,a.jsx)(t.p,{children:"A Seed instance can also be associated with one or more additional Plantings as the seed from which the Planting was grown. In this case, the Seed's ID appears in the Planting in the sowSeedID field. Those Plantings do not have to be in the same Garden (in fact, they will often be in a different garden)."}),"\n",(0,a.jsx)(t.p,{children:"The Seed entity provides information about the Planting from which it was harvested (but has no information about where/when it was used to sow new Plantings). This information includes the plantingID, gardenerID, cropID, cropName, varietyID, varietyName and seedsAvailable. Providing this information in the Seed entity simplifies presentation of Seed data in Index and View pages."}),"\n",(0,a.jsx)(t.p,{children:"Finally, in order to safely delete a Seed instance, it must not have been used to sow any Plantings. So that we don't have to search through all the Plantings across an entire chapter, the Seed entity provides a field called sowSeedCount. This field is initialized to zero and incremented whenever a Seed instance is referenced in the sowSeedID field of a new Planting. A Seed instance can only be deleted when the sowSeedCount is zero."}),"\n",(0,a.jsx)(t.admonition,{title:"SowSeedCount is never decremented",type:"warning",children:(0,a.jsx)(t.p,{children:"In the 1.0 release, sowSeedCount is incremented each time a Planting specifies it as their sowSeedID. It is not reliably decremented. Because of this, in the 1.0 release, once a Seed is created and used once as the sowSeedID, it can not be deleted. The only time you can delete a Seed is before it has ever been used as a sowSeedID in a Planting."})}),"\n",(0,a.jsx)(t.admonition,{title:"plantingID field is currently optional",type:"info",children:(0,a.jsx)(t.p,{children:"In a future version, plantingID will be made required."})}),"\n",(0,a.jsx)(t.h4,{id:"seed-entity-representation",children:"Seed entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Seed(\n {required String seedID, // 'seed-US-98225-102-001-3218'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-102-6789'\n String? plantingID,\n @Default(0) int sowSeedCount, // 0, 1, 2\n required String cachedGardenerID, // 'info@heritageseeds.com' \n required String cachedCropID, // 'crop-US-001-201-3462'\n required String cachedVarietyID, // 'variety-US-001-303-6534'\n required String cachedCropName, // 'Tomato'\n required String cachedVarietyName, // 'Cherokee Purple'\n @Default(true) bool cachedSeedsAvailable} // true, false\n)\n"})}),"\n",(0,a.jsx)(t.h4,{id:"seed-caveats",children:"Seed caveats"}),"\n",(0,a.jsxs)(t.p,{children:["In GGC, the Garden associated with a Vendor has a single Planting instance for each Variety for which they offer Seeds. This single Planting instance will have a SeedID in the harvestSeedID field, with ",(0,a.jsx)(t.code,{children:"seedsAvailable"})," set to ",(0,a.jsx)(t.code,{children:"true"}),"."]}),"\n",(0,a.jsx)(t.p,{children:"In reality, a vendor may or may not have seeds in stock for a given Variety at any given time. And, in reality, a vendor will produce their seeds from new Plantings each year. But, GGC will not attempt to keep track of real-time inventory."}),"\n",(0,a.jsx)(t.h3,{id:"observation",children:"Observation"}),"\n",(0,a.jsx)(t.p,{children:"An Observation is a textual comment (and, typically, a picture) provided by a Gardener regarding a specific Planting at a specific point in time."}),"\n",(0,a.jsx)(t.p,{children:"If a Gardener wishes to make a comment about a non-Planting issue (i.e. their Garden, or the Chapter, or whatever), they can use the Chat Rooms for Gardens and Chapters."}),"\n",(0,a.jsx)(t.p,{children:'The essential difference is that an Observation will be "carried along" with a Planting---in other words, when the Gardener retrieves a View of a specific Planting, they will also see all of the Observations associated with that Planting. We hope that this will help create a useful historical record of a Planting.'}),"\n",(0,a.jsx)(t.h4,{id:"observationid-management",children:"ObservationID management"}),"\n",(0,a.jsxs)(t.p,{children:["ObservationIDs have the format ",(0,a.jsx)(t.code,{children:"observation-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"ObservationNums start at 4001 and are incremented chapter-wide."}),"\n",(0,a.jsx)(t.p,{children:"The country and postal code fields are taken from the Planting associated with this Observation."}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-5",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Observations cache several values in order to allow the Observation card to present information without having to retrieve the Planting."}),"\n",(0,a.jsx)(t.p,{children:"Observations are presented in reverse chronological order by lastUpdate. When someone adds a comment, that sets the lastUpdate field."}),"\n",(0,a.jsx)(t.h4,{id:"observation-entity-representation",children:"Observation entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Observation(\n {required String observationID, // 'observation-US-98225-102-4001-5634'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-102-6789'\n required String gardenerID, // 'johnson@hawaii.edu'\n required String plantingID, // 'planting-US-98225-102-1002-9432'\n required DateTime observationDate, // '2023-03-19T12:19:14.164090'\n required DateTime lastUpdate, // '2023-03-19T12:19:14.164090'\n required List tagIDs, // ['tag-001-501']\n required List comments, // ['observation-US-98225-102-4001-001-9876']\n required String description, // 'First harvest of the season'\n String? pictureURL, // null, 'https://firebasestorage.googleapis.com/v0/...'\n @Default(false) bool isPrivate, // true, false\n required String cachedCropID, // 'crop-US-001-243-3425'\n required String cachedVarietyID, // 'variety-US-001-323-9654'\n required String cachedBedName, // '03'\n required String cachedCropName, // 'Tomato'\n required String cachedVarietyName, // 'Cherokee Purple'\n required String cachedGardenName, // 'Kale is for Kids'\n required DateTime cachedStartDate} // '2023-03-19T12:19:14.164090'\n)\n"})}),"\n",(0,a.jsx)(t.h4,{id:"observation-comments",children:"Observation Comments"}),"\n",(0,a.jsx)(t.p,{children:"As shown above, each Observation entity includes an embedded (potentially empty) list of ObservationComments, which have this structure:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory ObservationComment(\n {required String observationCommentID, // 'observationcomment-US-98225-102-4001-001-4532'\n required String gardenerID, // 'johnson@hawaii.edu'\n required String description, // 'Is that an aphid on the left leaf?'\n required DateTime lastUpdate} // '2023-03-19T12:19:14.164090' \n)\n"})}),"\n",(0,a.jsx)(t.p,{children:"The lastUpdate field indicates when the comment was made or updated."}),"\n",(0,a.jsx)(t.h3,{id:"tag",children:"Tag"}),"\n",(0,a.jsx)(t.p,{children:'The Tag entity provides "meta-data" that a gardener can use to provide information about the nature of an Observation. Tags serve two basic purposes:'}),"\n",(0,a.jsxs)(t.ol,{children:["\n",(0,a.jsxs)(t.li,{children:["\n",(0,a.jsx)(t.p,{children:"Filtering. A user can specify a set of Tags and filter the Observations by those that satisfy either (both?) of them."}),"\n"]}),"\n",(0,a.jsxs)(t.li,{children:["\n",(0,a.jsx)(t.p,{children:"Badge achievement. Many Badges are earned, at least in part, by posting (public) Observations with specific Tags."}),"\n"]}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'Tags, like Badges, Families, and Chapters, are "global" entities that are not Chapter-specific. Therefore, they can only be managed by system admins.'}),"\n",(0,a.jsx)(t.h4,{id:"tagid-management",children:"TagID management"}),"\n",(0,a.jsxs)(t.p,{children:["TagIDs have the format ",(0,a.jsx)(t.code,{children:"tag-"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"TagIDs start at 001."}),"\n",(0,a.jsx)(t.h4,{id:"tag-entity-representation",children:"Tag entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Tag(\n {required String tagID, // 'tag-001'\n required String name, // '#Biodiversity'\n required String description} // 'Use of practices to increase biodiversity...'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"task",children:"Task"}),"\n",(0,a.jsx)(t.p,{children:"A Task specifies an activity to perform for a specific Planting in a specific Garden. There are two types of tasks:"}),"\n",(0,a.jsxs)(t.ol,{children:["\n",(0,a.jsxs)(t.li,{children:["\n",(0,a.jsxs)(t.p,{children:["An ",(0,a.jsx)(t.em,{children:"automatically created"})," Task that is generated from the dates associated with a Planting, such as ",(0,a.jsx)(t.code,{children:"transplant date"})," or ",(0,a.jsx)(t.code,{children:"first harvest date"}),". Whenever the Gardener adjusts the dates associated with a Planting, the associated Task is updated. Conversely, if a Gardener adjusts the date associated with a Task, then the associated Planting date is updated as well."]}),"\n"]}),"\n",(0,a.jsxs)(t.li,{children:["\n",(0,a.jsxs)(t.p,{children:["A ",(0,a.jsx)(t.em,{children:"manually created"})," Task created by a gardener, such as ",(0,a.jsx)(t.code,{children:"Weed cucumbers"})," or ",(0,a.jsx)(t.code,{children:"Add top dressing to radishes"}),"."]}),"\n"]}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'Tasks are ephemeral. When a Gardener indicates that a task has been completed, it is deleted from the system. For automatically created Tasks that are associated with a Planting date, the system prompts the gardener to verify the completion date prior to deleting the Task. This prompt is used to update the date in the Planting instance. This is an important form of "quality assurance" for Planting dates, since the Gardener typically specifies these dates early in the season during planning. The ability of Tasks to help ensure that Planting dates are accurate can make Chapter data more useful.'}),"\n",(0,a.jsxs)(t.admonition,{title:"Non-ephemeral (manually generated) tasks would be cool",type:"info",children:[(0,a.jsx)(t.p,{children:'Currently, all tasks are ephemeral. It would be potentially useful for a Gardener to be able to mark a manually generated Task as "non-ephemeral". This would mean that if the Gardener plans a future Garden, that task could be retrieved and associated with a new Planting.'}),(0,a.jsx)(t.p,{children:"We will leave this as a feature for a future release."})]}),"\n",(0,a.jsx)(t.h4,{id:"taskid-management",children:"TaskID management"}),"\n",(0,a.jsxs)(t.p,{children:["TaskIDs have the format ",(0,a.jsx)(t.code,{children:"task------"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"TaskIDs start at 001."}),"\n",(0,a.jsx)(t.p,{children:"The country, postal code, gardenNum, and plantingNum fields are taken from the Planting associated with this Task."}),"\n",(0,a.jsx)(t.h4,{id:"task-types",children:"Task Types"}),"\n",(0,a.jsx)(t.p,{children:"Each Task has a TaskType:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"enum TaskType { start, transplant, firstHarvest, endHarvest, pull, other }\n"})}),"\n",(0,a.jsx)(t.p,{children:'The first five correspond to the Planting dates. "Other" is used for manually created Tasks.'}),"\n",(0,a.jsx)(t.h4,{id:"task-titles-and-descriptions",children:"Task titles and descriptions"}),"\n",(0,a.jsx)(t.p,{children:'For automatically generated tasks, the title is automatically generated using the task type plus the variety, for example "Start Tomato (Big Boy)". Automatically generated tasks are not created with a description.'}),"\n",(0,a.jsx)(t.p,{children:"For manually generated tasks, the Gardener must specify the title and can also supply a description if desired."}),"\n",(0,a.jsx)(t.h4,{id:"task-entity-representation",children:"Task entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"factory Task(\n {required String taskID, // 'task-US-98225-101-1003-001-7634' \n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-101-6789'\n required String taskType, // 'start'\n required String title, // 'Start Tomato (Big Boy)'\n String? gardenerID, // The owner of the garden, i.e. 'johnson@hawaii.edu'.\n String? description, // null, 'Clean up ground cherries.'\n required String cropID, // 'crop-US-001-203-5412'\n required String varietyID, // 'variety-US-001-101-304-6534'\n required String bedID, // 'bed-US-98225-101-003-8956'\n required String plantingID, // 'planting-US-98225-101-1003-3214'\n required DateTime dueDate, // '2023-03-19T12:19:14.164090'\n required String cachedBedName, // '02'\n required String cachedCropName, // 'Tomato'\n required String cachedVarietyName} // 'Kale is for Kids'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"badge",children:"Badge"}),"\n",(0,a.jsx)(t.p,{children:'GGC provides a game mechanic called "Badges". These are designations for Gardens, Gardeners, and (in future) Chapters that recognize the use of best practices for gardening (such as composting), or significant experience with a specific crop, or other behaviors that we wish to encourage.'}),"\n",(0,a.jsx)(t.p,{children:'The Badge game mechanic is implemented through two entities: "Badge" and "BadgeInstance". The Badge entity is a global entity (i.e. independent of any Chapter and defined by the system), and defines the game mechanic. The BadgeInstance entity represents the achievement of a Badge by a Garden, Gardener, or (in future) Chapter.'}),"\n",(0,a.jsx)(t.h4,{id:"badgeid-and-badgeinstanceid-management",children:"BadgeID and BadgeInstanceID management"}),"\n",(0,a.jsxs)(t.p,{children:["BadgeIDs have the format ",(0,a.jsx)(t.code,{children:"badge-"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"BadgeIDs start at 001."}),"\n",(0,a.jsxs)(t.p,{children:["BadgeInstanceIDs have the format ",(0,a.jsx)(t.code,{children:"badgeinstance----"}),"."]}),"\n",(0,a.jsx)(t.p,{children:"BadgeInstanceNums start at 001."}),"\n",(0,a.jsx)(t.p,{children:"The country and chapterCode fields are taken from the Chapter associated with this Garden or Gardener (and in the future, Chapter)."}),"\n",(0,a.jsx)(t.p,{children:"There is a BadgeType enum represented as follows:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"enum BadgeType { garden, gardener, chapter }\n"})}),"\n",(0,a.jsx)(t.h4,{id:"badge-entity-representation",children:"Badge entity representation"}),"\n",(0,a.jsx)(t.p,{children:"Badges:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Badge(\n {required String badgeID, // 'badge-001'\n required String type, // 'garden'\n required String name, // 'Climate Victory'\n required String criteria, // 'A climate victory garden has been...' \n required String level1, // 'The garden is present...'\n required String level2, // 'The garden is present..., and...'\n required String level3, // 'The garden is present..., and..., and...'\n required List tagIDs} // ['tag-024', 'tag-037']\n)\n"})}),"\n",(0,a.jsx)(t.p,{children:"Badge Instances:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory BadgeInstance(\n {required String badgeInstanceID, // 'badgeinstance-US-001-001-5634'\n required String chapterID, // 'chapter-US-001'\n required String badgeID, // 'badge-001'\n required int level, // 1\n required String id, // 'johnson@hawaii.edu', 'garden-US-98225-101-6789', 'chapter-US-001'\n required String type, // 'gardener', 'garden', 'chapter'\n required String cachedName, // 'Climate Victory'\n String? data, // null, 'supplementary data'\n String? data2, // null, 'supplementary data2'\n String? data3} // null, 'supplementary data3'\n)\n"})}),"\n",(0,a.jsx)(t.h2,{id:"collections-and-business-logic",children:"Collections and business logic"}),"\n",(0,a.jsx)(t.p,{children:"As noted above, each entity is represented as a Dart class, and made persistent as a document in Firebase."}),"\n",(0,a.jsxs)(t.p,{children:['Groups of entity instances of the same type are also represented as a Dart class, and made persistent as a collection in Firebase. So, for example, there is a Dart class called "Chapter" (to represent individual instances of that entity) and a Dart class called "ChapterCollection" (to manage a set of Chapter instances). On the Firebase side, there is a collection called Chapters, and each document in that collection has the same structure as the corresponding Dart class. We use ',(0,a.jsx)(t.a,{href:"https://pub.dev/packages/freezed",children:"freezed"})," to support the translation between the Dart class instance for an entity and its persistent representation as a Firebase document in JSON format."]}),"\n",(0,a.jsx)(t.p,{children:'The client-side collection classes (ChapterCollection, GardenCollection, etc) are intended to encapsulate the "business logic" for the application.'}),"\n",(0,a.jsx)(t.h2,{id:"privacy",children:"Privacy"}),"\n",(0,a.jsx)(t.p,{children:"On the one hand, we want to preserve certain types of privacy:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'Users pick a unique "username" which is used in postings so that they do not have to reveal their true name.'}),"\n",(0,a.jsx)(t.li,{children:"The application does not reveal (and does not know) the precise location of gardens, only their country and postal code."}),"\n",(0,a.jsx)(t.li,{children:'Users can tag an Observation as "private", and in that case it will not be visible to users outside of the garden\'s owner and editors. This allows users to take photos regarding the garden for their personal data collection without feeling inhibited about it becoming "public". For example, the photo might reveal faces or locations.'}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"On the other hand, we want to facilitate the creation of a community of practice. For this reason, all garden data (plantings, etc) are available, in at least a read-only format, to all members of a chapter."}),"\n",(0,a.jsx)(t.p,{children:"A significant goal for the 1.0 release is to test the hypothesis that it is not problematic for users to share these kinds garden details with others in the chapter."}),"\n",(0,a.jsx)(t.p,{children:"A broader question, that we will not explore in the 1.0 release, is what kinds of data could be made available across Chapters."}),"\n",(0,a.jsx)(t.h2,{id:"ids",children:"IDs"}),"\n",(0,a.jsxs)(t.p,{children:['In NoSQL databases, it is common for each document to be automatically provided upon creation with a unique string called a "docID" which looks something like this: ',(0,a.jsx)(t.code,{children:"tghHU4CVfxHGB"}),". The docID is generated by the server and is guaranteed to be unique. It serves as the primary key for entities in that collection."]}),"\n",(0,a.jsx)(t.p,{children:'In GGC, we use a different approach. There is no "docID" field. Instead, the Crop collection has a unique ID called "cropID", the Chapter collection has a unique ID called "chapterID", and so forth. We tell the NoSQL database (in our case, Firebase) that these various ID fields should be used as the primary key (i.e. the docID) for each of the collections.'}),"\n",(0,a.jsx)(t.p,{children:"Importantly, non-global entities are generally created by clients, and in GGC, clients (not the server) are responsible for generating the primary keys for non-global entities. (The global entities, such as Chapter, Family, Badge, etc. are constructed by the system, not clients.)"}),"\n",(0,a.jsx)(t.p,{children:"We have clients generate the primary keys for non-global entities for the following reasons:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'Rather than a server-generated random string, our client-generated primary keys are "human-readable". You can look at an ID string and know what kind of entity it is associated with (all GGC IDs have a prefix like "chapter-", "crop-", etc). Since many entities have fields containing the IDs of other entities, human-readable IDs help in development and system understanding.'}),"\n",(0,a.jsx)(t.li,{children:"In many cases, an update to the database can involve the creation of a new entity (or entities) as well as updates to other entities to include the primary key of the newly created entity (or entities). If primary keys are generated by the server, such updates would become a complex, multi-step process. Since primary keys are generated by the client, these updates are much more simple to accomplish."}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"However, client-generated primary keys have one significant drawback:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'It becomes technically possible for two clients to generate a "primary key collision", i.e. an attempt by different clients to create two entities with the same primary key value at the same time.'}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"To deal with this drawback, we have carefully designed the primary keys in GGC to make it extremely unlikely for primary key collisions to occur."}),"\n",(0,a.jsx)(t.p,{children:"First, primary keys are constructed to include one or more of the chapterID, the country code, the postal code, or the gardenID. This means, for example, that rather than it being possible for a primary key collision to occur by any two GGC users anywhere in the world, it is becomes only possible for it to occur between the owner and editors of a single garden."}),"\n",(0,a.jsx)(t.p,{children:'Second, client-generated primary keys are constructed with a "millis" field. This is a four digit number representing the millisecond value at the time the client created the primary key.'}),"\n",(0,a.jsx)(t.p,{children:"We believe that these two properties of primary keys mean that collisions will not occur in practice, even when clients are operating in disconnected mode."}),"\n",(0,a.jsx)(t.p,{children:"Finally, let's say that this exceedingly unlikely event actually occurs. In that case, because we have told Firebase that the plantingID (for example) is the primary key, Firebase will reject the second plantingID creation. In this case, the application can simply report the error and instruct the user to try again in a few seconds. By this time, the local cache should be updated and the request to create the new entity should succeed."}),"\n",(0,a.jsxs)(t.p,{children:["Note that ",(0,a.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore/best-practices#hotspots",children:"Firebase recommends against creating documentIDs with lexicographically close ranges"}),". We expect that the inclusion of the millis field mitigates this potential performance issue."]}),"\n",(0,a.jsx)(t.h2,{id:"normalization-and-caching",children:"Normalization and caching"}),"\n",(0,a.jsxs)(t.p,{children:['A best practice for relational database design is "',(0,a.jsx)(t.a,{href:"https://en.wikipedia.org/wiki/Database_normalization",children:"normalization"}),'", which means that a value should only occur in one place at a time. Normalization has a number of virtues, such as making updates and deletions more efficient and less error prone. But normalization also has a cost: queries can become very complicated, involving complex "joins" from a variety of data sources.']}),"\n",(0,a.jsx)(t.p,{children:"The GGC app has the following design considerations that impact on the issue of normalization:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'Updates and deletions are (relatively) rare. GGC is mostly an "additive" database. While deletions and updates can occur, it\'s OK if they are "expensive".'}),"\n",(0,a.jsx)(t.li,{children:"Reads are common, and to make these reads fast, GGC implements client-side caches (using Riverpod) for many of the entities."}),"\n",(0,a.jsx)(t.li,{children:"Gardeners do not access to data outside their Chapter, so client-side caches are not impacted if the number of Chapters in GGC becomes large."}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'To simplify retrieval and caching of the appropriate chapter or garden-level "slice" of the database by a client, almost all GGC entities include a chapterID and gardenID field.'}),"\n",(0,a.jsx)(t.p,{children:'We also "denormalize" by providing "redundant" fields in certain entities. For example, in some cases a document will include a cropName field even though it already has a cropID field. We do this avoid having to download large numbers of documents (i.e. Plantings for all Gardens in the Chapter) in order to perform a calculation. These redundant field names have the prefix "cached" in order to make this denormalization explicit in the data model.'}),"\n",(0,a.jsx)(t.h2,{id:"root-collections-vs-subcollections",children:"Root collections vs subcollections"}),"\n",(0,a.jsxs)(t.p,{children:["In Firebase, you can organize the data into root collections or subcollections, as explained in ",(0,a.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore/manage-data/structure-data",children:"Choose a data structure"}),"."]}),"\n",(0,a.jsx)(t.p,{children:"Since GGC involves many many-to-many relationships, we choose to organize all of our data as root collections."}),"\n",(0,a.jsx)(t.p,{children:"In Firebase, there are no performance differences between root collections and subcollections, so we do not gain or lose anything by making this choice."}),"\n",(0,a.jsx)(t.h2,{id:"chat-rooms",children:"Chat rooms"}),"\n",(0,a.jsxs)(t.p,{children:["We use the ",(0,a.jsx)(t.a,{href:"https://pub.dev/packages/flutter_chat_ui",children:"Flutter Chat UI"})," package to implement Chat rooms and users. This results in the addition of some collections to Firebase. We do not document this here."]})]})}function c(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>s});var a=n(7294);const i={},r=a.createContext(i);function s(e){const t=a.useContext(r);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),a.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/a6327429.52a1365e.js b/assets/js/a6327429.52a1365e.js new file mode 100644 index 000000000..d6383d855 --- /dev/null +++ b/assets/js/a6327429.52a1365e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[200],{8082:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>d});var r=n(5893),a=n(1151);const o={hide_table_of_contents:!1},s="Features",i={id:"develop/design/features",title:"Features",description:'The GGC app loosely follows the "feature first" design philosophy expressed in Andrea Bizzotto\'s article Flutter Project Structure',source:"@site/docs/develop/design/features.md",sourceDirName:"develop/design",slug:"/develop/design/features",permalink:"/docs/develop/design/features",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Backups",permalink:"/docs/develop/backups"},next:{title:"Badges",permalink:"/docs/develop/design/badges"}},c={},d=[];function l(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",header:"header",p:"p",pre:"pre",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.header,{children:(0,r.jsx)(t.h1,{id:"features",children:"Features"})}),"\n",(0,r.jsxs)(t.p,{children:['The GGC app loosely follows the "feature first" design philosophy expressed in Andrea Bizzotto\'s article ',(0,r.jsx)(t.a,{href:"https://codewithandrea.com/articles/flutter-project-structure/",children:"Flutter Project Structure: Feature-first or Layer-first?"}),". As noted in ",(0,r.jsx)(t.a,{href:"/docs/develop/architecture",children:"Architecture"}),", the top-level ",(0,r.jsx)(t.code,{children:"lib/"})," directory contains a ",(0,r.jsx)(t.code,{children:"features"})," subdirectory. Let's look at a snapshot of its contents:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",children:"~/GitHub/geogardenclub/ggc_app/lib/features git:[main] ls\nadmin/ bed/ common/ garden/ help/ outcome/ tag/ variety/\nauthentication/ chapter/ crop/ gardener/ home/ planting/ task/\nbadge/ chat/ family/ geobot/ observation/ settings/ user/\n"})}),"\n",(0,r.jsxs)(t.p,{children:["As you can see, the ",(0,r.jsx)(t.code,{children:"features/"}),' directory consists of a couple dozen subdirectories, each of which contains the implementation of a "feature". In many cases, a feature is an entity in the data model (i.e. Bed, Garden, Planting, etc.). In other cases, a feature is a conceptually coherent mechanism (i.e. authentication, help). Then there\'s the ',(0,r.jsx)(t.code,{children:"common/"})," directory, which isn't actually a feature at all, but which holds cross-cutting functionality that is used by multiple features, and which seems most appropriate to be located in this subdirectory even though it isn't actually a feature."]}),"\n",(0,r.jsx)(t.admonition,{title:"Tests are also organized by feature",type:"info",children:(0,r.jsxs)(t.p,{children:["Fun fact: if you look in the ",(0,r.jsx)(t.code,{children:"integration_test/features"})," directory, you'll see a set of subdirectories that almost directly correspond to the subdirectories in ",(0,r.jsx)(t.code,{children:"lib/features"}),"."]})}),"\n",(0,r.jsxs)(t.p,{children:['A distinguishing characteristic of a GGC feature is that it is implemented in terms of one or more of the following components: "data", "domain", and "presentation". As a result, if you look into a feature directory, you will find one or more of the following subdirectories: ',(0,r.jsx)(t.code,{children:"data/"}),", ",(0,r.jsx)(t.code,{children:"domain/"}),", and ",(0,r.jsx)(t.code,{children:"presentation/"}),'. Here\'s the contents of those directories for a relatively simple feature: "Crop":']}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",children:"crop/\n data/\n crop_database.dart \n crop_provider.dart \n crop_provider.g.dart \n fixture_crop_database.dart\n domain/\n crop.dart \n crop.freezed.dart \n crop.g.dart \n crop_collection.dart\n presentation/\n create_crop_screen.dart \n crop_chip.dart \n crop_delete_button.dart \n crop_form.dart \n crop_index_screen.dart \n crop_toggle_section.dart \n crop_view.dart \n crop_view_screen.dart\n delete_crop_screen.dart\n update_crop_screen.dart\n"})}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"data/"})," directory contains code that provides the mechanisms for persisting data associated with this feature to Firebase. It also includes the Riverpod Providers for accessing this data from elsewhere in the app. Finally, it includes the code for overriding the normal Firebase connection and replacing it with a connection to test fixture data. Note that at the ",(0,r.jsx)(t.code,{children:"data/"})," level, data is represented as JSON."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"domain/"})," directory contains class definitions (along with the ",(0,r.jsx)(t.a,{href:"https://pub.dev/packages/freezed",children:"Freezed"})," enhancements) to represent the feature data as instances of a Dart class, not JSON. In addition, the ",(0,r.jsx)(t.code,{children:"domain/"}),' directory can contain a "Collection" class. This is a class that aggregates together all the individual instances of the feature and provides operations (such as find or filter) to manipulate the entire population of feature instances.']}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"presentation/"}),' directory contains UI code. "Top-level" UI classes (containing a Scaffold) are called "screens", and there is a special kind of screen called an "index screen" which provides a way to present all the instances of a feature and search, sort, or filter them. Other common UI classes are "views" (which are reusable components within a screen), "forms" (containing one or more input controllers to gather information from the user), "chips" (presenting a clickable tile representing a feature instance), and "buttons". To support mutation of the feature, there can be "create", "update", and "delete" screens.']}),"\n",(0,r.jsxs)(t.p,{children:['Not all features will have all three of these subdirectories. For example, the "home" feature contains only a ',(0,r.jsx)(t.code,{children:"presentation/"})," subdirectory, because this feature only manipulates entities defined as part of other features."]})]})}function h(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>i,a:()=>s});var r=n(7294);const a={},o=r.createContext(a);function s(e){const t=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),r.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/a6327429.b29917b1.js b/assets/js/a6327429.b29917b1.js deleted file mode 100644 index 2473f9167..000000000 --- a/assets/js/a6327429.b29917b1.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[200],{8082:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>d});var r=n(5893),a=n(1151);const o={hide_table_of_contents:!1},s="Anatomy of a feature",i={id:"develop/design/features",title:"Anatomy of a feature",description:'The GGC app loosely follows the "feature first" design philosophy expressed in Andrea Bizzotto\'s article Flutter Project Structure',source:"@site/docs/develop/design/features.md",sourceDirName:"develop/design",slug:"/develop/design/features",permalink:"/docs/develop/design/features",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Backups",permalink:"/docs/develop/backups"},next:{title:"Data Model",permalink:"/docs/develop/design/data-model"}},c={},d=[];function l(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",header:"header",p:"p",pre:"pre",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.header,{children:(0,r.jsx)(t.h1,{id:"anatomy-of-a-feature",children:"Anatomy of a feature"})}),"\n",(0,r.jsxs)(t.p,{children:['The GGC app loosely follows the "feature first" design philosophy expressed in Andrea Bizzotto\'s article ',(0,r.jsx)(t.a,{href:"https://codewithandrea.com/articles/flutter-project-structure/",children:"Flutter Project Structure: Feature-first or Layer-first?"}),". As noted in ",(0,r.jsx)(t.a,{href:"/docs/develop/architecture",children:"Architecture"}),", the top-level ",(0,r.jsx)(t.code,{children:"lib/"})," directory contains a ",(0,r.jsx)(t.code,{children:"features"})," subdirectory. Let's look at a snapshot of its contents:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",children:"~/GitHub/geogardenclub/ggc_app/lib/features git:[main] ls\nadmin/ bed/ common/ garden/ help/ outcome/ tag/ variety/\nauthentication/ chapter/ crop/ gardener/ home/ planting/ task/\nbadge/ chat/ family/ geobot/ observation/ settings/ user/\n"})}),"\n",(0,r.jsxs)(t.p,{children:["As you can see, the ",(0,r.jsx)(t.code,{children:"features/"}),' directory consists of a couple dozen subdirectories, each of which contains the implementation of a "feature". In many cases, a feature is an entity in the data model (i.e. Bed, Garden, Planting, etc.). In other cases, a feature is a conceptually coherent mechanism (i.e. authentication, help). Then there\'s the ',(0,r.jsx)(t.code,{children:"common/"})," directory, which isn't actually a feature at all, but which holds cross-cutting functionality that is used by multiple features, and which seems most appropriate to be located in this subdirectory even though it isn't actually a feature."]}),"\n",(0,r.jsx)(t.admonition,{title:"Tests are also organized by feature",type:"info",children:(0,r.jsxs)(t.p,{children:["Fun fact: if you look in the ",(0,r.jsx)(t.code,{children:"integration_test/features"})," directory, you'll see a set of subdirectories that almost directly correspond to the subdirectories in ",(0,r.jsx)(t.code,{children:"lib/features"}),"."]})}),"\n",(0,r.jsxs)(t.p,{children:['A distinguishing characteristic of a GGC feature is that it is implemented in terms of one or more of the following components: "data", "domain", and "presentation". As a result, if you look into a feature directory, you will find one or more of the following subdirectories: ',(0,r.jsx)(t.code,{children:"data/"}),", ",(0,r.jsx)(t.code,{children:"domain/"}),", and ",(0,r.jsx)(t.code,{children:"presentation/"}),'. Here\'s the contents of those directories for a relatively simple feature: "Crop":']}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-shell",children:"crop/\n data/\n crop_database.dart \n crop_provider.dart \n crop_provider.g.dart \n fixture_crop_database.dart\n domain/\n crop.dart \n crop.freezed.dart \n crop.g.dart \n crop_collection.dart\n presentation/\n create_crop_screen.dart \n crop_chip.dart \n crop_delete_button.dart \n crop_form.dart \n crop_index_screen.dart \n crop_toggle_section.dart \n crop_view.dart \n crop_view_screen.dart\n delete_crop_screen.dart\n update_crop_screen.dart\n"})}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"data/"})," directory contains code that provides the mechanisms for persisting data associated with this feature to Firebase. It also includes the Riverpod Providers for accessing this data from elsewhere in the app. Finally, it includes the code for overriding the normal Firebase connection and replacing it with a connection to test fixture data. Note that at the ",(0,r.jsx)(t.code,{children:"data/"})," level, data is represented as JSON."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"domain/"})," directory contains class definitions (along with the ",(0,r.jsx)(t.a,{href:"https://pub.dev/packages/freezed",children:"Freezed"})," enhancements) to represent the feature data as instances of a Dart class, not JSON. In addition, the ",(0,r.jsx)(t.code,{children:"domain/"}),' directory can contain a "Collection" class. This is a class that aggregates together all the individual instances of the feature and provides operations (such as find or filter) to manipulate the entire population of feature instances.']}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"presentation/"}),' directory contains UI code. "Top-level" UI classes (containing a Scaffold) are called "screens", and there is a special kind of screen called an "index screen" which provides a way to present all the instances of a feature and search, sort, or filter them. Other common UI classes are "views" (which are reusable components within a screen), "forms" (containing one or more input controllers to gather information from the user), "chips" (presenting a clickable tile representing a feature instance), and "buttons". To support mutation of the feature, there can be "create", "update", and "delete" screens.']}),"\n",(0,r.jsxs)(t.p,{children:['Not all features will have all three of these subdirectories. For example, the "home" feature contains only a ',(0,r.jsx)(t.code,{children:"presentation/"})," subdirectory, because this feature only manipulates entities defined as part of other features."]})]})}function h(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>i,a:()=>s});var r=n(7294);const a={},o=r.createContext(a);function s(e){const t=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),r.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/a9a08fef.b569d67f.js b/assets/js/a9a08fef.b569d67f.js new file mode 100644 index 000000000..dffbd92f9 --- /dev/null +++ b/assets/js/a9a08fef.b569d67f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[6041],{5378:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>s,default:()=>c,frontMatter:()=>r,metadata:()=>o,toc:()=>l});var a=n(5893),i=n(1151);const r={hide_table_of_contents:!1},s="Data Model",o={id:"develop/data-model",title:"Data Model",description:"This page explains the data model (i.e. the set of entities and their relationships) for GGC, along with a rationale for the design decisions that we've made along the way.",source:"@site/docs/develop/data-model.md",sourceDirName:"develop",slug:"/develop/data-model",permalink:"/docs/develop/data-model",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Architecture",permalink:"/docs/develop/architecture"},next:{title:"Managing Firebase data",permalink:"/docs/develop/managing-firebase-data"}},d={},l=[{value:"Entities",id:"entities",level:2},{value:"Entity Hierarchy",id:"entity-hierarchy",level:4},{value:"Entity dependencies",id:"entity-dependencies",level:4},{value:"Chapter",id:"chapter",level:3},{value:"ChapterID management",id:"chapterid-management",level:4},{value:"User registration and chapter assignment",id:"user-registration-and-chapter-assignment",level:4},{value:"ChapterID as Firebase index",id:"chapterid-as-firebase-index",level:4},{value:"Chapter entity representation",id:"chapter-entity-representation",level:4},{value:"User",id:"user",level:3},{value:"Users vs Gardeners",id:"users-vs-gardeners",level:4},{value:"UserID management",id:"userid-management",level:4},{value:"User onboarding",id:"user-onboarding",level:4},{value:"User entity representation",id:"user-entity-representation",level:4},{value:"Gardener",id:"gardener",level:3},{value:"Chapter members vs Vendors",id:"chapter-members-vs-vendors",level:4},{value:"Cached values",id:"cached-values",level:4},{value:"Badge attestations",id:"badge-attestations",level:4},{value:"GardenerID management",id:"gardenerid-management",level:4},{value:"Gardener entity representation",id:"gardener-entity-representation",level:4},{value:"Garden",id:"garden",level:3},{value:"GardenID management",id:"gardenid-management",level:4},{value:"Field Notes",id:"field-notes",level:4},{value:"Cached values",id:"cached-values-1",level:4},{value:"Badge attestations",id:"badge-attestations-1",level:4},{value:"Garden entity representation",id:"garden-entity-representation",level:4},{value:"Editor",id:"editor",level:3},{value:"EditorID management",id:"editorid-management",level:4},{value:"Editor entity representation",id:"editor-entity-representation",level:4},{value:"Bed",id:"bed",level:3},{value:"BedID management",id:"bedid-management",level:4},{value:"Bed entity representation",id:"bed-entity-representation",level:4},{value:"Family",id:"family",level:3},{value:"FamilyID management",id:"familyid-management",level:4},{value:"Family entity representation",id:"family-entity-representation",level:4},{value:"Crop",id:"crop",level:3},{value:"CropID management",id:"cropid-management",level:4},{value:"Crop entity representation",id:"crop-entity-representation",level:4},{value:"Variety",id:"variety",level:3},{value:"VarietyID management",id:"varietyid-management",level:4},{value:"Field notes",id:"field-notes-1",level:4},{value:"Variety entity representation",id:"variety-entity-representation",level:4},{value:"Planting",id:"planting",level:3},{value:"Plantings and seeds",id:"plantings-and-seeds",level:4},{value:"PlantingID management",id:"plantingid-management",level:4},{value:"Field notes",id:"field-notes-2",level:4},{value:"Planting entity representation",id:"planting-entity-representation",level:4},{value:"Outcome",id:"outcome",level:3},{value:"OutcomeID management",id:"outcomeid-management",level:4},{value:"Field notes",id:"field-notes-3",level:4},{value:"Outcome entity representation",id:"outcome-entity-representation",level:4},{value:"Seed",id:"seed",level:3},{value:"SeedID management",id:"seedid-management",level:4},{value:"Field notes",id:"field-notes-4",level:4},{value:"Seed entity representation",id:"seed-entity-representation",level:4},{value:"Seed caveats",id:"seed-caveats",level:4},{value:"Observation",id:"observation",level:3},{value:"ObservationID management",id:"observationid-management",level:4},{value:"Field notes",id:"field-notes-5",level:4},{value:"Observation entity representation",id:"observation-entity-representation",level:4},{value:"Observation Comments",id:"observation-comments",level:4},{value:"Tag",id:"tag",level:3},{value:"TagID management",id:"tagid-management",level:4},{value:"Tag entity representation",id:"tag-entity-representation",level:4},{value:"Task",id:"task",level:3},{value:"TaskID management",id:"taskid-management",level:4},{value:"Task Types",id:"task-types",level:4},{value:"Task titles and descriptions",id:"task-titles-and-descriptions",level:4},{value:"Task entity representation",id:"task-entity-representation",level:4},{value:"Badge",id:"badge",level:3},{value:"BadgeID and BadgeInstanceID management",id:"badgeid-and-badgeinstanceid-management",level:4},{value:"Badge entity representation",id:"badge-entity-representation",level:4},{value:"Collections and business logic",id:"collections-and-business-logic",level:2},{value:"Privacy",id:"privacy",level:2},{value:"IDs",id:"ids",level:2},{value:"Normalization and caching",id:"normalization-and-caching",level:2},{value:"Root collections vs subcollections",id:"root-collections-vs-subcollections",level:2},{value:"Chat rooms",id:"chat-rooms",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.header,{children:(0,a.jsx)(t.h1,{id:"data-model",children:"Data Model"})}),"\n",(0,a.jsx)(t.p,{children:"This page explains the data model (i.e. the set of entities and their relationships) for GGC, along with a rationale for the design decisions that we've made along the way."}),"\n",(0,a.jsx)(t.h2,{id:"entities",children:"Entities"}),"\n",(0,a.jsx)(t.p,{children:'In GGC, "entity" refers to the fundamental forms of persistent data objects. Examples of entities are: "Chapter", "Garden", "Gardener", "Observation", etc.'}),"\n",(0,a.jsx)(t.p,{children:"Each entity is defined as a set of typed fields."}),"\n",(0,a.jsx)(t.p,{children:"Entities are persisted through a set of Firebase collections. In general, each entity is a document that is stored in a corresponding collection: all of the Chapter entity documents are stored in a Firebase collection called Chapters, all of the Gardener entity documents are stored in a Firebase collection called Gardeners."}),"\n",(0,a.jsx)(t.p,{children:'The GGC app implements a set of Dart "domain" classes that mirror these Firebase collections, so (for example) there is a Dart class called "Chapter" (that defines the structure of a Chapter entity), and a Dart class called "ChapterCollection" (which holds a list of Chapter entity instances and provides operations upon them).'}),"\n",(0,a.jsx)(t.h4,{id:"entity-hierarchy",children:"Entity Hierarchy"}),"\n",(0,a.jsx)(t.p,{children:"The following diagram provides an overview of the major entities in the data model organized into a three level hierarchy:"}),"\n",(0,a.jsx)("img",{style:{borderStyle:"solid"},src:"/img/develop/data-entity-overview.png"}),"\n",(0,a.jsx)(t.p,{children:"The diagram separates the entities into three categories:"}),"\n",(0,a.jsxs)(t.ol,{children:["\n",(0,a.jsx)(t.li,{children:'"Global-level" entities. These entities are globally accessible to all Chapters.'}),"\n",(0,a.jsx)(t.li,{children:'"Chapter-level" entities. These are "top-level" entities for any given chapter. These entities all include a chapterID field. Each user is always associated with a single Chapter, and thus can only "see" the chapter-level entities with a matching chapterID. They are normally downloaded and cached in the client application upon login.'}),"\n",(0,a.jsx)(t.li,{children:'"Garden-level" entities. These entities are all specific to a single Garden, and include both a chapterID and a gardenID. Garden-level entities are only visible within their Chapter. In addition, Garden-level entities might only be downloaded on-demand.'}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'This diagram can also be used to understand the relative numbers of entities that a given client must manipulate. Each of the "Global-level" entity will have dozens to hundreds of instances, and so it is practical for the client application to cache them locally without a large performance impact.'}),"\n",(0,a.jsx)(t.p,{children:'Since each User is associated with a single Chapter, the number of "Chapter-level" entity instances visible to a User is not expected to exceed several hundred to a thousand. This means it is practical for the client application to cache all "visible" Chapter-level entities locally.'}),"\n",(0,a.jsx)(t.p,{children:'We expect each User to be associated with one to a dozen Gardens. Each Garden might have hundreds to thousands of Plantings. This means it is practical for the client application to cache the "Garden-level" entities that they are associated with.'}),"\n",(0,a.jsx)(t.p,{children:"We do not think it is practical for the User to cache all the Plantings associated with all the Gardens in the Chapter. Our design is intended to avoid this, so that Plantings are only downloaded on an as-needed basis."}),"\n",(0,a.jsx)(t.p,{children:'The goal of this design is to create "chapter-level" and "garden-level" namespaces, such that GeoGardenClub can scale to hundreds of Chapters, where each Chapter contains hundreds of gardens, and where each Garden contains hundreds of Plantings (and other Garden-specific entities), all while providing a fast, intuitive, and responsive application for each user. Our design is intended to allow the GGC database to grow to millions of documents while enabling individual clients to require access to only thousands of documents.'}),"\n",(0,a.jsxs)(t.admonition,{title:"What about huge chapters?",type:"warning",children:[(0,a.jsx)(t.p,{children:"This design does have a potential problem: what if a Chapter becomes wildly popular and grows to many hundreds of members? It is possible that the performance of the client application can degrade if the number of members (and thus gardens) in a single Chapter becomes too large."}),(0,a.jsx)(t.p,{children:'To address this potential problem, the data model is designed to facilitate partitioning of large Chapters into multiple smaller Chapters in the event that the number of members becomes too large. For example, the initial definition of a Chapter may comprise 8 postal (zip) codes, corresponding to all the postal codes in that country. But if that Chapter becomes too large, we could split it into two Chapters, each defined with 4 postal codes (or one with 3 postal codes and one with 5 postal codes, depending upon the concentration of members in each postal code). Our data model does not currently allow Chapter definition "below" the level of a postal code, so the smallest possible Chapter in GeoGardenClub would be one defined by a single postal code.'}),(0,a.jsx)(t.p,{children:"We foresee an annual end-of-year review, where we see if any Chapters are reaching a size where it would be appropriate to split them up into smaller Chapters. By doing it in Winter (at least for the Northern Hemisphere), such Chapter reorganization should have less impact on the Gardeners."}),(0,a.jsx)(t.p,{children:"To facilitate Chapter splitting, the IDs associated with Garden-level entities do not encode the chapterID, but instead the two character (alpha2) country code and the postal code. This allows Garden-level data to more easily migrate to new Chapters without needing to change their entity IDs."})]}),"\n",(0,a.jsx)(t.h4,{id:"entity-dependencies",children:"Entity dependencies"}),"\n",(0,a.jsx)(t.p,{children:"The following diagram presents an alternative perspective on the entities. In this case, there is a line between two entities when there is a relationship between them; in other words, one of the entities refers to the other with a foreign key (i.e. ID) field."}),"\n",(0,a.jsx)("img",{style:{borderStyle:"solid"},src:"/img/develop/data-model-dependencies.png"}),"\n",(0,a.jsx)(t.p,{children:"The primary goal of this diagram is to make it clear that there is a fairly rich set of dependencies among the entities in this data model."}),"\n",(0,a.jsx)(t.p,{children:'This is a positive thing, because it means that there are many different and interesting ways to "slice and dice" the data.'}),"\n",(0,a.jsx)(t.p,{children:"It also illustrates why we have chosen to implement the data model as a set of top-level collections with no subcollections. The many different relationships argue against the use of subcollections. The Firestore documentation indicates that there is no performance penalty to using all top-level collections."}),"\n",(0,a.jsx)(t.p,{children:"Let's now turn to a more detailed description of the entities in the data model."}),"\n",(0,a.jsx)(t.h3,{id:"chapter",children:"Chapter"}),"\n",(0,a.jsx)(t.p,{children:"The Chapter entity defines a geographic region based on a country (represented as a two character (alpha-2) country code), and a set of one or more postal (zip) codes. GGC ensures that Chapter instances partition the world: every pair of (country code, postal code) is mapped to exactly one Chapter."}),"\n",(0,a.jsxs)(t.admonition,{title:"The following design for ChapterID management is not yet implemented",type:"warning",children:[(0,a.jsx)(t.h4,{id:"chapterid-management",children:"ChapterID management"}),(0,a.jsx)(t.p,{children:"A Firebase collection called ChapterZipMap will provide a default mapping of US postal (i.e. zip) codes to chapterIDs. This mapping initially defines a one-to-one correspondence between US counties and GGC Chapters."}),(0,a.jsx)(t.p,{children:"Outside of the US, each (country code, postal code) pair will be its own Chapter. This is not optimal but it provides a way to make GGC immediately available to users outside the US without constructing a world-wide ChapterPostalCodeMap. We can add this later without any change to the data model."}),(0,a.jsxs)(t.p,{children:["Unlike most other entity IDs, the complete set of chapterIDs is defined in advance in GGC. In other words, we can compute all of the chapterIDs on earth, and they do not depend upon the number of users or their behavior. In contrast, there is no ",(0,a.jsx)(t.em,{children:"a priori"})," limit to the number of (say) Planting IDs."]}),(0,a.jsxs)(t.p,{children:["While chapterIDs are finite, they are not necessarily ",(0,a.jsx)(t.em,{children:"fixed"})," in terms of their numbers and the geographic regions that they encompass. For US Chapters, we can change the set of chapters by changing the entries in the ChapterZipMap. For example, while our initial approach is to implement a one-to-one correspondence between US chapters and US counties, we could in future change the ChapterZipMap so that a single US county could have multiple Chapters, or multiple counties could be combined into a single Chapter, or some other approach. (Changing chapter geographic boundaries requires more than just changing the ChapterZipMap; the point here is that our representation does not lock us in to our initial definition for Chapters.) The only hard constraint is that each postal code is assigned to one and only one Chapter."]}),(0,a.jsxs)(t.p,{children:["ChapterIDs have the format ",(0,a.jsx)(t.code,{children:"chapter--"}),'. In the case of a US Chapter, an example Chapter ID is: "chapter-US-001". In the case of a non-US Chapter, an example Chapter ID is "chapter-CA-V6K1G8".']}),(0,a.jsx)(t.p,{children:"To support readability in this document, we will use US chapters and the chapterCodes will be numeric."}),(0,a.jsx)(t.h4,{id:"user-registration-and-chapter-assignment",children:"User registration and chapter assignment"}),(0,a.jsx)(t.p,{children:'New user registration works as follows. If they supply "US" as their country code, then the system will query the ChapterZipMap collection to determine their chapterID based on the postal (zip) code that they also supply. If no Chapter entity exists yet with that chapterID, it will be created with the chapterID provided by the ChapterZipMap collection.'}),(0,a.jsxs)(t.p,{children:["If the new user supplies a non-US country code, then the ChapterZipMap is not consulted. Instead, the chapterID is defined as ",(0,a.jsx)(t.code,{children:"chapter--"}),". If no Chapter entity exists yet corresponding to that ChapterID, then it will be created."]}),(0,a.jsxs)(t.p,{children:["Note that ",(0,a.jsx)(t.a,{href:"https://tosbourn.com/list-of-countries-without-a-postcode/",children:"some countries do not have a postal code"}),'. In this case, we will create a default postal code (i.e. "000") for those countries and not request it from the user if they select one of those countries. This implies that for those countries, there will be only one chapter for the entire country. Since most of those countries are pretty small, that seems like a reasonable design decision.']})]}),"\n",(0,a.jsx)(t.h4,{id:"chapterid-as-firebase-index",children:"ChapterID as Firebase index"}),"\n",(0,a.jsx)(t.p,{children:"As will be seen, many entities contain a chapterID field. When a client retrieves data from Firebase, it will normally request all of the documents where the chapterID field is the one associated with their chapter. This is the primary way in which GGC can scale. For this to work effectively, we must define an index on the chapterID field for all collections in which the entities have that field."}),"\n",(0,a.jsx)(t.h4,{id:"chapter-entity-representation",children:"Chapter entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Chapter(\n {required String chapterID, // 'chapter-US-001', or 'chapter-CA-V6K1G8'\n required String name, // 'Whatcom-WA', or 'CA-V6K1G8'\n required String countryCode, // 'US', 'CA'\n required List postalCodes} // ['98225', '98226'], or ['V6K1GB']\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"user",children:"User"}),"\n",(0,a.jsx)(t.p,{children:"A User entity is created for all the people who have created an account with the system."}),"\n",(0,a.jsx)(t.h4,{id:"users-vs-gardeners",children:"Users vs Gardeners"}),"\n",(0,a.jsx)(t.p,{children:"Note that all User entities will also have a Gardener entity, but not vice-versa: not all Gardener entities have a corresponding User entity. This is because commercial seed vendors won't generally have an account on the system, but they are represented within the system as Gardener entities."}),"\n",(0,a.jsx)(t.p,{children:"Every User is associated with a unique email address, which is their UserID. (Their email is also used for their gardenerID.)"}),"\n",(0,a.jsx)(t.h4,{id:"userid-management",children:"UserID management"}),"\n",(0,a.jsx)(t.p,{children:"UserIDs are the email addresses of the user. We obtain the email as part of registration."}),"\n",(0,a.jsx)(t.h4,{id:"user-onboarding",children:"User onboarding"}),"\n",(0,a.jsx)(t.p,{children:"After a user successfully registers with the system using the Firebase authentication procedures, they are logged in. Whenever a user logs in, the system checks to see if there is a User document associated with the email address of the currently logged in user. If there is no User document for that email, then the system displays an Onboarding screen."}),"\n",(0,a.jsx)(t.p,{children:"The onboarding screen is essentially a form that must be successfully filled out in order for the logged in user to proceed to their home page (as well as to any other areas of the application)."}),"\n",(0,a.jsx)(t.p,{children:"The form provides fields for the user's:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"Name"}),"\n",(0,a.jsx)(t.li,{children:"Username"}),"\n",(0,a.jsx)(t.li,{children:"Country"}),"\n",(0,a.jsx)(t.li,{children:"Postal (Zip) code"}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"In addition, the user can provide a picture at this time if they want."}),"\n",(0,a.jsxs)(t.admonition,{title:"1.0 Release modifications",type:"info",children:[(0,a.jsx)(t.p,{children:"For the initial 1.0 release:"}),(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'The country field will be a read-only drop-down and "United States" will be selected. It returns the alpha2 code for the United States (i.e. "US")'}),"\n",(0,a.jsx)(t.li,{children:"The Postal (Zip) Code input field will be a pull-down list of postal codes associated with Whatcom, Washington."}),"\n"]}),(0,a.jsx)(t.p,{children:"These modifications to the Onboarding screen guarantee that 1.0 test users will be associated with the Whatcom-WA Chapter, and allow us to avoid the need to design and implement the ChapterZipMap and associated processing."})]}),"\n",(0,a.jsx)(t.p,{children:"Once the form is successfully filled out, a User and Gardener document is created for that email address. If those documents are created successfully, then the application displays the Home screen for that User."}),"\n",(0,a.jsx)(t.h4,{id:"user-entity-representation",children:"User entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory User(\n {required String userID, // 'johnson@hawaii.edu'\n required String chapterID, // 'chapter-US-001'\n required String name, // 'Philip Johnson'\n required String username, // '@fiveoclockphil'\n required String country, // 'US'\n required String postalCode, // '98225'\n required String uid, // '22e9fe1b-445c-4523-89c2-4450244f1959'\n String? pictureURL} // null, or 'https://firebasestorage.googleapis.com/v0/...'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"gardener",children:"Gardener"}),"\n",(0,a.jsx)(t.p,{children:'There is one Gardener entity for each Chapter member and vendor in GGC. This entity is designed to represent two distinct classes of gardeners: (1) "normal" home gardeners (who are Chapter members) and (2) commercial seed vendors (who are not (normally) Chapter members).'}),"\n",(0,a.jsx)(t.h4,{id:"chapter-members-vs-vendors",children:"Chapter members vs Vendors"}),"\n",(0,a.jsx)(t.p,{children:'The benefit of having the Gardener entity represent both Chapter members as well as commercial seed vendors is that it results in a uniform mechanism in the app to support "seed providers". Any Gardener (which can either be a normal home gardener or a commercial seed vendor) owns a Garden which contains Plantings which (may or may not) produce seeds that are available within the Chapter.'}),"\n",(0,a.jsx)(t.p,{children:'This does create some UI complexity, in that commercial seed vendors do not appear in the list of "Gardeners" and instead appear in the UI as "Vendors". Underneath, however, commercial seed vendors will (like Chapter members) have a Gardener entity, a Garden entity, and for each seed that someone in the Chapter uses, there will be a Seed entity and a Planting entity. (To as great an extent as possible, all of this Vendor entity management is managed internally and hidden from the UI.)'}),"\n",(0,a.jsx)(t.p,{children:"The Gardener entity indicates that it is representing a Vendor by setting the isVendor flag to true. If that flag is true, then the vendorName, vendorShortName, and vendorUrl fields must be non-null."}),"\n",(0,a.jsx)(t.p,{children:"The Vendors in a Chapter are crowd-sourced, which means any Chapter member can create a new Vendor. When a Vendor is created, they are given the country and postal code of the member who defined them. This is necessary so that their implicitly defined Garden and Plantings can have Chapter-appropriate ID strings."}),"\n",(0,a.jsx)(t.h4,{id:"cached-values",children:"Cached values"}),"\n",(0,a.jsx)(t.p,{children:'We want to provide information about Gardeners such as the crops and varieties that they are growing in the Index screens, and for performance reasons, we want to provide this information without having to retrieve all of the Planting instances associated with their gardens. To do this, we "cache" the cropIDs and varietyIDs associated with this gardener in this entity.'}),"\n",(0,a.jsx)(t.p,{children:'By "associated", we mean the crops and varieties in the garden(s) for which this gardener is an owner.'}),"\n",(0,a.jsx)(t.h4,{id:"badge-attestations",children:"Badge attestations"}),"\n",(0,a.jsx)(t.p,{children:'Certain badges require Gardeners to "attest" to having performed activities. The Gardener entity contains an attestations field that holds strings indicating what has been attested to.'}),"\n",(0,a.jsx)(t.h4,{id:"gardenerid-management",children:"GardenerID management"}),"\n",(0,a.jsxs)(t.p,{children:["GardenerIDs are the email addresses of the gardener. In the case of registered users, the UserID is the same as the GardenerID. In the case of Vendors, the GardenerID is the contact email for the vendor company (for example, ",(0,a.jsx)(t.a,{href:"mailto:info@johnnyseeds.com",children:"info@johnnyseeds.com"}),")."]}),"\n",(0,a.jsx)(t.h4,{id:"gardener-entity-representation",children:"Gardener entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Gardener(\n {required String gardenerID, // 'johnson@hawaii.edu'\n required String chapterID, // 'chapter-US-001'\n required List cachedCropIDs, // ['crop-US-001-203-9987']\n required List cachedVarietyIDs, // ['variety-US-001-305-8765']\n required String country, // 'US'\n required String postalCode, // '98225'\n required List attestations, // ['PermacultureWorkshop']\n @Default(false) bool isVendor, // true, or false\n String? vendorName, // null, or 'Johnnys Seeds and Supplies'\n String? vendorShortName, // null, or 'Johnnys'\n String? vendorURL} // true, or false\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"garden",children:"Garden"}),"\n",(0,a.jsx)(t.p,{children:"The Garden entity represents a plot of land (or maybe even just some pots) that can hold Plantings over one or more years."}),"\n",(0,a.jsx)(t.h4,{id:"gardenid-management",children:"GardenID management"}),"\n",(0,a.jsx)(t.p,{children:"GardenIDs are generated dynamically when a Chapter member defines a new Garden or when a Chapter member defines a new Vendor (which implicitly results in the creation of a new Garden)."}),"\n",(0,a.jsxs)(t.p,{children:["GardenIDs have the format ",(0,a.jsx)(t.code,{children:"garden----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"The GardenID embeds the country code and postal code associated with the ownerID. Note that this might not be the same postal code as the one associated with the physical location of the garden! We do this in order to ensure that if a Chapter's set of postal codes is reorganized, then the Gardens owned by a Gardener will always end up in the same Chapter as their owner."}),"\n",(0,a.jsx)(t.p,{children:'To support readability in this document and initial development, the gardenNum starts at "101" for each chapter.'}),"\n",(0,a.jsx)(t.h4,{id:"field-notes",children:"Field Notes"}),"\n",(0,a.jsxs)(t.p,{children:["The form field for vendor name entry imposes validation criteria. See ",(0,a.jsx)(t.a,{href:"https://github.com/geogardenclub/ggc_app/blob/main/lib/features/common/input-fields/validators.dart",children:"validators.dart"})," for details."]}),"\n",(0,a.jsx)(t.p,{children:"The Garden name must be unique within a Chapter."}),"\n",(0,a.jsx)(t.p,{children:"The cachedYears value is based on the StartDate for the Plantings associated with the Garden."}),"\n",(0,a.jsx)(t.h4,{id:"cached-values-1",children:"Cached values"}),"\n",(0,a.jsx)(t.p,{children:"Each Garden entity caches the CropIDs, VarietyIDs, years, and the number of Plantings. This allows the Index screens to show this information about Gardens without needing to retrieve and process Plantings."}),"\n",(0,a.jsx)(t.p,{children:"In addition, whenever there is a change to the Plantings associated with this Garden, the lastUpdated field is set to the current time. This allows the community to see which Gardens in their Chapter are active."}),"\n",(0,a.jsx)(t.h4,{id:"badge-attestations-1",children:"Badge attestations"}),"\n",(0,a.jsx)(t.p,{children:'Certain badges require Gardeners to "attest" to their Garden having certain properties. The Garden entity contains an attestations field with strings indicating the properties that they have attested to.'}),"\n",(0,a.jsx)(t.h4,{id:"garden-entity-representation",children:"Garden entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Garden(\n {required String gardenID, // 'garden-US-98225-101-4567'\n required String chapterID, // 'chapter-US-001'\n required String name, // 'Kale is for Kids'\n required String ownerID, // 'jessie@gmail.com'\n required List cachedCropIDs, // ['crop-US-001-201-9876']\n required List cachedVarietyIDs, // ['variety-US-001-302-7865']\n required List cachedYears, // [2023, 2022]\n required int cachedNumPlantings, // 231\n required List attestations, // ['ClimateVictory', 'PesticideFree', 'CommunityOrSchool']\n String? pictureURL, // null, 'https://firebasestorage.googleapis.com/v0/...'\n String? plotPlanURL, // null, 'https://firebasestorage.googleapis.com/v0/...' \n DateTime? lastUpdate, // null (for vendors), '2023-03-19T12:19:14.164090' \n @Default(false) bool isVendor} // true, false\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"editor",children:"Editor"}),"\n",(0,a.jsx)(t.p,{children:'The owner of a Garden can add other Chapter members as "editors", which enables those users to edit the Plantings and other information associated with a Garden.'}),"\n",(0,a.jsx)(t.p,{children:"There are some things Editors cannot do. For example, they cannot delete the garden. Only the owner can do that."}),"\n",(0,a.jsx)(t.p,{children:"To earn a Gardener Badge, only the data associated with Gardens that you own is used. Being an Editor on a Garden does not support Badge processing."}),"\n",(0,a.jsx)(t.p,{children:"In addition, when displaying the Crops and Varieties associated with a Gardener, only those Crops and Varieties for the Gardens that you own are displayed. The Crops and Varieties for Gardens for which you are an Editor are not included."}),"\n",(0,a.jsx)(t.h4,{id:"editorid-management",children:"EditorID management"}),"\n",(0,a.jsx)(t.p,{children:"Editor entities are created or deleted when the owner of a Garden edits the Editor field of the Garden Details form."}),"\n",(0,a.jsxs)(t.p,{children:["EditorIDs have the format ",(0,a.jsx)(t.code,{children:"editor-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"EditorNums start at 001 for each garden."}),"\n",(0,a.jsx)(t.h4,{id:"editor-entity-representation",children:"Editor entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Editor(\n {required String editorID, // 'editor-US-98225-102-001-5231'\n required String gardenID, // 'garden-US-98225-102-6789'\n required String chapterID, // 'chapter-US-001'\n required String gardenerID} // 'johnson@hawaii.edu'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"bed",children:"Bed"}),"\n",(0,a.jsx)(t.p,{children:"Each Garden consists of a number of Beds. An owner can edit the name of an existing Bed, and can add a new Bed to a Garden, but cannot delete a Bed if there are any Plantings associated with it."}),"\n",(0,a.jsx)(t.h4,{id:"bedid-management",children:"BedID management"}),"\n",(0,a.jsxs)(t.p,{children:["BedIDs have the format ",(0,a.jsx)(t.code,{children:"bed-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"BedNums start at 001 for each garden."}),"\n",(0,a.jsx)(t.h4,{id:"bed-entity-representation",children:"Bed entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:" const factory Bed(\n {required String bedID, // 'bed-US-98225-101-001-5634'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-101-6789'\n required String name, // '02'\n String? gardenerID, // The owner of the garden, i.e. 'johnson@hawaii.edu'.\n } \n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"family",children:"Family"}),"\n",(0,a.jsx)(t.p,{children:'The Family entity specifies the botanical family associated with one or more Crops (and implicitly, Varieties). For example, the "Nightshade" family groups together Tomatoes, Potatoes, and Peppers. Each Crop is associated with exactly one Family.'}),"\n",(0,a.jsx)(t.p,{children:"Family data is useful to facilitate planning issues including crop rotation and companion planting. However, in Release 1.0, we do not provide any explicit support for rotation or companion planning."}),"\n",(0,a.jsx)(t.p,{children:'The Family entity is a "global" collection in GGC. In other words, it does not include a ChapterID; every Chapter will download this collection, and it cannot be edited except by developers.'}),"\n",(0,a.jsx)(t.h4,{id:"familyid-management",children:"FamilyID management"}),"\n",(0,a.jsxs)(t.p,{children:["FamilyIDs have the format ",(0,a.jsx)(t.code,{children:"family-"}),". The set of Family entity documents is defined in advance by GGC developers, and editing this collection requires direct interaction with the database."]}),"\n",(0,a.jsx)(t.p,{children:"FamilyNums start at 001."}),"\n",(0,a.jsx)(t.h4,{id:"family-entity-representation",children:"Family entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Family(\n {required String familyID, // 'family-001'\n required String formal, // 'Amryllidaceae'\n required String common, // 'Allium'\n required String examples} // 'onion, leek, garlic, shallot'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"crop",children:"Crop"}),"\n",(0,a.jsx)(t.p,{children:'The Crop entity specifies a type of plant independent of its Variety. For example, "Tomato" is a Crop, while "Big Boy Tomato" is a specific Variety of Tomato.'}),"\n",(0,a.jsx)(t.p,{children:"Each Crop is associated with exactly one Family entity. A Crop can be associated with many Varieties."}),"\n",(0,a.jsx)(t.p,{children:'Each Chapter is responsible for "crowd-sourcing" the set of Crop entities. This puts on burden on early Chapter members to define Crops. We estimate that most chapters will need to define between 50 and 100 Crop entities.'}),"\n",(0,a.jsxs)(t.p,{children:["The reason we do not provide a global collection of Crops is because a single collection containing all the crops grown world-wide would have several hundred entities, many of which would not be relevant to the Chapter. We want each Chapter's UI to show only the Crops (and Varieties, and Seeds) that are ",(0,a.jsx)(t.em,{children:"actually being grown"})," in that Chapter. We hypothesize that the benefits of focusing on what is actually being grown outweigh the cost of crowd-sourced management."]}),"\n",(0,a.jsx)(t.h4,{id:"cropid-management",children:"CropID management"}),"\n",(0,a.jsxs)(t.p,{children:["CropIDs have the format ",(0,a.jsx)(t.code,{children:"crop----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"CropIDs embed the chapter's country code and chapterCode. (ChapterCodes could be a number like '001' in the case of a US Chapter, or a postal code like 'VNZ76T' in the case of a non-US chapter.)"}),"\n",(0,a.jsx)(t.p,{children:"In the event that a Chapter is divided into two or more smaller chapters, each of the new Chapters needs a copy of the Crop collection where the IDs have been changed to embed the new chapterCode. This will require a pass through all of the Garden-level entities to update the value of their cropID fields to the new string value."}),"\n",(0,a.jsx)(t.p,{children:"CropNums start at 201 for each chapter."}),"\n",(0,a.jsx)(t.h4,{id:"crop-entity-representation",children:"Crop entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Crop(\n {required String cropID, // 'crop-US-001-201-3452'\n required String chapterID, // 'chapter-US-001'\n required String familyID, // 'family-001'\n required String name} // 'Tomato'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"variety",children:"Variety"}),"\n",(0,a.jsx)(t.p,{children:'Variety is a specific kind of Crop which can actually be grown, i.e. it has seeds. For example, a seed packet such as "Tomato (Sun Gold)" specifies the crop ("Tomato") and the Variety ("Sun Gold").'}),"\n",(0,a.jsx)(t.p,{children:'In some cases, the Variety associated with a given seed might not be known. In those cases, by convention, the Variety name can be specified as "Unknown". (It is not, however, appropriate to create a Crop called "Unknown". If you plant some seeds that you know absolutely nothing about, you should wait until they germinate and you can identify their Crop before you can enter data about it into GGC!)'}),"\n",(0,a.jsx)(t.p,{children:"Note that it is possible (and common) for multiple gardeners (either home or commercial vendors) to produce seeds of the same Variety."}),"\n",(0,a.jsx)(t.h4,{id:"varietyid-management",children:"VarietyID management"}),"\n",(0,a.jsxs)(t.p,{children:["VarietyIDs have the format ",(0,a.jsx)(t.code,{children:"variety----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"Like CropIDs, VarietyIDs embed the country code and chapterCode. (Like CropIDs, ChapterCodes could be a number like '001' in the case of a US Chapter, or a postal code like 'VNZ76T' in the case of a non-US chapter.)"}),"\n",(0,a.jsx)(t.p,{children:"In the event that a Chapter is divided into two or more smaller chapters, each of the new Chapters needs a copy of the Variety collection with the updated chapterCode. This will require a pass through all of the Garden-level entities to update their varietyID fields to the new string value."}),"\n",(0,a.jsx)(t.p,{children:"VarietyNums start at 301 for each chapter."}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-1",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Note that we cache the Crop Name because it will rarely, if ever, change and it is useful to have it in the Variety document so that we can return the full name without needing the Crop collection."}),"\n",(0,a.jsx)(t.p,{children:"That implies, however, that if the name of a Crop is ever changed, then we must find all of the Variety documents associated with that cropID and update the cachedCropName field. This is an acceptable trade-off."}),"\n",(0,a.jsx)(t.h4,{id:"variety-entity-representation",children:"Variety entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Variety(\n {required String varietyID, // 'variety-US-001-302-7654'\n required String chapterID, // 'chapter-US-001'\n required String cropID, // 'crop-US-001-203-2354'\n required String cachedCropName, // 'Asparagus'\n bool? isGold, // If present and set to true, the variety has \"gold\" status.\n required String name} // 'Jersey Knight' \n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"planting",children:"Planting"}),"\n",(0,a.jsx)(t.p,{children:"A Planting represents a set of plants of the same variety (or crop), planted in a single bed, all with the same approximate timings (i.e. sow date, transplant date, first harvest date, etc.)."}),"\n",(0,a.jsx)(t.p,{children:"If the same variety (or crop) is planted in two different beds, then this must be represented by two Planting instances."}),"\n",(0,a.jsx)(t.p,{children:'It is common during the garden planning process to first design the garden at the "crop" level, and then later refine the plan by specifying the specific variety to be planted. To support this incremental planning process, you can create a Planting instance and specify only the Crop, not the Variety.'}),"\n",(0,a.jsx)(t.h4,{id:"plantings-and-seeds",children:"Plantings and seeds"}),"\n",(0,a.jsx)(t.p,{children:"One innovative feature of GGC is that we provide an explicit representation of the seeds grown by a Planting. Here is how it manifests in the Planting entity."}),"\n",(0,a.jsx)(t.p,{children:"In each Planting document, we two optional fields called sowSeedID and harvestSeedID. The sowSeedID represents the seeds from which this Planting was grown (if known), and the harvestSeedID represents the seeds produced by this Planting (if any were produced)."}),"\n",(0,a.jsx)(t.p,{children:"Finally, there is a boolean field called seedsAvailable. If true, this means not only that the Planting grew seeds (and thus there is a harvestSeedID), but that this gardener is willing to share these seeds with others in the Chapter. When seedsAvailable is true, then other Gardeners looking at the Variety associated with this planting will see that they can contact the owner of this Garden to request seeds from this Planting. They might also be able to see the Outcome data for this Planting, which provides some evidence for the future success of these seeds when grown."}),"\n",(0,a.jsx)(t.h4,{id:"plantingid-management",children:"PlantingID management"}),"\n",(0,a.jsxs)(t.p,{children:["PlantingIDs have the format ",(0,a.jsx)(t.code,{children:"planting-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"The country and postal code fields in the ID must match those fields in the gardenID associated with this Planting."}),"\n",(0,a.jsx)(t.p,{children:"Since, over a period of years, a single garden can result in over a thousand plantings, we generally use a four digit number for the plantingNum."}),"\n",(0,a.jsx)(t.p,{children:"PlantingNums start at 1001 for each garden."}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-2",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Validators should guarantee that startDate < transplantDate < firstHarvestDate < endHarvestDate < pullDate."}),"\n",(0,a.jsx)(t.p,{children:"All Plantings must have a startDate, a pullDate, and a bedID. These values are required so that the Planting can be displayed as a horizontal bar in the Garden details view."}),"\n",(0,a.jsx)(t.p,{children:"If a Gardener wants to indicate that seeds are available, they must provide the Variety for this Planting."}),"\n",(0,a.jsx)(t.p,{children:"If the gardener sets usedGreenhouse to true, then they should (eventually) record a transplantDate, although this is not mandatory."}),"\n",(0,a.jsx)(t.p,{children:'Note that if both a cropID and varietyID is provided, then the varietyID must "match" the cropID. Put another way, the associated Variety\'s cropID field should match the Planting\'s cropID field. (Put yet another way, this would be illegal: a Planting in which the Crop is "Corn" but the Variety is "Big Boy (Tomato)"). The UI for defining and managing Planting entities will enforce this by only showing the Varieties associated with the currently selected Crop.'}),"\n",(0,a.jsx)(t.h4,{id:"planting-entity-representation",children:"Planting entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"factory Planting(\n {required String plantingID, // 'planting-US-98225-102-1001-7645'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-102-5678'\n required String cropID, // 'crop-US-001-202-9432'\n required String cachedCropName,// 'Bean'\n required String bedID, // 'bed-US-98225-102-003-4823'\n required String cachedBedName, // '02'\n required DateTime startDate, // '2023-03-19T12:19:14.164090'\n required DateTime pullDate, // '2023-07-19T12:19:14.164090'\n String? varietyID, // null, 'variety-US-001-310-7645'\n String? cachedVarietyName, // null, 'Big Boy'\n String? outcomeID, // null, 'outcome-US-98225-102-1001-3472'\n DateTime? transplantDate, // null, '2023-04-19T12:19:14.164090'\n DateTime? firstHarvestDate, // null, '2023-05-19T12:19:14.164090'\n DateTime? endHarvestDate, // null, '2023-06-19T12:19:14.164090'\n String? sowSeedID, // null, 'seed-US-98225-102-001-3563'\n String? harvestSeedID, // null, 'seed-US-98225-102-005-2185'\n @Default(false) bool usedGreenhouse, // true, false \n @Default(false) bool isVendor, // true, false\n @Default(false) bool seedsAvailable} // true, false\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"outcome",children:"Outcome"}),"\n",(0,a.jsx)(t.p,{children:"Outcome data is gardener-supplied information about the result of a single Planting. We want to specify planting results in a way that:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:"Is useful and actionable for gardeners,"}),"\n",(0,a.jsx)(t.li,{children:"Captures important properties of a planting,"}),"\n",(0,a.jsx)(t.li,{children:"Is relatively easy to provide,"}),"\n",(0,a.jsx)(t.li,{children:"Is interpreted in a relatively consistent manner by different gardeners,"}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'To support these requirements, we define five outcome types: germination, yield, flavor, pest and disease resistance, and appearance. Each planting can receive a "grade" for each of these outcome types on a five point scale. The following table presents the definitions for each scale value for each outcome type.'}),"\n",(0,a.jsxs)(t.table,{children:[(0,a.jsx)(t.thead,{children:(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.th,{}),(0,a.jsx)(t.th,{children:"1"}),(0,a.jsx)(t.th,{children:"2"}),(0,a.jsx)(t.th,{children:"3"}),(0,a.jsx)(t.th,{children:"4"}),(0,a.jsx)(t.th,{children:"5"})]})}),(0,a.jsxs)(t.tbody,{children:[(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Germination"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"None."})," No germination."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," ~25% germination."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," ~50% germination."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," ~75% germination."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," >90% germination.."]})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Yield"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"None."})," Died and/or no food"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," Less food than expected"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," Expected amount of food"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," More food than expected"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," TWay more food than expected"]})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Flavor"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Bad."})," Unappealing flavor"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," Bland flavor"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," Expected flavor."]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," Enjoyable flavor"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," Awesome flavor."]})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Pest and disease resistance"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Very poor."})," >90% damaged"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," ~50% damaged"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," < 25% damaged"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," Very few damaged"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," No damage."]})]}),(0,a.jsxs)(t.tr,{children:[(0,a.jsx)(t.td,{children:(0,a.jsx)(t.strong,{children:"Appearance"})}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Very poor."})," >90% ugly"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Poor."})," ~60% ugly"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"OK."})," ~60% not ugly"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Good."})," ~60% beautiful"]}),(0,a.jsxs)(t.td,{children:[(0,a.jsx)(t.strong,{children:"Excellent."})," >90% beautiful"]})]})]})]}),"\n",(0,a.jsx)(t.p,{children:'In addition, an Outcome type can have a value of "0", which means there is no data regarding that type of outcome.'}),"\n",(0,a.jsx)(t.h4,{id:"outcomeid-management",children:"OutcomeID management"}),"\n",(0,a.jsxs)(t.p,{children:["OutcomeIDs have the format ",(0,a.jsx)(t.code,{children:"outcome-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"Each Outcome entity is associated with exactly one Planting entity. (Note that the converse is not true: a Planting entity need not be associated with an Outcome entity, since the Gardener might not choose to record any Outcome data.)"}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-3",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Outcomes cache the cropID and varietyID associated with their Planting. This is to allow Index and View widgets to display Outcome data without having to retrieve Plantings from the database."}),"\n",(0,a.jsx)(t.p,{children:"Outcome value must be integers between 0 (indicating no data) and 5 (indicating Excellent)."}),"\n",(0,a.jsx)(t.h4,{id:"outcome-entity-representation",children:"Outcome entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Outcome(\n {required String outcomeID, // 'outcome-US-98225-102-1001-5218'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-102-6789'\n required String plantingID, // 'planting-US-98225-102-1001-9213'\n required String cachedCropID, // 'crop-US-001-245-4376'\n required String cachedVarietyID, // 'variety-US-001-321-3214'\n @Default(0) int germination, // 0-5\n @Default(0) int yieldd, // 0-5 (yield is a reserved word)\n @Default(0) int flavor, // 0-5\n @Default(0) int resistance, // 0-5\n @Default(0) int appearance} // 0-5\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"seed",children:"Seed"}),"\n",(0,a.jsx)(t.p,{children:"The ability to save and share seeds within a Chapter is a significant core value proposition for GGC."}),"\n",(0,a.jsx)(t.p,{children:'By "seed", we don\'t mean each individual, tiny seed. We mean the set of all seeds harvested from a planting in a garden in a particular season, or the set of seeds in a seed packet from a commercial vendor.'}),"\n",(0,a.jsxs)(t.p,{children:["Our data model enables us to represent both seeds that are locally produced by gardeners as well as seeds that are produced by vendors. Because a Planting can represent both the seeds that were used to grow it (in the field ",(0,a.jsx)(t.code,{children:"sowSeedID"}),") as well as the seeds that it produced and could be used to grow a new Planting in a subsequent season (in the field ",(0,a.jsx)(t.code,{children:"harvestSeedID"}),'), we get the ability to track the "provenance" of a seed:']}),"\n",(0,a.jsx)("img",{style:{borderStyle:"solid"},src:"/img/develop/release-1.0/data-model/seed-provenance.png"}),"\n",(0,a.jsx)(t.h4,{id:"seedid-management",children:"SeedID management"}),"\n",(0,a.jsxs)(t.p,{children:["SeedIDs have the format ",(0,a.jsx)(t.code,{children:"seed-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"SeedNums is typically taken from the plantingNum of the Planting from which the Seed was harvested."}),"\n",(0,a.jsx)(t.p,{children:"The country and postal code fields are taken from the Planting that this seed was harvested from. This is to ensure that if a Chapter is reorganized, the Seed will move with the Planting it was harvested from."}),"\n",(0,a.jsx)(t.admonition,{title:"Chapter reorganization and seeds",type:"warning",children:(0,a.jsx)(t.p,{children:'Note that Seeds harvested from one postal code in a Chapter can be sowed in another postal code in a Chapter. This means that if a Chapter is split up into two sub-Chapters, there is the possibility that the original Seed will need to be "cloned" into the two sub-Chapters.'})}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-4",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Seed instances cache the gardenerID, cropID, varietyID, cropName, and seedsAvailable field values from the Planting from which they were harvested."}),"\n",(0,a.jsx)(t.p,{children:"A Seed instance is always associated with a single Planting. Each Seed has a plantingID field indicating the Planting from which the Seed was harvested. In addition, that Planting has a harvestSeedID field which points to this Seed. So, there is a bi-directional mapping."}),"\n",(0,a.jsx)(t.p,{children:"A Seed instance can also be associated with one or more additional Plantings as the seed from which the Planting was grown. In this case, the Seed's ID appears in the Planting in the sowSeedID field. Those Plantings do not have to be in the same Garden (in fact, they will often be in a different garden)."}),"\n",(0,a.jsx)(t.p,{children:"The Seed entity provides information about the Planting from which it was harvested (but has no information about where/when it was used to sow new Plantings). This information includes the plantingID, gardenerID, cropID, cropName, varietyID, varietyName and seedsAvailable. Providing this information in the Seed entity simplifies presentation of Seed data in Index and View pages."}),"\n",(0,a.jsx)(t.p,{children:"Finally, in order to safely delete a Seed instance, it must not have been used to sow any Plantings. So that we don't have to search through all the Plantings across an entire chapter, the Seed entity provides a field called sowSeedCount. This field is initialized to zero and incremented whenever a Seed instance is referenced in the sowSeedID field of a new Planting. A Seed instance can only be deleted when the sowSeedCount is zero."}),"\n",(0,a.jsx)(t.admonition,{title:"SowSeedCount is never decremented",type:"warning",children:(0,a.jsx)(t.p,{children:"In the 1.0 release, sowSeedCount is incremented each time a Planting specifies it as their sowSeedID. It is not reliably decremented. Because of this, in the 1.0 release, once a Seed is created and used once as the sowSeedID, it can not be deleted. The only time you can delete a Seed is before it has ever been used as a sowSeedID in a Planting."})}),"\n",(0,a.jsx)(t.admonition,{title:"plantingID field is currently optional",type:"info",children:(0,a.jsx)(t.p,{children:"In a future version, plantingID will be made required."})}),"\n",(0,a.jsx)(t.h4,{id:"seed-entity-representation",children:"Seed entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Seed(\n {required String seedID, // 'seed-US-98225-102-001-3218'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-102-6789'\n String? plantingID,\n @Default(0) int sowSeedCount, // 0, 1, 2\n required String cachedGardenerID, // 'info@heritageseeds.com' \n required String cachedCropID, // 'crop-US-001-201-3462'\n required String cachedVarietyID, // 'variety-US-001-303-6534'\n required String cachedCropName, // 'Tomato'\n required String cachedVarietyName, // 'Cherokee Purple'\n @Default(true) bool cachedSeedsAvailable} // true, false\n)\n"})}),"\n",(0,a.jsx)(t.h4,{id:"seed-caveats",children:"Seed caveats"}),"\n",(0,a.jsxs)(t.p,{children:["In GGC, the Garden associated with a Vendor has a single Planting instance for each Variety for which they offer Seeds. This single Planting instance will have a SeedID in the harvestSeedID field, with ",(0,a.jsx)(t.code,{children:"seedsAvailable"})," set to ",(0,a.jsx)(t.code,{children:"true"}),"."]}),"\n",(0,a.jsx)(t.p,{children:"In reality, a vendor may or may not have seeds in stock for a given Variety at any given time. And, in reality, a vendor will produce their seeds from new Plantings each year. But, GGC will not attempt to keep track of real-time inventory."}),"\n",(0,a.jsx)(t.h3,{id:"observation",children:"Observation"}),"\n",(0,a.jsx)(t.p,{children:"An Observation is a textual comment (and, typically, a picture) provided by a Gardener regarding a specific Planting at a specific point in time."}),"\n",(0,a.jsx)(t.p,{children:"If a Gardener wishes to make a comment about a non-Planting issue (i.e. their Garden, or the Chapter, or whatever), they can use the Chat Rooms for Gardens and Chapters."}),"\n",(0,a.jsx)(t.p,{children:'The essential difference is that an Observation will be "carried along" with a Planting---in other words, when the Gardener retrieves a View of a specific Planting, they will also see all of the Observations associated with that Planting. We hope that this will help create a useful historical record of a Planting.'}),"\n",(0,a.jsx)(t.h4,{id:"observationid-management",children:"ObservationID management"}),"\n",(0,a.jsxs)(t.p,{children:["ObservationIDs have the format ",(0,a.jsx)(t.code,{children:"observation-----"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"ObservationNums start at 4001 and are incremented chapter-wide."}),"\n",(0,a.jsx)(t.p,{children:"The country and postal code fields are taken from the Planting associated with this Observation."}),"\n",(0,a.jsx)(t.h4,{id:"field-notes-5",children:"Field notes"}),"\n",(0,a.jsx)(t.p,{children:"Observations cache several values in order to allow the Observation card to present information without having to retrieve the Planting."}),"\n",(0,a.jsx)(t.p,{children:"Observations are presented in reverse chronological order by lastUpdate. When someone adds a comment, that sets the lastUpdate field."}),"\n",(0,a.jsx)(t.h4,{id:"observation-entity-representation",children:"Observation entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Observation(\n {required String observationID, // 'observation-US-98225-102-4001-5634'\n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-102-6789'\n required String gardenerID, // 'johnson@hawaii.edu'\n required String plantingID, // 'planting-US-98225-102-1002-9432'\n required DateTime observationDate, // '2023-03-19T12:19:14.164090'\n required DateTime lastUpdate, // '2023-03-19T12:19:14.164090'\n required List tagIDs, // ['tag-001-501']\n required List comments, // ['observation-US-98225-102-4001-001-9876']\n required String description, // 'First harvest of the season'\n String? pictureURL, // null, 'https://firebasestorage.googleapis.com/v0/...'\n @Default(false) bool isPrivate, // true, false\n required String cachedCropID, // 'crop-US-001-243-3425'\n required String cachedVarietyID, // 'variety-US-001-323-9654'\n required String cachedBedName, // '03'\n required String cachedCropName, // 'Tomato'\n required String cachedVarietyName, // 'Cherokee Purple'\n required String cachedGardenName, // 'Kale is for Kids'\n required DateTime cachedStartDate} // '2023-03-19T12:19:14.164090'\n)\n"})}),"\n",(0,a.jsx)(t.h4,{id:"observation-comments",children:"Observation Comments"}),"\n",(0,a.jsx)(t.p,{children:"As shown above, each Observation entity includes an embedded (potentially empty) list of ObservationComments, which have this structure:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory ObservationComment(\n {required String observationCommentID, // 'observationcomment-US-98225-102-4001-001-4532'\n required String gardenerID, // 'johnson@hawaii.edu'\n required String description, // 'Is that an aphid on the left leaf?'\n required DateTime lastUpdate} // '2023-03-19T12:19:14.164090' \n)\n"})}),"\n",(0,a.jsx)(t.p,{children:"The lastUpdate field indicates when the comment was made or updated."}),"\n",(0,a.jsx)(t.h3,{id:"tag",children:"Tag"}),"\n",(0,a.jsx)(t.p,{children:'The Tag entity provides "meta-data" that a gardener can use to provide information about the nature of an Observation. Tags serve two basic purposes:'}),"\n",(0,a.jsxs)(t.ol,{children:["\n",(0,a.jsxs)(t.li,{children:["\n",(0,a.jsx)(t.p,{children:"Filtering. A user can specify a set of Tags and filter the Observations by those that satisfy either (both?) of them."}),"\n"]}),"\n",(0,a.jsxs)(t.li,{children:["\n",(0,a.jsx)(t.p,{children:"Badge achievement. Many Badges are earned, at least in part, by posting (public) Observations with specific Tags."}),"\n"]}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'Tags, like Badges, Families, and Chapters, are "global" entities that are not Chapter-specific. Therefore, they can only be managed by system admins.'}),"\n",(0,a.jsx)(t.h4,{id:"tagid-management",children:"TagID management"}),"\n",(0,a.jsxs)(t.p,{children:["TagIDs have the format ",(0,a.jsx)(t.code,{children:"tag-"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"TagIDs start at 001."}),"\n",(0,a.jsx)(t.h4,{id:"tag-entity-representation",children:"Tag entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Tag(\n {required String tagID, // 'tag-001'\n required String name, // '#Biodiversity'\n required String description} // 'Use of practices to increase biodiversity...'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"task",children:"Task"}),"\n",(0,a.jsx)(t.p,{children:"A Task specifies an activity to perform for a specific Planting in a specific Garden. There are two types of tasks:"}),"\n",(0,a.jsxs)(t.ol,{children:["\n",(0,a.jsxs)(t.li,{children:["\n",(0,a.jsxs)(t.p,{children:["An ",(0,a.jsx)(t.em,{children:"automatically created"})," Task that is generated from the dates associated with a Planting, such as ",(0,a.jsx)(t.code,{children:"transplant date"})," or ",(0,a.jsx)(t.code,{children:"first harvest date"}),". Whenever the Gardener adjusts the dates associated with a Planting, the associated Task is updated. Conversely, if a Gardener adjusts the date associated with a Task, then the associated Planting date is updated as well."]}),"\n"]}),"\n",(0,a.jsxs)(t.li,{children:["\n",(0,a.jsxs)(t.p,{children:["A ",(0,a.jsx)(t.em,{children:"manually created"})," Task created by a gardener, such as ",(0,a.jsx)(t.code,{children:"Weed cucumbers"})," or ",(0,a.jsx)(t.code,{children:"Add top dressing to radishes"}),"."]}),"\n"]}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'Tasks are ephemeral. When a Gardener indicates that a task has been completed, it is deleted from the system. For automatically created Tasks that are associated with a Planting date, the system prompts the gardener to verify the completion date prior to deleting the Task. This prompt is used to update the date in the Planting instance. This is an important form of "quality assurance" for Planting dates, since the Gardener typically specifies these dates early in the season during planning. The ability of Tasks to help ensure that Planting dates are accurate can make Chapter data more useful.'}),"\n",(0,a.jsxs)(t.admonition,{title:"Non-ephemeral (manually generated) tasks would be cool",type:"info",children:[(0,a.jsx)(t.p,{children:'Currently, all tasks are ephemeral. It would be potentially useful for a Gardener to be able to mark a manually generated Task as "non-ephemeral". This would mean that if the Gardener plans a future Garden, that task could be retrieved and associated with a new Planting.'}),(0,a.jsx)(t.p,{children:"We will leave this as a feature for a future release."})]}),"\n",(0,a.jsx)(t.h4,{id:"taskid-management",children:"TaskID management"}),"\n",(0,a.jsxs)(t.p,{children:["TaskIDs have the format ",(0,a.jsx)(t.code,{children:"task------"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"TaskIDs start at 001."}),"\n",(0,a.jsx)(t.p,{children:"The country, postal code, gardenNum, and plantingNum fields are taken from the Planting associated with this Task."}),"\n",(0,a.jsx)(t.h4,{id:"task-types",children:"Task Types"}),"\n",(0,a.jsx)(t.p,{children:"Each Task has a TaskType:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"enum TaskType { start, transplant, firstHarvest, endHarvest, pull, other }\n"})}),"\n",(0,a.jsx)(t.p,{children:'The first five correspond to the Planting dates. "Other" is used for manually created Tasks.'}),"\n",(0,a.jsx)(t.h4,{id:"task-titles-and-descriptions",children:"Task titles and descriptions"}),"\n",(0,a.jsx)(t.p,{children:'For automatically generated tasks, the title is automatically generated using the task type plus the variety, for example "Start Tomato (Big Boy)". Automatically generated tasks are not created with a description.'}),"\n",(0,a.jsx)(t.p,{children:"For manually generated tasks, the Gardener must specify the title and can also supply a description if desired."}),"\n",(0,a.jsx)(t.h4,{id:"task-entity-representation",children:"Task entity representation"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"factory Task(\n {required String taskID, // 'task-US-98225-101-1003-001-7634' \n required String chapterID, // 'chapter-US-001'\n required String gardenID, // 'garden-US-98225-101-6789'\n required String taskType, // 'start'\n required String title, // 'Start Tomato (Big Boy)'\n String? gardenerID, // The owner of the garden, i.e. 'johnson@hawaii.edu'.\n String? description, // null, 'Clean up ground cherries.'\n required String cropID, // 'crop-US-001-203-5412'\n required String varietyID, // 'variety-US-001-101-304-6534'\n required String bedID, // 'bed-US-98225-101-003-8956'\n required String plantingID, // 'planting-US-98225-101-1003-3214'\n required DateTime dueDate, // '2023-03-19T12:19:14.164090'\n required String cachedBedName, // '02'\n required String cachedCropName, // 'Tomato'\n required String cachedVarietyName} // 'Kale is for Kids'\n)\n"})}),"\n",(0,a.jsx)(t.h3,{id:"badge",children:"Badge"}),"\n",(0,a.jsx)(t.p,{children:'GGC provides a game mechanic called "Badges". These are designations for Gardens, Gardeners, and (in future) Chapters that recognize the use of best practices for gardening (such as composting), or significant experience with a specific crop, or other behaviors that we wish to encourage.'}),"\n",(0,a.jsx)(t.p,{children:'The Badge game mechanic is implemented through two entities: "Badge" and "BadgeInstance". The Badge entity is a global entity (i.e. independent of any Chapter and defined by the system), and defines the game mechanic. The BadgeInstance entity represents the achievement of a Badge by a Garden, Gardener, or (in future) Chapter.'}),"\n",(0,a.jsx)(t.h4,{id:"badgeid-and-badgeinstanceid-management",children:"BadgeID and BadgeInstanceID management"}),"\n",(0,a.jsxs)(t.p,{children:["BadgeIDs have the format ",(0,a.jsx)(t.code,{children:"badge-"}),". Please see the ",(0,a.jsx)(t.a,{href:"#ids",children:"ID Section"})," for details regarding our approach to ID management."]}),"\n",(0,a.jsx)(t.p,{children:"BadgeIDs start at 001."}),"\n",(0,a.jsxs)(t.p,{children:["BadgeInstanceIDs have the format ",(0,a.jsx)(t.code,{children:"badgeinstance----"}),"."]}),"\n",(0,a.jsx)(t.p,{children:"BadgeInstanceNums start at 001."}),"\n",(0,a.jsx)(t.p,{children:"The country and chapterCode fields are taken from the Chapter associated with this Garden or Gardener (and in the future, Chapter)."}),"\n",(0,a.jsx)(t.p,{children:"There is a BadgeType enum represented as follows:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"enum BadgeType { garden, gardener, chapter }\n"})}),"\n",(0,a.jsx)(t.h4,{id:"badge-entity-representation",children:"Badge entity representation"}),"\n",(0,a.jsx)(t.p,{children:"Badges:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory Badge(\n {required String badgeID, // 'badge-001'\n required String type, // 'garden'\n required String name, // 'Climate Victory'\n required String criteria, // 'A climate victory garden has been...' \n required String level1, // 'The garden is present...'\n required String level2, // 'The garden is present..., and...'\n required String level3, // 'The garden is present..., and..., and...'\n required List tagIDs} // ['tag-024', 'tag-037']\n)\n"})}),"\n",(0,a.jsx)(t.p,{children:"Badge Instances:"}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-dart",children:"const factory BadgeInstance(\n {required String badgeInstanceID, // 'badgeinstance-US-001-001-5634'\n required String chapterID, // 'chapter-US-001'\n required String badgeID, // 'badge-001'\n required int level, // 1\n required String id, // 'johnson@hawaii.edu', 'garden-US-98225-101-6789', 'chapter-US-001'\n required String type, // 'gardener', 'garden', 'chapter'\n required String cachedName, // 'Climate Victory'\n String? data, // null, 'supplementary data'\n String? data2, // null, 'supplementary data2'\n String? data3} // null, 'supplementary data3'\n)\n"})}),"\n",(0,a.jsx)(t.h2,{id:"collections-and-business-logic",children:"Collections and business logic"}),"\n",(0,a.jsx)(t.p,{children:"As noted above, each entity is represented as a Dart class, and made persistent as a document in Firebase."}),"\n",(0,a.jsxs)(t.p,{children:['Groups of entity instances of the same type are also represented as a Dart class, and made persistent as a collection in Firebase. So, for example, there is a Dart class called "Chapter" (to represent individual instances of that entity) and a Dart class called "ChapterCollection" (to manage a set of Chapter instances). On the Firebase side, there is a collection called Chapters, and each document in that collection has the same structure as the corresponding Dart class. We use ',(0,a.jsx)(t.a,{href:"https://pub.dev/packages/freezed",children:"freezed"})," to support the translation between the Dart class instance for an entity and its persistent representation as a Firebase document in JSON format."]}),"\n",(0,a.jsx)(t.p,{children:'The client-side collection classes (ChapterCollection, GardenCollection, etc) are intended to encapsulate the "business logic" for the application.'}),"\n",(0,a.jsx)(t.h2,{id:"privacy",children:"Privacy"}),"\n",(0,a.jsx)(t.p,{children:"On the one hand, we want to preserve certain types of privacy:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'Users pick a unique "username" which is used in postings so that they do not have to reveal their true name.'}),"\n",(0,a.jsx)(t.li,{children:"The application does not reveal (and does not know) the precise location of gardens, only their country and postal code."}),"\n",(0,a.jsx)(t.li,{children:'Users can tag an Observation as "private", and in that case it will not be visible to users outside of the garden\'s owner and editors. This allows users to take photos regarding the garden for their personal data collection without feeling inhibited about it becoming "public". For example, the photo might reveal faces or locations.'}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"On the other hand, we want to facilitate the creation of a community of practice. For this reason, all garden data (plantings, etc) are available, in at least a read-only format, to all members of a chapter."}),"\n",(0,a.jsx)(t.p,{children:"A significant goal for the 1.0 release is to test the hypothesis that it is not problematic for users to share these kinds garden details with others in the chapter."}),"\n",(0,a.jsx)(t.p,{children:"A broader question, that we will not explore in the 1.0 release, is what kinds of data could be made available across Chapters."}),"\n",(0,a.jsx)(t.h2,{id:"ids",children:"IDs"}),"\n",(0,a.jsxs)(t.p,{children:['In NoSQL databases, it is common for each document to be automatically provided upon creation with a unique string called a "docID" which looks something like this: ',(0,a.jsx)(t.code,{children:"tghHU4CVfxHGB"}),". The docID is generated by the server and is guaranteed to be unique. It serves as the primary key for entities in that collection."]}),"\n",(0,a.jsx)(t.p,{children:'In GGC, we use a different approach. There is no "docID" field. Instead, the Crop collection has a unique ID called "cropID", the Chapter collection has a unique ID called "chapterID", and so forth. We tell the NoSQL database (in our case, Firebase) that these various ID fields should be used as the primary key (i.e. the docID) for each of the collections.'}),"\n",(0,a.jsx)(t.p,{children:"Importantly, non-global entities are generally created by clients, and in GGC, clients (not the server) are responsible for generating the primary keys for non-global entities. (The global entities, such as Chapter, Family, Badge, etc. are constructed by the system, not clients.)"}),"\n",(0,a.jsx)(t.p,{children:"We have clients generate the primary keys for non-global entities for the following reasons:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'Rather than a server-generated random string, our client-generated primary keys are "human-readable". You can look at an ID string and know what kind of entity it is associated with (all GGC IDs have a prefix like "chapter-", "crop-", etc). Since many entities have fields containing the IDs of other entities, human-readable IDs help in development and system understanding.'}),"\n",(0,a.jsx)(t.li,{children:"In many cases, an update to the database can involve the creation of a new entity (or entities) as well as updates to other entities to include the primary key of the newly created entity (or entities). If primary keys are generated by the server, such updates would become a complex, multi-step process. Since primary keys are generated by the client, these updates are much more simple to accomplish."}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"However, client-generated primary keys have one significant drawback:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'It becomes technically possible for two clients to generate a "primary key collision", i.e. an attempt by different clients to create two entities with the same primary key value at the same time.'}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:"To deal with this drawback, we have carefully designed the primary keys in GGC to make it extremely unlikely for primary key collisions to occur."}),"\n",(0,a.jsx)(t.p,{children:"First, primary keys are constructed to include one or more of the chapterID, the country code, the postal code, or the gardenID. This means, for example, that rather than it being possible for a primary key collision to occur by any two GGC users anywhere in the world, it is becomes only possible for it to occur between the owner and editors of a single garden."}),"\n",(0,a.jsx)(t.p,{children:'Second, client-generated primary keys are constructed with a "millis" field. This is a four digit number representing the millisecond value at the time the client created the primary key.'}),"\n",(0,a.jsx)(t.p,{children:"We believe that these two properties of primary keys mean that collisions will not occur in practice, even when clients are operating in disconnected mode."}),"\n",(0,a.jsx)(t.p,{children:"Finally, let's say that this exceedingly unlikely event actually occurs. In that case, because we have told Firebase that the plantingID (for example) is the primary key, Firebase will reject the second plantingID creation. In this case, the application can simply report the error and instruct the user to try again in a few seconds. By this time, the local cache should be updated and the request to create the new entity should succeed."}),"\n",(0,a.jsxs)(t.p,{children:["Note that ",(0,a.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore/best-practices#hotspots",children:"Firebase recommends against creating documentIDs with lexicographically close ranges"}),". We expect that the inclusion of the millis field mitigates this potential performance issue."]}),"\n",(0,a.jsx)(t.h2,{id:"normalization-and-caching",children:"Normalization and caching"}),"\n",(0,a.jsxs)(t.p,{children:['A best practice for relational database design is "',(0,a.jsx)(t.a,{href:"https://en.wikipedia.org/wiki/Database_normalization",children:"normalization"}),'", which means that a value should only occur in one place at a time. Normalization has a number of virtues, such as making updates and deletions more efficient and less error prone. But normalization also has a cost: queries can become very complicated, involving complex "joins" from a variety of data sources.']}),"\n",(0,a.jsx)(t.p,{children:"The GGC app has the following design considerations that impact on the issue of normalization:"}),"\n",(0,a.jsxs)(t.ul,{children:["\n",(0,a.jsx)(t.li,{children:'Updates and deletions are (relatively) rare. GGC is mostly an "additive" database. While deletions and updates can occur, it\'s OK if they are "expensive".'}),"\n",(0,a.jsx)(t.li,{children:"Reads are common, and to make these reads fast, GGC implements client-side caches (using Riverpod) for many of the entities."}),"\n",(0,a.jsx)(t.li,{children:"Gardeners do not access to data outside their Chapter, so client-side caches are not impacted if the number of Chapters in GGC becomes large."}),"\n"]}),"\n",(0,a.jsx)(t.p,{children:'To simplify retrieval and caching of the appropriate chapter or garden-level "slice" of the database by a client, almost all GGC entities include a chapterID and gardenID field.'}),"\n",(0,a.jsx)(t.p,{children:'We also "denormalize" by providing "redundant" fields in certain entities. For example, in some cases a document will include a cropName field even though it already has a cropID field. We do this avoid having to download large numbers of documents (i.e. Plantings for all Gardens in the Chapter) in order to perform a calculation. These redundant field names have the prefix "cached" in order to make this denormalization explicit in the data model.'}),"\n",(0,a.jsx)(t.h2,{id:"root-collections-vs-subcollections",children:"Root collections vs subcollections"}),"\n",(0,a.jsxs)(t.p,{children:["In Firebase, you can organize the data into root collections or subcollections, as explained in ",(0,a.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore/manage-data/structure-data",children:"Choose a data structure"}),"."]}),"\n",(0,a.jsx)(t.p,{children:"Since GGC involves many many-to-many relationships, we choose to organize all of our data as root collections."}),"\n",(0,a.jsx)(t.p,{children:"In Firebase, there are no performance differences between root collections and subcollections, so we do not gain or lose anything by making this choice."}),"\n",(0,a.jsx)(t.h2,{id:"chat-rooms",children:"Chat rooms"}),"\n",(0,a.jsxs)(t.p,{children:["We use the ",(0,a.jsx)(t.a,{href:"https://pub.dev/packages/flutter_chat_ui",children:"Flutter Chat UI"})," package to implement Chat rooms and users. This results in the addition of some collections to Firebase. We do not document this here."]})]})}function c(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>s});var a=n(7294);const i={},r=a.createContext(i);function s(e){const t=a.useContext(r);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),a.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/bc1f8660.74e6e406.js b/assets/js/bc1f8660.74e6e406.js deleted file mode 100644 index f664440d1..000000000 --- a/assets/js/bc1f8660.74e6e406.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[2446],{9504:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>h,contentTitle:()=>d,default:()=>o,frontMatter:()=>r,metadata:()=>a,toc:()=>l});var s=n(5893),i=n(1151);const r={hide_table_of_contents:!1},d="Data Model",a={id:"develop/design/data-model-old",title:"Data Model",description:"This page documents the data model intended to satisfy the 1.0 release requirements.",source:"@site/docs/develop/design/data-model-old.md",sourceDirName:"develop/design",slug:"/develop/design/data-model-old",permalink:"/docs/develop/design/data-model-old",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1}},h={},l=[{value:"Entities",id:"entities",level:2},{value:"Chapter",id:"chapter",level:3},{value:"User",id:"user",level:3},{value:"Gardener",id:"gardener",level:3},{value:"Garden",id:"garden",level:3},{value:"Editor",id:"editor",level:3},{value:"Bed",id:"bed",level:3},{value:"Planting",id:"planting",level:3},{value:"Variety",id:"variety",level:3},{value:"Crop",id:"crop",level:3},{value:"Family",id:"family",level:3},{value:"Outcome",id:"outcome",level:3},{value:"Seed",id:"seed",level:3},{value:"Observation",id:"observation",level:3},{value:"Task",id:"task",level:3},{value:"Collections and business logic",id:"collections-and-business-logic",level:2},{value:"ChapterCollection",id:"chaptercollection",level:3},{value:"UserCollection",id:"usercollection",level:3},{value:"GardenCollection",id:"gardencollection",level:3},{value:"Other Data Model issues",id:"other-data-model-issues",level:2},{value:"Privacy",id:"privacy",level:3},{value:"IDs",id:"ids",level:3},{value:"Normalization and caching",id:"normalization-and-caching",level:3},{value:"Local-first, caching, and disconnected operation",id:"local-first-caching-and-disconnected-operation",level:3}];function c(e){const t={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.header,{children:(0,s.jsx)(t.h1,{id:"data-model",children:"Data Model"})}),"\n",(0,s.jsx)(t.p,{children:"This page documents the data model intended to satisfy the 1.0 release requirements."}),"\n",(0,s.jsx)(t.h2,{id:"entities",children:"Entities"}),"\n",(0,s.jsx)(t.p,{children:'In this document, "entity" refers to the fundamental forms of persistent data objects. Each entity is defined as a set of typed fields.'}),"\n",(0,s.jsx)(t.p,{children:'Entities are persisted through a set of Firebase collections. In general, each entity is a document that is stored in a corresponding collection: all of the Chapter entity documents are stored in a Firebase collection called Chapters, all of the Gardener entity documents are stored in a Firebase collection called Gardeners. Unfortunately, the "News" entity documents are stored in a Firebase collection called "Newss" (with two s\'s at the end) so that the entity and collection name are different.'}),"\n",(0,s.jsx)(t.p,{children:'In the ggc_app application, there are Dart "domain" classes that mirror these Firebase collections, so there is a Dart class called "Chapter", a Dart class called "ChapterCollection", and so forth.'}),"\n",(0,s.jsx)(t.p,{children:'To facilitate the design description, each field of an entity will be documented with one of the following "variants" R, O, or D:'}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Variant"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"Required: The field value is stored as an explicit value in each document of the entity's collection, and all documents have a value for this field."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"Optional: The field value may or may not exist in a given document associated with the collection."})]})]})]}),"\n",(0,s.jsxs)(t.p,{children:["Finally, the following documentation includes example documents (JSON objects) generated from the ",(0,s.jsx)(t.a,{href:"https://github.com/geogardenclub/data-model-migrator",children:"DataModelMigrator"})," application."]}),"\n",(0,s.jsx)(t.h3,{id:"chapter",children:"Chapter"}),"\n",(0,s.jsx)(t.p,{children:"The Chapter entity contains the following fields:"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"chapter-"})]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"name"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:'The name of the chapter, such as "Whatcom-WA"'})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"zipCodes"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The zip codes associated with the chapter, derived from the ChapterZipMap."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"profilePicture"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The path to a profile picture for this chapter"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"pictures"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The paths for additional pictures of this chapter."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here is an example of a Chapter collection document from the migrated data:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:' {\n "chapterID": "chapter-001",\n "name": "Whatcom-WA",\n "profilePicture": "/img/chapters/bellingham/bellingham-chapter-map.png",\n "pictures": [\n "/img/chapters/bellingham/chapter-007.jpg"\n ],\n "zipcodes": [\n "98225",\n "98226",\n "98227",\n "98228",\n "98229"\n ],\n "lastUpdate": "2023-03-19T12:19:14.164090"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"user",children:"User"}),"\n",(0,s.jsx)(t.p,{children:"The User entity represents all of the people who have created an account with the system."}),"\n",(0,s.jsx)(t.p,{children:"Note that not all Gardeners are users: commercial seed vendors won't generally have an account on the system."}),"\n",(0,s.jsx)(t.p,{children:"Currently all Users are also Gardeners, though the design does not require this. In future, there may also be Users who are not Gardeners."}),"\n",(0,s.jsx)(t.p,{children:"Every user is associated with a unique email address, which is their UserID."}),"\n",(0,s.jsx)(t.p,{children:"For the 1.0 release, the data model does not include information about the subscriptions, payments, credit card, etc associated with a gardener."}),"\n",(0,s.jsx)(t.p,{children:"Each User entity provides the following information:"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"userID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"UserID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"A unique ID corresponding to the email address associated with this user."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The chapterID associated with this Gardener."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"name"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The users name. The user name is normally not provided in the UI."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"username"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The username is what is normally used to identify the user in the UI."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"imagePath"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"A path to the image to be associated with this user."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The DateTime object indicating the last update."})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"To illustrate, here is an example document from the Gardener collection:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:' {\n "userID": "johnson@hawaii.edu",\n "chapterID": "chapter-001",\n "name": "Philip Johnson",\n "username": "@fiveoclockphil",\n "imagePath": "",\n "lastUpdate": "2023-04-01T00:00:00.000Z"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"gardener",children:"Gardener"}),"\n",(0,s.jsx)(t.p,{children:'The Gardener entity is designed to represent two distinct classes of gardeners in GGC: (1) "normal" home gardeners and (2) commercial seed vendors.'}),"\n",(0,s.jsx)(t.p,{children:'The benefit of having the Gardener entity represent both "normal" gardeners as well as commercial seed vendors is that it results in a uniform mechanism in the app to support "seed providers": any Gardener (which can either be a normal home gardener or a commercial seed vendor) grows a Garden which contains Plantings which (may or may not) produce seeds that are available within the Chapter.'}),"\n",(0,s.jsxs)(t.p,{children:['This does create some UI complexity, in that commercial seed vendors are not intended to be "Chapter members" in the normal sense. There is a boolean ',(0,s.jsx)(t.code,{children:"isVendor"}),' field that can be used to maintain two local caches of Gardener collections: one containing all of the "normal" home gardeners, and one containing the "vendor" gardeners.']}),"\n",(0,s.jsx)(t.p,{children:"Each Gardener entity provides the following information:"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenerID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenerID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"A unique ID corresponding to the email address of this user."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The chapterID associated with this Gardener."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"isVendor"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"bool"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A flag indicating whether this entity instance represents a vendor (if ",(0,s.jsx)(t.code,{children:"true"}),") or a home gardener (if ",(0,s.jsx)(t.code,{children:"false"}),")"]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"vendorName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsxs)(t.td,{children:["If ",(0,s.jsx)(t.code,{children:"isVendor"}),", then this string is present and specifies the full vendor name."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"vendorShortName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsxs)(t.td,{children:["If ",(0,s.jsx)(t.code,{children:"isVendor"}),", then this string is present and specifies a short vendor name."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"vendorURL"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsxs)(t.td,{children:["If ",(0,s.jsx)(t.code,{children:"isVendor"}),", then this string is present and specifies a URL to the vendor site."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"masterGardener"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"boolean"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.code,{children:"true"}),' if this gardener is a Master Gardener. (This is an example "badge". There could be many others.)']})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The DateTime object indicating the last update."})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"To illustrate, here is an example document from the Gardener collection:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:' {\n "gardenerID": "jennacorindeane@gmail.com",\n "chapterID": "chapter-001",\n "isMasterGardener": true,\n "isVendor": false,\n "vendorName": "",\n "vendorShortName": "",\n "vendorURL": "",\n "lastUpdate": "2023-03-19T12:19:14.164836"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"garden",children:"Garden"}),"\n",(0,s.jsx)(t.p,{children:"The Garden entity represents a plot of land (or maybe even just some pots) that can hold Plantings over one or more years."}),"\n",(0,s.jsx)(t.p,{children:"The Garden entity contains the following fields:"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O/I"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"garden--"}),". Each ",(0,s.jsx)(t.code,{children:""})," is unique within a Chapter and starts at 100."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"name"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name of the Chapter. This should normally be unique within a Chapter."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"ownerID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenerID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:'The single Gardener who "owns" this Garden, which gives them full management rights. This ID corresponds to their email.'})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"profilePicture"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"The path to an image to be used as the profile picture for this garden."}),(0,s.jsx)(t.td,{})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"pictures"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List"})}),(0,s.jsx)(t.td,{children:"A list of image paths."}),(0,s.jsx)(t.td,{})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"isVendor"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"bool"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["If ",(0,s.jsx)(t.code,{children:"true"}),", then this is a commercial garden, not a home garden."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"pictures"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"(Public) Pictures of this garden."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"climateVictoryGarden"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"boolean"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:'An example "badge" associated with this garden.'})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The last update timestamp."})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here is an example Garden document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:' {\n "gardenID": "garden-001-102",\n "chapterID": "chapter-001",\n "name": "Kale is for Kids",\n "ownerID": "jbeck913360@hotmail.com",\n "profilePicture": "/img/gardens/45ght3cf/garden-001.jpg",\n "pictures": [\n "/img/gardens/45ght3cf/garden-007-birds-eye-view.jpg",\n "/img/gardens/45ght3cf/garden-002.jpg"\n ],\n "isVendor": false,\n "isClimateVictoryGarden": false,\n "lastUpdate": "2023-03-20T15:45:56.856468"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"editor",children:"Editor"}),"\n",(0,s.jsx)(t.p,{children:'In the 1.0 release, the access control capability enables a Gardener to allow another Chapter member to edit one of their gardens. (There is no implementation of a "viewer", who can see more of someone else\'s garden than a normal Chapter member.)'}),"\n",(0,s.jsx)(t.p,{children:"This capability is implemented by the Editor entity, which implements a mapping between a Garden and a Gardener:"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"editorID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"editor--"})]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The garden for which editor access is being granted."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenerID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenerID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The gardener who is obtaining editor access to the above garden."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here is an example Editor document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{\n "editorID": "editor-001-001",\n "gardenID": "garden-001-101",\n "chapterID": "chapter-001",\n "gardenerID": "jbeck913360@hotmail.com",\n "lastUpdate": "2023-03-20T15:45:56.856359"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"bed",children:"Bed"}),"\n",(0,s.jsx)(t.p,{children:"Each Garden consists of a number of Beds."}),"\n",(0,s.jsx)(t.p,{children:"The Bed entity has the following conceptual structure."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"bedID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"BedID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"bed---"}),". BedNums are unique within a Chapter and Garden and start at 200."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The garden associated with this Bed."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"name"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name associated with this Bed."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here is an example Bed document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:' {\n "bedID": "bed-001-102-215",\n "chapterID": "chapter-001",\n "gardenID": "garden-001-102",\n "name": "15",\n "lastUpdate": "2023-03-20T15:45:56.856565"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"planting",children:"Planting"}),"\n",(0,s.jsx)(t.p,{children:'A Planting is represents a set of plants of the same variety or crop, planted in a single bed, all with the same approximate timings (i.e. planting, transplanting, harvesting, etc.). If the same variety or crop is planted in two different beds, then this must be represented by two Planting instances. (Alternatively, you could define an additional, "virtual" Bed that conceptually represents the contents of two physical beds and put a single Planting in it.)'}),"\n",(0,s.jsx)(t.p,{children:'It is common during the garden planning process to first design the garden at the "crop" level, and then later refine the plan by specifying a specific variety of each crop. To support this incremental design process, the Planting entity only requires a Crop to be specified.'}),"\n",(0,s.jsx)(t.p,{children:"The Planting entity has the following conceptual structure."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"plantingID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"PlantingID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"planting---"}),". PlantingNums are unique within a Chapter and Garden and start at 1000."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The garden associated with this Bed."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"CropID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Crop associated with this Planting."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name associated with the above CropID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"year"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"Number"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The year associated with a Garden. Not required when this Planting is associated with a vendor Garden."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"bedID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"BedID"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The BedID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"VarietyID"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The VarietyID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The name associated with the above VarietyID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"outcomeID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"OutcomeID"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The outcomes associated with this planting."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"seedID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SeedID"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The seed that was used to create this planting."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"startDate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"When the plant was started."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"transplantDate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"When the plant was transplanted from greenhouse to bed (if that happened.)"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"firstHarvestDate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"When the plant first produced food."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"endHarvestDate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"When the plant last produced food."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"pullDate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"When the plant was pulled from the garden."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"usedGreenhouse"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"boolean"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsxs)(t.td,{children:["If the planting was started in a greenhouse. Defaults to ",(0,s.jsx)(t.code,{children:"false"}),"."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"isVendor"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"boolean"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsxs)(t.td,{children:["If this planting is associated with a commercial seed grower. Defaults to ",(0,s.jsx)(t.code,{children:"false"}),"."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"hasSeeds"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"boolean"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsxs)(t.td,{children:["If this planting produced seeds. Defaults to ",(0,s.jsx)(t.code,{children:"false"}),"."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"seedsAvailable"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"boolean"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsxs)(t.td,{children:["If this planting produced seeds that the Gardener can provide to others. Defaults to ",(0,s.jsx)(t.code,{children:"false"}),"."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here is an example Planting document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:' {\n "plantingID": "planting-001-101-1034",\n "chapterID": "chapter-001",\n "gardenID": "garden-001-101",\n "cropID": "crop-001-544",\n "cropName": "Tomatillo",\n "year": 2023,\n "bedID": "bed-001-101-218",\n "varietyID": "variety-001-904",\n "varietyName": "De Milpa",\n "outcomeID": null,\n "startDate": "2023-03-10T00:00:00.000",\n "transplantDate": "2023-05-01T00:00:00.000",\n "firstHarvestDate": null,\n "endHarvestDate": null,\n "pullDate": "2023-08-31T00:00:00.000",\n "seedID": "seed-001-105-1048-103",\n "usedGreenhouse": true,\n "isVendor": false,\n "hasSeeds": false,\n "seedsAvailable": false,\n "lastUpdate": "2023-03-20T15:45:56.872599"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"variety",children:"Variety"}),"\n",(0,s.jsx)(t.p,{children:'Variety is a specific kind of Crop which has seeds. For example, a seed packet such as "Tomato (Sun Gold)" specifies the crop ("Tomato") and the Variety ("Sun Gold").'}),"\n",(0,s.jsx)(t.p,{children:"It is possible for multiple gardeners (either home or commercial) to produce Seeds of the same Variety."}),"\n",(0,s.jsx)(t.p,{children:"The Variety entity has the following conceptual structure."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"VarietyID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"variety-chapterNum-varietyNum"}),". VarietyNums are unique within a Chapter and start at 900."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"CropID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Crop associated with this Variety."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name associated with the above CropID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"name"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name associated with this Variety."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here is a sample Variety document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:' {\n "varietyID": "variety-001-923",\n "chapterID": "chapter-001",\n "cropID": "crop-001-534",\n "cropName": "Radicchio",\n "name": "Pasqualino",\n "lastUpdate": "2023-03-20T15:45:56.858247"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"crop",children:"Crop"}),"\n",(0,s.jsx)(t.p,{children:'Crop specifies a type of plant independent of its Variety. For example, "Tomato" is a Crop.'}),"\n",(0,s.jsx)(t.p,{children:"Each Variety is associated with a single Crop."}),"\n",(0,s.jsx)(t.p,{children:"The Crop entity has the following conceptual structure."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"CropID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"crop--"}),". CropNums are unique within a Chapter and start at 500."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"familyID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"FamilyID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The plant Family associated with this Crop."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"name"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name associated with this Crop."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here is an example Crop document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{\n "cropID": "crop-001-503",\n "chapterID": "chapter-001",\n "familyID": "family-411",\n "name": "Asparagus",\n "lastUpdate": "2023-03-20T15:45:56.857232"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"family",children:"Family"}),"\n",(0,s.jsx)(t.p,{children:'Family specifies the botanical family associated with one or more Crops (and implicitly, Varieties). For example, the "Nightshade" family groups together Tomatoes, Potatoes, and Peppers. Family data is useful during garden planning to facilitate planning issues including crop rotation and companion planting.'}),"\n",(0,s.jsx)(t.p,{children:'The Family entity is one of the few "global" collections in GGC. In other words, it does not include a ChapterID; every Chapter will download this collection in its entirety. (Which is not a hardship, there are only around a dozen Family documents.)'}),"\n",(0,s.jsx)(t.p,{children:"The Family entity has the following conceptual structure."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"familyID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"FamilyID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"family-"}),". FamilyNums are unique and start at 400."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"formal"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The formal name associated with this Family."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"common"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The common name associated with this Family."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"examples"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"A documentation string providing examples of Crops within this Family."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Note that computing the cropIDs or varietyIDs associated with a familyID requires specifying a chapterID."}),"\n",(0,s.jsx)(t.p,{children:"Here is an example Family document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{\n "familyID": "family-406",\n "formal": "Fabaaceae",\n "common": "Legume",\n "examples": "bean, pea, peanuts",\n "lastUpdate": "2023-03-20T15:45:56.856873"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"outcome",children:"Outcome"}),"\n",(0,s.jsx)(t.p,{children:"Outcome data is gardener-supplied information about the result of a single planting. We want to specify results of a planting that is useful and actionable for gardeners, that captures the most important properties of a planting, that is relatively easy to provide, and that is specified in sufficient detail that we can create meaningful aggregations of outcome data for crops and varieties."}),"\n",(0,s.jsx)(t.p,{children:'To support these requirements, we define five outcome types: germination, yield, flavor, pest and disease resistance, and appearance. Each planting can receive a "grade" for each of these outcome types on a five point scale. The following table presents the definitions for each scale value for each outcome type.'}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{}),(0,s.jsx)(t.th,{children:"1"}),(0,s.jsx)(t.th,{children:"2"}),(0,s.jsx)(t.th,{children:"3"}),(0,s.jsx)(t.th,{children:"4"}),(0,s.jsx)(t.th,{children:"5"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.strong,{children:"Germination"})}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Failure."})," No seeds germinated."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Poor."})," Approximately a quarter of the seeds germinated."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"OK."})," Approximately half of the seeds germinated."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Good."})," Approximately 3/4 of the seeds germinated"]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Outstanding."})," 90% or more of the seeds germinated."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.strong,{children:"Yield"})}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"None."})," The planting died and/or did not yield any food."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Minimal."})," The planting yielded significantly less food than expected."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"OK."})," The planting yielded the expected amount of food."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Good."})," The planting yielded somewhat more food than expected."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Outstanding."})," The planting yielded significantly more food than expected."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.strong,{children:"Flavor"})}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Bad."})," Not worth eating."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Bland."})," Worth eating, but only a little."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"OK."})," Expected level of flavor."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Good."})," Better than OK flavor, enjoyable to eat."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Outstanding."})," Can't imagine it tasting better."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.strong,{children:"Pest and disease resistance"})}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Extremely poor."})," 90% or more of the plantings have damage."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Poor."})," More than half of the plantings have damage."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"OK."})," No more than a quarter of plantings have damage."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Good."})," Only a few plantings have damage."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Outstanding."})," No observable damage."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.strong,{children:"Appearance"})}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Almost all ugly."})," 90% or more of the crop is ugly."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Mostly ugly."})," Over 50% of the crop is ugly."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Mostly OK."})," Over 50% of the crop is OK."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Mostly beautiful."})," Over 50% of the crop is beautiful."]}),(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.strong,{children:"Almost all beautiful."})," 90% or more of the crop is beautiful."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"The Family entity has the following conceptual structure."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"outcomeID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"OutcomeID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"outcome---"}),". OutcomeNums are equal to the PlantingNum of the Planting associated with this Outcome."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"CropID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Crop associated with this Outcome."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"VarietyID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Variety associated with this Outcome."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"plantingID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"PlantingID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Planting associated with this Outcome."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"year"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"Number"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The year associated with this Outcome."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"appearance"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"number"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The appearance outcome value, if available."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"flavor"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"number"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The flavor outcome value, if available."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"germination"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"number"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The germination outcome value, if available."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"resistance"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"number"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The resistance outcome value, if available."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"yield"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"number"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The yield outcome value, if available."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"By design, an outcomeID's numerical suffix will always be the same as its associated plantingID."}),"\n",(0,s.jsx)(t.p,{children:"Here is an example of an Outcome document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{\n "outcomeID": "outcome-001-101-1039",\n "chapterID": "chapter-001",\n "gardenID": "garden-001-101",\n "cropID": "crop-001-546",\n "varietyID": "variety-001-861",\n "plantingID": "planting-001-101-1039",\n "year": 2022,\n "germination": 3,\n "yield": 5,\n "flavor": 5,\n "resistance": 4,\n "appearance": 5,\n "lastUpdate": "2023-03-20T15:45:56.873320"\n }\n'})}),"\n",(0,s.jsx)(t.h3,{id:"seed",children:"Seed"}),"\n",(0,s.jsx)(t.p,{children:"The ability to save and share seeds within a Chapter is a significant core value proposition for GGC."}),"\n",(0,s.jsxs)(t.p,{children:['Creating an effective UX for seed saving and sharing means (among other things) that we need to represent seeds explicitly within the data model. By "seed", we don\'t mean each individual, tiny seed. We mean the set of all seeds harvested from a planting in a garden in a particular season. We won\'t represent a "count" of the number of seeds available, as that seems too onerous. Instead, we\'ll just provide a flag (',(0,s.jsx)(t.code,{children:"seedsAvailable"}),") associated with a Planting that a Gardener can use to indicate that there exist (some number of) seeds to share."]}),"\n",(0,s.jsx)(t.p,{children:'Our data model enables us to represent both seeds that are locally produced by gardeners as well as seeds that are produced by vendors. One benefit of our design is the ability to represent the "provenance" of a seed. As a simple example:'}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Planting"}),(0,s.jsx)(t.th,{children:"Origin of the seeds for this Planting"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:'Bean (Scarlet Runner), "Alderwood" garden (2023)'}),(0,s.jsx)(t.td,{children:'Bean (Scarlet Runner), "Alderwood" garden (2022)'})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:'Bean (Scarlet Runner), "Alderwood" garden (2022)'}),(0,s.jsx)(t.td,{children:'Bean (Scarlet Runner), "Kale is for Kids" garden (2021)'})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:'Bean (Scarlet Runner), "Kale is for Kids" garden (2021)'}),(0,s.jsx)(t.td,{children:'Bean (Scarlet Runner), "Johnny\'s Seeds" garden (vendor)'})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:'Bean (Scarlet Runner), "Johnny\'s Seeds" garden (vendor)'}),(0,s.jsx)(t.td,{children:"unknown"})]})]})]}),"\n",(0,s.jsx)(t.p,{children:'In other words, our data model can represent a "chain" of Plantings, in which one Planting produces Seeds which are used to grow a subsequent Planting. When you add in the ability for a gardener to inspect this chain, and even learn about the history and observations of any of the Plantings in the chain, it becomes apparent that this has the potential to be an interesting resource for seed saving and sharing.'}),"\n",(0,s.jsx)(t.p,{children:"The Seed entity has the following conceptual structure:"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"seedID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"SeedID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"seed----"}),". SeedNums are unique within a Chapter and Garden and start at 000."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The GardenID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"plantingID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"PlantingID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The PlantingID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"CropID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The CropID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"VarietyID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The VarietyID"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name of the Garden associated with the above GardenID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name of the Crop associated with the above CropID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name of the Variety associated with the above VarietyID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"seedsAvailable"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"bool"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["This field is ",(0,s.jsx)(t.code,{children:"true"})," if this Seed is currently available for sharing."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here is an example Seed document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{\n "seedID": "seed-001-115-1035-104",\n "chapterID": "chapter-001",\n "gardenID": "garden-001-115",\n "plantingID": "planting-001-115-1035",\n "cropID": "crop-001-524",\n "varietyID": "variety-001-905",\n "gardenName": "Unknown vendor",\n "cropName": "Lettuce",\n "varietyName": "Mix",\n "seedsAvailable": true,\n "lastUpdate": "2023-03-20T15:45:56.891813"\n }\n'})}),"\n",(0,s.jsxs)(t.p,{children:["In general, a Garden associated with a vendor will have a single Planting instance for each Variety that they offer. This Planting instance will have a single Seed instance, with ",(0,s.jsx)(t.code,{children:"seedsAvailable"})," set to ",(0,s.jsx)(t.code,{children:"true"}),'. In reality, a vendor may or may not have seeds in stock for a given Variety at any given time. And, in reality, a vendor will produce their seeds from growing plants each year. But, we will not represent these "realities" about vendor gardens and seeds in our data model, at least for the 1.0 release.']}),"\n",(0,s.jsx)(t.h3,{id:"observation",children:"Observation"}),"\n",(0,s.jsx)(t.p,{children:"An observation is a note (and, typically, a picture) taken by a gardener regarding a planting at a specific point in time."}),"\n",(0,s.jsx)(t.p,{children:"The Observation entity has the following conceptual structure."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"observationID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ObservationID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique ID with the format ",(0,s.jsx)(t.code,{children:"observation---"}),". ObservationNums are unique within a Chapter and Garden and start at 700."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Garden associated with this Observation."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Garden name associated with the above GardenID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"plantingID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"PlantingID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Planting associated with this Observation."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"CropID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Crop associated with this Observation."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name associated with the above CropID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"VarietyID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The VarietyID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The name associated with the above VarietyID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"observationDate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The time and date associated with this Observation."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"tags"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"A list of strings that tag this Observation."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"description"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"A textual description of this Observation."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"picture"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"A string that can be used to retrieve the picture associated with this Observation. Or the empty string."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A ",(0,s.jsx)(t.code,{children:"DateTime"})," instance that timestamps the last update."]})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here is an example Observation document:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:' {\n "observationID": "observation-001-101-707",\n "chapterID": "chapter-001",\n "gardenID": "garden-001-101",\n "plantingID": "planting-001-101-1044",\n "cropID": "crop-001-529",\n "varietyID": "variety-001-812",\n "gardenName": "Alderwood",\n "cropName": "Pea",\n "varietyName": "Sugar Snap",\n "observationDate": "2022-05-20T00:00:00.000",\n "tags": [\n "phenology",\n "first flower"\n ],\n "description": "First pea flower! Peas looking very happy.",\n "picture": "observation-004.jpg",\n "lastUpdate": "2023-04-01T00:00:00.000Z"\n }\n'})}),"\n",(0,s.jsx)(t.p,{children:"If we provide a specific set of tags, rather than allow a gardener to enter free text, then the tag system will be much more useful. Here is a proposal for an initial set of tags:"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Tag"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Pest"}),(0,s.jsx)(t.td,{children:"This observation is useful because: (a) the gardener might choose to rotate beds for this Planting in future seasons, and other gardeners will find it useful to know in real time that the pest is present in their community."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"First Harvest, Last Harvest"}),(0,s.jsx)(t.td,{children:"These observations are useful to the gardener because it provides more detailed guidance on how long a particular PlantID needs to actually be in a bed. Community members also will find this of use in their own garden planning."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"First Frost, Last Frost"}),(0,s.jsx)(t.td,{children:'These seem like they could be useful for planning purposes in future years to decide when it\'s safe to have certain plants in the garden They could also be used to validate weather station data against the actual climate situation in the garden. Interestingly, if we want to get chapter-wide info on frost dates, we might have to communicate that one gardener observed a frost event to all the other gardeners in the chapter and ask them to confirm/deny frost in their garden (this disambiguates the "no frost" from "no data" situation.)'})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Disease"}),(0,s.jsx)(t.td,{children:"Different than pest, which is animal specific. Disease might be leaf curl, wilts etc."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Companion"}),(0,s.jsx)(t.td,{children:"I think it would be interesting to see examples of plants benefitting from each other. For example, plants vining on each other, providing shade, protecting from pests."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Technique"}),(0,s.jsx)(t.td,{children:"Jessie once brought up that she would love to see examples of how other gardeners trellis/support plants and I think there could be good learning from sharing of these systems. That makes me think about planting strategies in general. I just planted potatoes for the first time and went online to see how best to do that. There are many ways to plant potatoes. Unknown if there are ways more suited for my climate or the type of potato I planted. I could imagine using the app to peruse pictures of potato planting strategies, perhaps even filtering by variety, to get support for this process."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"First leaf, First bud, First Flower, First seed"}),(0,s.jsx)(t.td,{children:"These phenology tags make patterns in climate change obvious as well as provide insight into how two garden's climates might differ. For example, Jessie and I both have the same kind of raspberry. Knowing if her raspberry flowers/fruits/leafs out etc earlier or later than mine can explain to me other differences in our garden's performance."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Aesthetic"}),(0,s.jsx)(t.td,{children:"In addition to the above \"useful\" tags, we also think it's important to have a final category of tag that indicates an observation that the gardener thinks is interesting even if it's not particularly actionable. (These observations can be filtered out by other gardeners if they don't want to see them.)"})]})]})]}),"\n",(0,s.jsx)(t.p,{children:'In addition, we might want to have tags that provide "meta" information:'}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Tag"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Public"}),(0,s.jsx)(t.td,{children:"Indicate if an observation can appear on the public page."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Help wanted"}),(0,s.jsx)(t.td,{children:'Indicates if the Observation describes an issue or problem for which the gardener needs help. For example, "What is this pest?"'})]})]})]}),"\n",(0,s.jsx)(t.h3,{id:"task",children:"Task"}),"\n",(0,s.jsx)(t.p,{children:"A task is a todo/reminder for the gardener. There are two types of tasks:"}),"\n",(0,s.jsxs)(t.ol,{children:["\n",(0,s.jsxs)(t.li,{children:["A task generated from a planting, such as ",(0,s.jsx)(t.code,{children:"transplant"})," or ",(0,s.jsx)(t.code,{children:"first harvest"}),". Eventually, GeoGardenClub will generate these tasks automatically when a new planting is created."]}),"\n",(0,s.jsxs)(t.li,{children:["A task created by the gardener, such as ",(0,s.jsx)(t.code,{children:"Weed bed 1"})," or ",(0,s.jsx)(t.code,{children:"Water bed 2"}),"."]}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"The Task entity has the following conceptual structure."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Field"}),(0,s.jsx)(t.th,{children:"Type"}),(0,s.jsx)(t.th,{children:"R/O"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"taskID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"TaskID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["A unique identifier for this Task with the format ",(0,s.jsx)(t.code,{children:"task----taskNum"}),". If the task is created by the gardener the ",(0,s.jsx)(t.code,{children:""})," is ",(0,s.jsx)(t.code,{children:"0000"}),"."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"chapterID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"ChapterID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The ChapterID."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"gardenID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"GardenID"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The Garden associated with this Task."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"taskType"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"TaskType"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsxs)(t.td,{children:["The type of task. There are 6 task types: ",(0,s.jsx)(t.code,{children:"sow"}),", ",(0,s.jsx)(t.code,{children:"transplant"}),", ",(0,s.jsx)(t.code,{children:"firstHarvest"}),", ",(0,s.jsx)(t.code,{children:"endHarvest"}),", ",(0,s.jsx)(t.code,{children:"pull"}),", and ",(0,s.jsx)(t.code,{children:"other"}),"."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"description"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsxs)(t.td,{children:["A description of the task. This is used for ",(0,s.jsx)(t.code,{children:"other"})," tasks."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"dueDate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The date the task is due."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"CropID"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The CropID associated with this Task. This is used for planting generated tasks."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cropName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The name of the crop associated with this Task. This is used for planting generated tasks."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"bedID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"BedID"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The BedID associated with this Task. This is used for planting generated tasks."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyID"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"VarietyID"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The VarietyID associated with this Task. This is used for planting generated tasks."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"varietyName"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String"})}),(0,s.jsx)(t.td,{children:"O"}),(0,s.jsx)(t.td,{children:"The name of the variety associated with this Task. This is used for planting generated tasks."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"lastUpdate"}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"DateTime"})}),(0,s.jsx)(t.td,{children:"R"}),(0,s.jsx)(t.td,{children:"The date the task was last updated."})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"Here's an example planting generated task."}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{\n "taskID": "task-001-101-1068-006",\n "chapterID": "chapter-001",\n "gardenID": "garden-001-101",\n "taskType": "transplant",\n "description": "",\n "dueDate": "2023-07-25T00:00:00.000",\n "cropID": "crop-001-516",\n "cropName": "Dill",\n "bedID": "bed-001-101-208",\n "varietyID": "variety-001-848",\n "varietyName": "Goldkrone",\n "lastUpdate": "2023-04-01T00:00:00.000Z"\n }\n'})}),"\n",(0,s.jsx)(t.p,{children:"and a gardener generated task."}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{\n "taskID": "task-001-101-0000-008",\n "chapterID": "chapter-001",\n "gardenID": "garden-001-101",\n "taskType": "other",\n "description": "Weed bed 1",\n "dueDate": "2023-07-25T00:00:00.000",\n "cropID": "",\n "cropName": "",\n "bedID": "",\n "varietyID": "",\n "varietyName": "",\n "lastUpdate": "2023-04-01T00:00:00.000Z"\n }\n'})}),"\n",(0,s.jsx)(t.h2,{id:"collections-and-business-logic",children:"Collections and business logic"}),"\n",(0,s.jsx)(t.p,{children:"As noted above, each entity is represented in the ggc_app as a Dart class, and made persistent as a document in Firebase."}),"\n",(0,s.jsxs)(t.p,{children:['Groups of entity instances of the same type are also represented in the ggc_app as a Dart class, and made persistent as a collection in Firebase. So, for example, in ggc_app, there is a Dart class called "Chapter" (to represent individual instances of that entity) and a Dart class called "ChapterCollection" (to manage a set of Chapter instances). On the Firebase side, there is a collection called Chapters, and each document in that collection has the same structure as the corresponding Dart class. We use ',(0,s.jsx)(t.a,{href:"https://pub.dev/packages/freezed",children:"freezed"})," to support the translation between the Dart class instance for an entity and its persistent representation as a Firebase document in JSON format."]}),"\n",(0,s.jsx)(t.p,{children:"That said, not all Collections in the ggc_app are created equally! Consider the following typical query:"}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsxs)(t.em,{children:['"What are the names of the crops that have been planted by ',(0,s.jsx)(t.a,{href:"mailto:johnson@hawaii.edu",children:"johnson@hawaii.edu"}),'?"']})}),"\n",(0,s.jsxs)(t.p,{children:["The answer to this query involves finding all the Gardens owned by ",(0,s.jsx)(t.a,{href:"mailto:johnson@hawaii.edu",children:"johnson@hawaii.edu"}),", then retrieving all of the Plantings associated with those Gardens, then building a set of Crop entities from those Plantings, then mapping over that set of Crop entities to build a list of crop names, then sorting that list of names into alphabetical order, and finally returning that list."]}),"\n",(0,s.jsx)(t.p,{children:"In this case, three different collections (Gardens, Plantings, and Crops) must be manipulated to satisfy the query. Other queries could require the manipulation of even more collections."}),"\n",(0,s.jsxs)(t.p,{children:['These kinds of queries represent the "business logic" of the application. In ggc_app, we want to follow the software engineering best practice of "separating business logic from user interface logic". To do that, ggc_app defines three "top-level" collections: UserCollection, GardenCollection, and ChapterCollection. Whenever possible, the UI can simply call a method on one of those top-level collections to obtain the data to present in the UI. So, if a UI component needs to present a list of crop names planted by a user, it can simply call ',(0,s.jsx)(t.code,{children:"users.getCrops(userID)"}),". The ",(0,s.jsx)(t.code,{children:"getCrops()"})," method takes care of accessing all of the additional collections to obtain the desired data."]}),"\n",(0,s.jsx)(t.p,{children:'To make this more concrete, here are a sampling of the methods associated with the ggc_app "top-level" collections.'}),"\n",(0,s.jsx)(t.h3,{id:"chaptercollection",children:"ChapterCollection"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Method signature"}),(0,s.jsx)(t.th,{children:"Return value"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List getChapterIDs()"})}),(0,s.jsx)(t.td,{children:"All chapter IDs"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List getAssociatedUserIDs(String chapterID)"})}),(0,s.jsx)(t.td,{children:"All users in this chapter"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List getChapterNames()"})}),(0,s.jsx)(t.td,{children:"Chapter names"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String getChapterIDFromName(String name)"})}),(0,s.jsx)(t.td,{children:"(Since chapter names are unique.)"})]})]})]}),"\n",(0,s.jsx)(t.h3,{id:"usercollection",children:"UserCollection"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Method signature"}),(0,s.jsx)(t.th,{children:"Return value"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"User getUser(String userID)"})}),(0,s.jsx)(t.td,{children:"Return the User entity"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"bool areUserNames(List userNames)"})}),(0,s.jsx)(t.td,{children:"Verify the list of usernames"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"int getNumNews(userID)"})}),(0,s.jsx)(t.td,{children:"Number of news items for this user."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List getAssociatedGardenNames(userID)"})}),(0,s.jsx)(t.td,{children:"Gardens associated with this user."})]})]})]}),"\n",(0,s.jsx)(t.h3,{id:"gardencollection",children:"GardenCollection"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Method signature"}),(0,s.jsx)(t.th,{children:"Return value"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List getGardens({String? userID, String? chapterID})"})}),(0,s.jsx)(t.td,{children:"The gardens associated with either the user or the chapter."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"String getOwnerUserID(String gardenID)"})}),(0,s.jsx)(t.td,{children:"The garden owner."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"List getEditorUserIDs(String gardenID)"})}),(0,s.jsx)(t.td,{children:"The garden editors."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"bool _userIsAssociated(String gardenID, String userID)"})}),(0,s.jsx)(t.td,{children:"Is this user an owner or editor of this garden"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"void setGarden(Garden garden)"})}),(0,s.jsx)(t.td,{children:"Update the Firebase document associated with this garden."})]})]})]}),"\n",(0,s.jsx)(t.p,{children:'When doing UI design, if you find yourself writing more than a couple of lines of code to produce the data for display, you should consider whether this code should be made "business logic" and provided as a method in either the ChapterCollection, GardenCollection, or UserCollection.'}),"\n",(0,s.jsx)(t.h2,{id:"other-data-model-issues",children:"Other Data Model issues"}),"\n",(0,s.jsx)(t.h3,{id:"privacy",children:"Privacy"}),"\n",(0,s.jsx)(t.p,{children:'Our goals for GGC create a very particular and constrained approach to "privacy".'}),"\n",(0,s.jsx)(t.p,{children:"On the one hand, we want to preserve certain types of privacy:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:'Users can pick a "username" which is used in postings so that they do not have to reveal their real name.'}),"\n",(0,s.jsx)(t.li,{children:"The system does not reveal the precise location of gardens."}),"\n",(0,s.jsx)(t.li,{children:'Users can tag an observation and/or photo as "private", and in that case it will not be visible to others.'}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"On the other hand, we want to facilitate the creation of a community of practice, which is accomplished by making many aspects of garden planning and management public to all members of a chapter."}),"\n",(0,s.jsx)(t.p,{children:"A significant goal for the 1.0 release is to test the hypothesis that it is not problematic for users to share this kind of information with others."}),"\n",(0,s.jsx)(t.p,{children:'A common approach to privacy is to make sharing "opt-in". In other words, your data is private unless you explicitly agree to share it. One concern with this approach is that if we allow some users to make their garden info private, it creates an "information asymmetry", where some users get to exploit the experiences of others while not offering up their experiences in return. That seems corrosive to the morale of the chapter and impedes the creation of a community of practice. It seems better to test the hypothesis that there is enough value from sharing to make it mandatory (outside the "privacy" mechanisms listed above.)'}),"\n",(0,s.jsx)(t.h3,{id:"ids",children:"IDs"}),"\n",(0,s.jsxs)(t.p,{children:['In NoSQL databases, each document is automatically provided upon creation with a unique string called a "docID" which looks something like this: ',(0,s.jsx)(t.code,{children:"tghHU4CVf"}),"."]}),"\n",(0,s.jsxs)(t.p,{children:['In the GGC data model, there is a docID field, but it is ignored by the application. Instead, the application relies on the fact that each document has a ID field whose name and values are based on the associated collection. So, the Gardener collection has a field named "gardenerID" and the value of that field will be a string with the prefix ',(0,s.jsx)(t.code,{children:"gardener-"})," and a suffix that consists of two numbers: the chapterID associated with the gardener and a number that uniquely identifies the gardener within the chapter. For example, ",(0,s.jsx)(t.code,{children:"gardener-001-301"}),"."]}),"\n",(0,s.jsx)(t.p,{children:'In my prior development experience, having a "user friendly" ID field for NoSQL documents improves the developer experience in two ways:'}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsxs)(t.li,{children:["It is a little easier to remember that ",(0,s.jsx)(t.code,{children:"garden-001-101"})," is Jenna's garden than ",(0,s.jsx)(t.code,{children:"tghHU4CVf"})," is Jenna's garden."]}),"\n",(0,s.jsx)(t.li,{children:'Certain bugs are easier to identity. For example, if a field named "gardenerIDs" contains the value "crop-001-503".'}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"One design problem with explicitly creating and managing ID fields in this way is ensuring that they are unique. While the database itself can trivially ensure that it always provides a unique randomly generated docID for each document, our design requires clients to create the gardenID, plantingID, etc. locally and hope that there is not already a document with this ID in the database."}),"\n",(0,s.jsxs)(t.p,{children:["With FireBase, it is possible to create a ",(0,s.jsx)(t.a,{href:"https://stackoverflow.com/questions/67111638/i-want-to-make-unique-usernames-in-firebase-firestore",children:"security rule to prevent documents with duplicate field values"}),". Thus, we can have clients create IDs, and in the event that there is a collision (i.e. a document with the same ID for that field already exists), then the client request will fail with an error."]}),"\n",(0,s.jsx)(t.p,{children:'We believe this "error due to pre-existing ID" situation to be a very unlikely scenario, because the GGC unique IDs are crafted to be as "local as possible". For example, when creating a Planting, the unique ID includes the chapter and garden IDs. This means that a collision is only possible if two gardeners try to create a Planting for the same Garden in the same Chapter at "almost" the exact same time. As a result of this design of GGC IDs, we expect collisions to rarely, if ever, occur in practice. If they do, then the above Firebase security rule will prevent the document creation request from succeeding. In the UI, we will catch this failure and ask the user to retry.'}),"\n",(0,s.jsx)(t.p,{children:"In addition, this 1.0 release data model implements a simple numbering convention to further improve the human readability of the unique IDs. The idea is to begin numbering entity documents of a given type at a different number. Here is the numbering system we are using:"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Starting Number"}),(0,s.jsx)(t.th,{children:"Entities"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"000"}),(0,s.jsx)(t.td,{children:"Chapter, Editor, Seed"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"100"}),(0,s.jsx)(t.td,{children:"Garden"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"200"}),(0,s.jsx)(t.td,{children:"Bed"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"400"}),(0,s.jsx)(t.td,{children:"Family"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"500"}),(0,s.jsx)(t.td,{children:"Crop"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"900"}),(0,s.jsx)(t.td,{children:"Variety"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"1000"}),(0,s.jsx)(t.td,{children:"Planting, Outcome"})]})]})]}),"\n",(0,s.jsxs)(t.p,{children:["So, for example, a plantingID looks like this: ",(0,s.jsx)(t.code,{children:"planting-001-101-1034"}),", and (if you recall the numbering convention), you can decode the ID as Chapter (0xx) followed by Garden (1xx) followed by the Planting (1xxx). Similarly, a bedID looks like ",(0,s.jsx)(t.code,{children:"bed-001-102-215"})," which is a Chapter (0xx) followed by a Garden (1xx) followed by a Bed (2xx)."]}),"\n",(0,s.jsx)(t.p,{children:'Note that there is no implementation problem with an Entity having so many documents that the IDs eventually cross over into the next category. For example, there is definitely the possibility of more than 100 Chapters, at which point there could be a chapterNum of 101, which would be the same as a gardenNum. That doesn\'t create any conflicts or problems internally in the system: unique IDs do not depend upon entities "staying in" their starting range.'}),"\n",(0,s.jsx)(t.p,{children:"The goal of this numbering convention is simply to make the 1.0 release database documents slightly easier to understand while the relative numbers of Chapters, Gardens, Crops, etc are low. Once we have hundreds of Chapters and tens of thousands of Gardeners, we will have outgrown the use of this simple partitioning to understand the data."}),"\n",(0,s.jsxs)(t.p,{children:["Note that ",(0,s.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore/best-practices#hotspots",children:"Firebase recommends against creating documentIDs with lexicographically close ranges"}),". However, this recommendation applies only to situations with ",(0,s.jsx)(t.strong,{children:"high"}),' levels of reads or writes. Even at scale, GGC will not be experience "high" levels of reads or writes (from a database point of view), so I am hopeful we can implement this numbering scheme, at least for the 1.0 Release. (If necessary, we could easily migrate to a randomized string for IDs in future if this actually becomes an database bottleneck.)']}),"\n",(0,s.jsx)(t.h3,{id:"normalization-and-caching",children:"Normalization and caching"}),"\n",(0,s.jsxs)(t.p,{children:['A best practice for relational database design is "',(0,s.jsx)(t.a,{href:"https://en.wikipedia.org/wiki/Database_normalization",children:"normalization"}),'", which means that a value should only occur in one place at a time. Normalization has a number of virtues, such as making updates and deletions more efficient and less error prone. But normalization has a substantial cost: queries can become very complicated, involving complex "joins" of data from a variety of tables.']}),"\n",(0,s.jsx)(t.p,{children:"The GGC app has the following design considerations that impact on the issue of normalization:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:'Updates and deletions are rare. GGC is mostly an "additive" database. While deletions and updates can occur, they are relatively rare and it\'s OK if they are "expensive" in time.'}),"\n",(0,s.jsx)(t.li,{children:"Reads are common, and we need local caches for certain kinds of Chapter data, and all of the user's Garden-related data."}),"\n",(0,s.jsx)(t.li,{children:"In general, Gardeners do not access data outside their Chapter."}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"As a result of these design considerations, GGC collections are designed to facilitate caching by including chapterID and gardenID fields whenever relevant."}),"\n",(0,s.jsx)(t.p,{children:'We also "denormalize" by occasionally providing "redundant" fields in a collection\'s documents. For example, in some cases a document will include a cropName field even though it already has a cropID field. We do this to simplify the developer experience: it simplifies construction of the UI by reducing the number of collection lookups, and it makes the contents of the database easier to understand and debug.'}),"\n",(0,s.jsx)(t.h3,{id:"local-first-caching-and-disconnected-operation",children:"Local-first, caching, and disconnected operation"}),"\n",(0,s.jsx)(t.p,{children:'We intend to have a "local-first" approach to data. In other words, there will be a local cache of relevant collections on each user\'s device, so that Firebase queries will be minimized. I am not sure yet whether the local cache will be automatically synced in the background with the global store, or whether we will need to implement a "pull down to refresh" mode. I think either approach would be OK for the purposes of the 1.0 release.'}),"\n",(0,s.jsx)(t.p,{children:"On the other hand, our approach to IDs makes completely disconnected operation problematic. Actually, it goes beyond that: imagine two gardeners working in the same garden in disconnected mode. The opportunities for problematic data entry are present even if we moved to more traditional forms of document IDs."}),"\n",(0,s.jsx)(t.p,{children:"For that reason, at least for the 1.0 release, we will require an internet connection in order to make the app fully functional. It should be straightforward to allow the app to work based on the locally cached data when disconnected, but when that occurs, provide some indication that certain operations will not be available until an internet connection is re-established."})]})}function o(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>d});var s=n(7294);const i={},r=s.createContext(i);function d(e){const t=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:d(e.components),s.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/ed0568ab.67eb3729.js b/assets/js/ed0568ab.67eb3729.js new file mode 100644 index 000000000..3e2fcf309 --- /dev/null +++ b/assets/js/ed0568ab.67eb3729.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[8392],{9987:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>o,contentTitle:()=>a,default:()=>u,frontMatter:()=>r,metadata:()=>l,toc:()=>s});var i=t(5893),d=t(1151);const r={hide_table_of_contents:!0},a="Input Fields",l={id:"develop/design/input-fields",title:"Input Fields",description:"Motivation",source:"@site/docs/develop/design/input-fields.md",sourceDirName:"develop/design",slug:"/develop/design/input-fields",permalink:"/docs/develop/design/input-fields",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!0},sidebar:"developSidebar",previous:{title:"Badges",permalink:"/docs/develop/design/badges"},next:{title:'"With" widgets',permalink:"/docs/develop/design/with-widgets"}},o={},s=[{value:"Motivation",id:"motivation",level:2},{value:"Background: Form Builder Input Fields",id:"background-form-builder-input-fields",level:2},{value:"Custom Field Example",id:"custom-field-example",level:2},{value:"GGC Input Fields",id:"ggc-input-fields",level:2},{value:"Predefined Field Keys",id:"predefined-field-keys",level:3},{value:"GGC Input fields in forms",id:"ggc-input-fields-in-forms",level:2},{value:"GGC Input field outside of forms",id:"ggc-input-field-outside-of-forms",level:2}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"input-fields",children:"Input Fields"})}),"\n",(0,i.jsx)(n.h2,{id:"motivation",children:"Motivation"}),"\n",(0,i.jsxs)(n.p,{children:["GGC uses the ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/flutter_form_builder",children:"Flutter Form Builder"})," package to support data collection from gardeners. Flutter Form Builder simplifies form-based data collection by reducing the code needed to: (a) build a form, (b) validate fields, (c) react to changes, and (d) collect final user input."]}),"\n",(0,i.jsx)(n.p,{children:"While this is great, Flutter Form Builder does not, by itself, accomplish two additional important design goals for GGC:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Provide specialized widgets for commonly used GGC data input fields. For example, a dropdown displaying the names of all gardens associated with this user; and"}),"\n",(0,i.jsx)(n.li,{children:"Provide a single location for specifying the look-and-feel for input fields. We want to minimize the amount of duplicated code (and hopefully eliminate look-and-feel code) when creating a form to collect data in a screen."}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:'There is a third design goal as well. GGC sometimes wants to use input fields outside the context of a "form"---i.e. a context in which data is gathered but not made available to the system until a "Submit" button is pressed. For example, the Outcome screen has input fields to select a garden, crop, and/or variety, and as these fields are manipulated by the user, the screen immediately refreshes to show Outcome data filtered by the values of the input fields. There is no "Submit" button in this screen, and so some of the Flutter Form Builder mechanisms are not used. The third design goal is:'}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Support both in-form and outside-form contexts without having to create two separate Garden dropdown widgets (for example)."}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["To support these three design goals, GGC provides a set of custom input fields (in the ",(0,i.jsx)(n.code,{children:"lib/features/common/input-fields directory"}),'). We call these "GGC Input Fields" to distinguish them from "Form Builder Input Fields".']}),"\n",(0,i.jsx)(n.p,{children:"The goal of this page is to document how GGC Input Fields are created and used in order to facilitate their future evolution."}),"\n",(0,i.jsx)(n.h2,{id:"background-form-builder-input-fields",children:"Background: Form Builder Input Fields"}),"\n",(0,i.jsxs)(n.p,{children:["A good overview of Form Builder Input Fields and their use is available in the ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/flutter_form_builder",children:"Flutter Form Builder Readme"}),". As noted in the ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/flutter_form_builder#parameters",children:"Parameters section"}),", there are several attributes that all Form Builder Input Fields support. In many cases, a GGC Input Field will provide a value for these standard attributes:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Form Builder Input Field Attribute"}),(0,i.jsx)(n.th,{children:"GGC Input Field Value"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"name"}),(0,i.jsx)(n.td,{children:'The input field name, i.e. "New Garden Name", "Garden Dropdown", etc.'})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"initialValue"}),(0,i.jsx)(n.td,{children:"Not typically needed."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"enabled"}),(0,i.jsx)(n.td,{children:"Same default (true)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"decoration"}),(0,i.jsx)(n.td,{children:"Provided: implements standard border, icons, and styles across all GGC input fields"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"validator"}),(0,i.jsx)(n.td,{children:'Provided as needed. For example, the "New Garden Name" input field will validate that the provided string does not match any other garden name (case-insensitive, spaces and special characters removed).'})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"onChanged"}),(0,i.jsx)(n.td,{children:"Made available in case input field is used outside of a form"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"valueTransformer"}),(0,i.jsx)(n.td,{children:"A function might be provided for some GGC Input Fields, not sure yet."})]})]})]}),"\n",(0,i.jsx)(n.p,{children:'Let\'s look at a simple Form using Form Builder, which displays two text fields ("Email" and "Password") and a "Login" button.'}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"final _formKey = GlobalKey();\n\nFormBuilder(\n key: _formKey,\n child: Column(\n children: [\n FormBuilderTextField(\n key: _emailFieldKey,\n name: 'email',\n decoration: const InputDecoration(labelText: 'Email'),\n validator: FormBuilderValidators.compose([\n FormBuilderValidators.required(),\n FormBuilderValidators.email(),\n ]),\n ),\n const SizedBox(height: 10),\n FormBuilderTextField(\n name: 'password',\n decoration: const InputDecoration(labelText: 'Password'),\n obscureText: true,\n validator: FormBuilderValidators.compose([\n FormBuilderValidators.required(),\n ]),\n ),\n MaterialButton(\n color: Theme.of(context).colorScheme.secondary,\n onPressed: () {\n if (_formKey.currentState?.saveAndValidate() ?? false) {\n debugPrint('validation succeeded');\n debugPrint(_formKey.currentState?.value.toString());\n } else {\n debugPrint('validation failed');\n }\n },\n child: const Text('Login'),\n )\n ],\n ),\n),\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Form Builder provides pre-defined input fields for the following types of input controllers: Checkbox, Radio Button, Date Picker, Dropdown, Slider, Toggle, and Text Field. In addition, the ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/form_builder_extra_fields",children:"Form Builder Extra Fields"})," package provides input controllers for: Color Picker, Rating, Searchable Dropdown, Signature Pad, Spinnable Number Selector, and Text Field with Auto-Complete."]}),"\n",(0,i.jsxs)(n.p,{children:["If you want to build a custom field, there is a set of ",(0,i.jsx)(n.a,{href:"https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/blob/main/example/lib/sources/custom_fields.dart",children:"Example Custom Fields"}),", as well as two how-to articles: ",(0,i.jsx)(n.a,{href:"https://medium.com/@danvickmiller/building-a-custom-flutter-form-builder-field-c67e2b2a27f4",children:"Building a Custom Field with FormBuilder Flutter Package"})," and ",(0,i.jsx)(n.a,{href:"https://medium.com/@danvickmiller/turn-any-flutter-widget-into-a-form-input-c23223042e3b",children:"Turn any widget into a Form Input"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"custom-field-example",children:"Custom Field Example"}),"\n",(0,i.jsx)(n.p,{children:"Here's a simple example of a custom field, built inline:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"FormBuilderField(\n name: 'name',\n builder: (FormFieldState field) {\n return Autocomplete(\n optionsBuilder: (TextEditingValue textEditingValue) {\n if (textEditingValue.text == '') {\n return const Iterable.empty();\n }\n return _kOptions.where((String option) {\n return option.contains(textEditingValue.text.toLowerCase());\n });\n },\n onSelected: (String selection) {\n field.didChange(selection);\n },\n );\n },\n autovalidateMode: AutovalidateMode.always,\n validator: (valueCandidate) {\n if (valueCandidate?.isEmpty ?? true) {\n return 'This field is required.';\n }\n return null;\n },\n),\n"})}),"\n",(0,i.jsx)(n.p,{children:"FormBuilderField has two required fields: name, and builder. There are many optional fields, including: onSaved, initialValue, autovalidateMode, decoration, enabled, validator, valueTransformer, onChanged, and onReset."}),"\n",(0,i.jsx)(n.p,{children:"The above example has the required fields plus two fields to implement validation. The field can return either a String or null."}),"\n",(0,i.jsx)(n.h2,{id:"ggc-input-fields",children:"GGC Input Fields"}),"\n",(0,i.jsx)(n.p,{children:"The above section provides a brief introduction to generic Form Builder and Input Fields. Here is how we are building GGC-specific abstractions to address the three design requirements."}),"\n",(0,i.jsx)(n.h3,{id:"predefined-field-keys",children:"Predefined Field Keys"}),"\n",(0,i.jsx)(n.p,{children:"One assumption we can make in GGC is that a given GGC form (i.e. GardenDropdown, CropDropdown, TitleField, etc) appears only once in any given form. That means we can reduce the amount of code required to build a form by predefining field keys. We do this in the FieldKey class, which contains a set of static fields that are initialized to a field key:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"/// The FieldKey associated with each GGC Input Field type.\n/// This assumes each GGC Input Field type can occur only once in a form.\nclass FieldKey {\n static GlobalKey, dynamic>>\n gardenDropdown = GlobalKey();\n static GlobalKey, dynamic>>\n gardenTextField = GlobalKey();\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"This means that when you build a form using the GGC Input Fields, you do not have to create or pass a field key to an input field, as the input field will use one of these values according to the input field type. Then, in the submit callback, you can retrieve the input field value this way:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"String gardenID = FieldKey.gardenDropdown.currentState?.value;\n"})}),"\n",(0,i.jsx)(n.h2,{id:"ggc-input-fields-in-forms",children:"GGC Input fields in forms"}),"\n",(0,i.jsx)(n.p,{children:"Using a GGC Input Field in a form is really easy. For example, here is how to add a required dropdown where the user must specify a Garden:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"GardenDropdown(gardens: widget.gardens, chapters: widget.chapters, required: true);\n"})}),"\n",(0,i.jsx)(n.p,{children:"As implied above, the value that you can retrieve from this dropdown in the submit callback is the gardenID. From this, you can easily get the garden name (or any other garden details). For example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"String gardenID = FieldKey.gardenDropdown.currentState?.value;\nString gardenName = widget.gardens.getGarden(gardenID).name;\n"})}),"\n",(0,i.jsx)(n.h2,{id:"ggc-input-field-outside-of-forms",children:"GGC Input field outside of forms"}),"\n",(0,i.jsx)(n.p,{children:"We can also use the GardenDropdown Input Field in a non-form context. For example, in the Outcomes screen accessible from the Drawer, there is a Garden dropdown such that the displayed outcomes update immediately each time a new garden is selected."}),"\n",(0,i.jsx)(n.p,{children:"To do this, the Outcomes screen must provide an onTap function which is called each time the dropdown is manipulated. Here's is how the GardenDropdown can be called to provide this functionality:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"GardenDropdown(\n gardens: widget.gardens,\n chapters: widget.chapters,\n gardenID: gardenID,\n initialValue: gardenID ?? 'All',\n addAll: true,\n enabled: widget.gardenID == null,\n onTap: (value) => setState(() {\n gardenID = (value == 'All') ? null : value;\n cropID = null;\n varietyID = null;\n }),\n);\n"})}),"\n",(0,i.jsx)(n.p,{children:"In this situation, we pass in an onTap method that calls setState() to update local state variables for gardenID, cropID, and varietyID. This forces a rebuild of the screen with those new state values, which in turn recomputes the outcomes to be displayed."}),"\n",(0,i.jsx)(n.p,{children:"This example illustrates how GardenDropdown achieves the three design goals:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"It is specialized for a given GGC entity. The client just passes in the gardens and chapters collection instances and GardenDropdown does the work of extracting garden names and IDs and building the dropdown object."}),"\n",(0,i.jsx)(n.li,{children:'The invocation of the GardenDropdown has no "look-and-feel" code associated with it. All of the decoration and theme data is internal.'}),"\n",(0,i.jsx)(n.li,{children:"The GardenDropdown can be used both within a form (where the data is extracted using a FormKey) or outside a form (where the data is extracted using an onTap callback)."}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"(More documentation to come)"})]})}function u(e={}){const{wrapper:n}={...(0,d.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>a});var i=t(7294);const d={},r=i.createContext(d);function a(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:a(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/ed0568ab.a5d4194d.js b/assets/js/ed0568ab.a5d4194d.js deleted file mode 100644 index 72ca06e95..000000000 --- a/assets/js/ed0568ab.a5d4194d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[8392],{9987:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>o,contentTitle:()=>a,default:()=>u,frontMatter:()=>r,metadata:()=>l,toc:()=>s});var i=t(5893),d=t(1151);const r={hide_table_of_contents:!0},a="GGC Input Fields",l={id:"develop/design/input-fields",title:"GGC Input Fields",description:"Motivation",source:"@site/docs/develop/design/input-fields.md",sourceDirName:"develop/design",slug:"/develop/design/input-fields",permalink:"/docs/develop/design/input-fields",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!0},sidebar:"developSidebar",previous:{title:"Badges",permalink:"/docs/develop/design/badges"},next:{title:'"With" widgets',permalink:"/docs/develop/design/with-widgets"}},o={},s=[{value:"Motivation",id:"motivation",level:2},{value:"Background: Form Builder Input Fields",id:"background-form-builder-input-fields",level:2},{value:"Custom Field Example",id:"custom-field-example",level:2},{value:"GGC Input Fields",id:"ggc-input-fields-1",level:2},{value:"Predefined Field Keys",id:"predefined-field-keys",level:3},{value:"GGC Input fields in forms",id:"ggc-input-fields-in-forms",level:2},{value:"GGC Input field outside of forms",id:"ggc-input-field-outside-of-forms",level:2}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"ggc-input-fields",children:"GGC Input Fields"})}),"\n",(0,i.jsx)(n.h2,{id:"motivation",children:"Motivation"}),"\n",(0,i.jsxs)(n.p,{children:["GGC uses the ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/flutter_form_builder",children:"Flutter Form Builder"})," package to support data collection from gardeners. Flutter Form Builder simplifies form-based data collection by reducing the code needed to: (a) build a form, (b) validate fields, (c) react to changes, and (d) collect final user input."]}),"\n",(0,i.jsx)(n.p,{children:"While this is great, Flutter Form Builder does not, by itself, accomplish two additional important design goals for GGC:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Provide specialized widgets for commonly used GGC data input fields. For example, a dropdown displaying the names of all gardens associated with this user; and"}),"\n",(0,i.jsx)(n.li,{children:"Provide a single location for specifying the look-and-feel for input fields. We want to minimize the amount of duplicated code (and hopefully eliminate look-and-feel code) when creating a form to collect data in a screen."}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:'There is a third design goal as well. GGC sometimes wants to use input fields outside the context of a "form"---i.e. a context in which data is gathered but not made available to the system until a "Submit" button is pressed. For example, the Outcome screen has input fields to select a garden, crop, and/or variety, and as these fields are manipulated by the user, the screen immediately refreshes to show Outcome data filtered by the values of the input fields. There is no "Submit" button in this screen, and so some of the Flutter Form Builder mechanisms are not used. The third design goal is:'}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Support both in-form and outside-form contexts without having to create two separate Garden dropdown widgets (for example)."}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["To support these three design goals, GGC provides a set of custom input fields (in the ",(0,i.jsx)(n.code,{children:"lib/features/common/input-fields directory"}),'). We call these "GGC Input Fields" to distinguish them from "Form Builder Input Fields".']}),"\n",(0,i.jsx)(n.p,{children:"The goal of this page is to document how GGC Input Fields are created and used in order to facilitate their future evolution."}),"\n",(0,i.jsx)(n.h2,{id:"background-form-builder-input-fields",children:"Background: Form Builder Input Fields"}),"\n",(0,i.jsxs)(n.p,{children:["A good overview of Form Builder Input Fields and their use is available in the ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/flutter_form_builder",children:"Flutter Form Builder Readme"}),". As noted in the ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/flutter_form_builder#parameters",children:"Parameters section"}),", there are several attributes that all Form Builder Input Fields support. In many cases, a GGC Input Field will provide a value for these standard attributes:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Form Builder Input Field Attribute"}),(0,i.jsx)(n.th,{children:"GGC Input Field Value"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"name"}),(0,i.jsx)(n.td,{children:'The input field name, i.e. "New Garden Name", "Garden Dropdown", etc.'})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"initialValue"}),(0,i.jsx)(n.td,{children:"Not typically needed."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"enabled"}),(0,i.jsx)(n.td,{children:"Same default (true)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"decoration"}),(0,i.jsx)(n.td,{children:"Provided: implements standard border, icons, and styles across all GGC input fields"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"validator"}),(0,i.jsx)(n.td,{children:'Provided as needed. For example, the "New Garden Name" input field will validate that the provided string does not match any other garden name (case-insensitive, spaces and special characters removed).'})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"onChanged"}),(0,i.jsx)(n.td,{children:"Made available in case input field is used outside of a form"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"valueTransformer"}),(0,i.jsx)(n.td,{children:"A function might be provided for some GGC Input Fields, not sure yet."})]})]})]}),"\n",(0,i.jsx)(n.p,{children:'Let\'s look at a simple Form using Form Builder, which displays two text fields ("Email" and "Password") and a "Login" button.'}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"final _formKey = GlobalKey();\n\nFormBuilder(\n key: _formKey,\n child: Column(\n children: [\n FormBuilderTextField(\n key: _emailFieldKey,\n name: 'email',\n decoration: const InputDecoration(labelText: 'Email'),\n validator: FormBuilderValidators.compose([\n FormBuilderValidators.required(),\n FormBuilderValidators.email(),\n ]),\n ),\n const SizedBox(height: 10),\n FormBuilderTextField(\n name: 'password',\n decoration: const InputDecoration(labelText: 'Password'),\n obscureText: true,\n validator: FormBuilderValidators.compose([\n FormBuilderValidators.required(),\n ]),\n ),\n MaterialButton(\n color: Theme.of(context).colorScheme.secondary,\n onPressed: () {\n if (_formKey.currentState?.saveAndValidate() ?? false) {\n debugPrint('validation succeeded');\n debugPrint(_formKey.currentState?.value.toString());\n } else {\n debugPrint('validation failed');\n }\n },\n child: const Text('Login'),\n )\n ],\n ),\n),\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Form Builder provides pre-defined input fields for the following types of input controllers: Checkbox, Radio Button, Date Picker, Dropdown, Slider, Toggle, and Text Field. In addition, the ",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/form_builder_extra_fields",children:"Form Builder Extra Fields"})," package provides input controllers for: Color Picker, Rating, Searchable Dropdown, Signature Pad, Spinnable Number Selector, and Text Field with Auto-Complete."]}),"\n",(0,i.jsxs)(n.p,{children:["If you want to build a custom field, there is a set of ",(0,i.jsx)(n.a,{href:"https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/blob/main/example/lib/sources/custom_fields.dart",children:"Example Custom Fields"}),", as well as two how-to articles: ",(0,i.jsx)(n.a,{href:"https://medium.com/@danvickmiller/building-a-custom-flutter-form-builder-field-c67e2b2a27f4",children:"Building a Custom Field with FormBuilder Flutter Package"})," and ",(0,i.jsx)(n.a,{href:"https://medium.com/@danvickmiller/turn-any-flutter-widget-into-a-form-input-c23223042e3b",children:"Turn any widget into a Form Input"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"custom-field-example",children:"Custom Field Example"}),"\n",(0,i.jsx)(n.p,{children:"Here's a simple example of a custom field, built inline:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"FormBuilderField(\n name: 'name',\n builder: (FormFieldState field) {\n return Autocomplete(\n optionsBuilder: (TextEditingValue textEditingValue) {\n if (textEditingValue.text == '') {\n return const Iterable.empty();\n }\n return _kOptions.where((String option) {\n return option.contains(textEditingValue.text.toLowerCase());\n });\n },\n onSelected: (String selection) {\n field.didChange(selection);\n },\n );\n },\n autovalidateMode: AutovalidateMode.always,\n validator: (valueCandidate) {\n if (valueCandidate?.isEmpty ?? true) {\n return 'This field is required.';\n }\n return null;\n },\n),\n"})}),"\n",(0,i.jsx)(n.p,{children:"FormBuilderField has two required fields: name, and builder. There are many optional fields, including: onSaved, initialValue, autovalidateMode, decoration, enabled, validator, valueTransformer, onChanged, and onReset."}),"\n",(0,i.jsx)(n.p,{children:"The above example has the required fields plus two fields to implement validation. The field can return either a String or null."}),"\n",(0,i.jsx)(n.h2,{id:"ggc-input-fields-1",children:"GGC Input Fields"}),"\n",(0,i.jsx)(n.p,{children:"The above section provides a brief introduction to generic Form Builder and Input Fields. Here is how we are building GGC-specific abstractions to address the three design requirements."}),"\n",(0,i.jsx)(n.h3,{id:"predefined-field-keys",children:"Predefined Field Keys"}),"\n",(0,i.jsx)(n.p,{children:"One assumption we can make in GGC is that a given GGC form (i.e. GardenDropdown, CropDropdown, TitleField, etc) appears only once in any given form. That means we can reduce the amount of code required to build a form by predefining field keys. We do this in the FieldKey class, which contains a set of static fields that are initialized to a field key:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"/// The FieldKey associated with each GGC Input Field type.\n/// This assumes each GGC Input Field type can occur only once in a form.\nclass FieldKey {\n static GlobalKey, dynamic>>\n gardenDropdown = GlobalKey();\n static GlobalKey, dynamic>>\n gardenTextField = GlobalKey();\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"This means that when you build a form using the GGC Input Fields, you do not have to create or pass a field key to an input field, as the input field will use one of these values according to the input field type. Then, in the submit callback, you can retrieve the input field value this way:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"String gardenID = FieldKey.gardenDropdown.currentState?.value;\n"})}),"\n",(0,i.jsx)(n.h2,{id:"ggc-input-fields-in-forms",children:"GGC Input fields in forms"}),"\n",(0,i.jsx)(n.p,{children:"Using a GGC Input Field in a form is really easy. For example, here is how to add a required dropdown where the user must specify a Garden:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"GardenDropdown(gardens: widget.gardens, chapters: widget.chapters, required: true);\n"})}),"\n",(0,i.jsx)(n.p,{children:"As implied above, the value that you can retrieve from this dropdown in the submit callback is the gardenID. From this, you can easily get the garden name (or any other garden details). For example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"String gardenID = FieldKey.gardenDropdown.currentState?.value;\nString gardenName = widget.gardens.getGarden(gardenID).name;\n"})}),"\n",(0,i.jsx)(n.h2,{id:"ggc-input-field-outside-of-forms",children:"GGC Input field outside of forms"}),"\n",(0,i.jsx)(n.p,{children:"We can also use the GardenDropdown Input Field in a non-form context. For example, in the Outcomes screen accessible from the Drawer, there is a Garden dropdown such that the displayed outcomes update immediately each time a new garden is selected."}),"\n",(0,i.jsx)(n.p,{children:"To do this, the Outcomes screen must provide an onTap function which is called each time the dropdown is manipulated. Here's is how the GardenDropdown can be called to provide this functionality:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-dart",children:"GardenDropdown(\n gardens: widget.gardens,\n chapters: widget.chapters,\n gardenID: gardenID,\n initialValue: gardenID ?? 'All',\n addAll: true,\n enabled: widget.gardenID == null,\n onTap: (value) => setState(() {\n gardenID = (value == 'All') ? null : value;\n cropID = null;\n varietyID = null;\n }),\n);\n"})}),"\n",(0,i.jsx)(n.p,{children:"In this situation, we pass in an onTap method that calls setState() to update local state variables for gardenID, cropID, and varietyID. This forces a rebuild of the screen with those new state values, which in turn recomputes the outcomes to be displayed."}),"\n",(0,i.jsx)(n.p,{children:"This example illustrates how GardenDropdown achieves the three design goals:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"It is specialized for a given GGC entity. The client just passes in the gardens and chapters collection instances and GardenDropdown does the work of extracting garden names and IDs and building the dropdown object."}),"\n",(0,i.jsx)(n.li,{children:'The invocation of the GardenDropdown has no "look-and-feel" code associated with it. All of the decoration and theme data is internal.'}),"\n",(0,i.jsx)(n.li,{children:"The GardenDropdown can be used both within a form (where the data is extracted using a FormKey) or outside a form (where the data is extracted using an onTap callback)."}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"(More documentation to come)"})]})}function u(e={}){const{wrapper:n}={...(0,d.a)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>a});var i=t(7294);const d={},r=i.createContext(d);function a(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:a(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/f3759001.73ba512d.js b/assets/js/f3759001.73ba512d.js deleted file mode 100644 index 5cfb6331b..000000000 --- a/assets/js/f3759001.73ba512d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[7346],{1714:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>n,metadata:()=>o,toc:()=>l});var r=a(5893),i=a(1151);const n={hide_table_of_contents:!1},s="Architecture",o={id:"develop/architecture",title:"Architecture",description:'The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the "Riverpod Architecture". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the architecture module in my mobile application development course.',source:"@site/docs/develop/architecture.md",sourceDirName:"develop",slug:"/develop/architecture",permalink:"/docs/develop/architecture",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Coding Standards",permalink:"/docs/develop/coding-standards"},next:{title:"Managing Firebase data",permalink:"/docs/develop/managing-firebase-data"}},c={},l=[{value:"Client-server architecture perspective",id:"client-server-architecture-perspective",level:2},{value:"Layered application architecture perspective",id:"layered-application-architecture-perspective",level:2},{value:"Directory structure perspective",id:"directory-structure-perspective",level:2},{value:"Data flow perspective",id:"data-flow-perspective",level:2}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",p:"p",pre:"pre",strong:"strong",...(0,i.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.header,{children:(0,r.jsx)(t.h1,{id:"architecture",children:"Architecture"})}),"\n",(0,r.jsxs)(t.p,{children:['The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the "Riverpod Architecture". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the ',(0,r.jsx)(t.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/architecture/",children:"architecture module"})," in my mobile application development course."]}),"\n",(0,r.jsx)(t.h2,{id:"client-server-architecture-perspective",children:"Client-server architecture perspective"}),"\n",(0,r.jsxs)(t.p,{children:["To begin, GGC can be viewed as a simple client-server application: there is a central back-end server (in our case, ",(0,r.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore",children:"Firestore"}),") that communicates with front-end clients (in our case, the Flutter ggc_app application):"]}),"\n",(0,r.jsx)("img",{src:"/img/develop/release-1.0/ggc-architecture.png"}),"\n",(0,r.jsx)(t.h2,{id:"layered-application-architecture-perspective",children:"Layered application architecture perspective"}),"\n",(0,r.jsx)(t.p,{children:"In the above diagram, the ggc_app is represented as four layers. This layering is strict, in that each layer communicates only with the layer above and below it. Let's introduce each layer, moving from bottom to top:"}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Repository Layer"}),". This bottom-most layer implements generic code for communication with Firebase: querying collections for documents; adding, deleting, and modifying documents, and so forth. In this layer, data is represented in JSON format (for entities) or binary format (for images)."]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Data Layer"}),'. The data layer implements "feature-specific" communication with Firebase. For example, the Data Layer code for the Chapter feature implements classes that queries the Chapter collection in appropriate ways. In this layer, data is still represented as JSON or binary.']}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Domain Layer"}),'. The domain layer implements code to translate between the data representations used at the data layer (i.e. JSON and Binary) and the data representations used at the Presentation Layer (i.e. Dart classes for entities and collections). The domain layer also implements the "business logic" of the application as discussed in the ',(0,r.jsx)(t.a,{href:"/docs/develop/design/data-model#collections-and-business-logic",children:"Collections and business logic"})," section."]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Presentation Layer"}),'. The presentation layer implements the Flutter-based user interface. All the classes at the presentation layer are Widgets. GGC implements many kinds of UI objects; for example, "Screens" and "Views". Screens implement a "top-level" page: they return a Scaffold Widget and they can be routed to. Views are "components": they are the building blocks for Screens and can potentially appear in multiple Screens.']}),"\n",(0,r.jsx)(t.h2,{id:"directory-structure-perspective",children:"Directory structure perspective"}),"\n",(0,r.jsx)(t.p,{children:"There is a relatively straightforward correspondence between the above layers and the directory structure in the ggc_app repository. The top-level of the repo is more or less like any Flutter app. Here is a semi-annotated version of some of the top-level files and directories to give you an idea of the organization:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"ggc_app/\n .github/ # GitHub Actions\n android/ # (Managed by Flutter)\n assets/ # Static data resources\n ios/ # (Managed by Flutter)\n integration_test # Test code \n lib/ # Source code \n linux/ # (Managed by Flutter)\n macos/ # (Managed by Flutter)\n stories/ # Monarch code\n web/ # (Managed by Flutter)\n windows/ # (Managed by Flutter)\n analysis_options.yaml\n pubspec.yaml\n run_build_runner.sh # Example of a GGC script \n"})}),"\n",(0,r.jsx)(t.p,{children:"The lib/ directory is where most of the action is. Here's a semi-annotated perspective of some the top-level of the lib/ directory:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:'lib/\n features/ # Feature-based organization for Data, Domain, and Presentation layers\n repositories/ # Implements the "Repository" layer\n theme/ # UI Theme (fonts, colors, etc.\n main.dart # Main entry point\n main_test_fixture.dart # An entry point that uses the test fixture data.\n router.dart # Implements routes using go_router \n'})}),"\n",(0,r.jsx)(t.p,{children:"Finally, here's a look inside the features/ directory:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"features/\n admin/\n authentication/ # Authentication using firebase_ui_auth.\n /presentation # Implementation only requires UI widgets.\n common/ # Cross-cutting code\n chapter/ # Implementation of Chapter feature\n data/ # Firebase interface\n domain/ # Chapter, ChapterCollection, etc.\n presentation/ # ChapterIndexScreen, ChapterView, etc. \n crop/\n garden/\n gardener/\n home/\n observation/\n :\n :\n"})}),"\n",(0,r.jsx)(t.p,{children:"Each feature can have one or more of the following subdirectories: domain/, data/, and presentation/. The authentication feature only requires a presentation/ subdirectory, while the chapter feature requires all three."}),"\n",(0,r.jsx)(t.h2,{id:"data-flow-perspective",children:"Data flow perspective"}),"\n",(0,r.jsx)(t.p,{children:"A final architectural perspective illustrates the way data flows between external sources (i.e. Firestore) and the app. Here is a diagram that illustrates some of the key components:"}),"\n",(0,r.jsx)("img",{src:"/img/develop/release-1.0/ggc-dataflow-diagram.png"}),"\n",(0,r.jsx)(t.p,{children:'Note that at the Firestore level, data is stored in a set of "flat" collections corresponding in many cases to "features" (i.e. Beds, Chapters, Crops, etc.). For the most part, the data model is similar to the table structure favored by SQL databases, even though Firestore is a document-oriented (NoSQL) database.'}),"\n",(0,r.jsx)(t.p,{children:'This diagram also illustrates the way GGC addresses the issue of communication with external services, which is intrinsically asynchronous. In asynchronous communications, there will be an unpredictable delay between the "request" (i.e. to retrieve or to store data) and the "response" (i.e. the request succeeded or failed). During this time, the UI should show some sort of "loading" indicator rather than freezing, or potentially just as bad, allowing the user to interact further with the UI. Finally, if the request fails (network or service is down), the UI should indicate the failure.'}),"\n",(0,r.jsx)(t.p,{children:'Managing asynchronous communication in code is complex. In GGC, we have encapsulated the complexities of asynchronous communication into two mechanisms: the "With" widgets (for retrieval of data asynchronously), and the MutateController (for persisting data asynchronously). What this provides is the ability to write app code with clearly defined boundaries between asynchronous and synchronous actions, which simplifies the code and reduces the chances for bugs in data storage and retrieval.'}),"\n",(0,r.jsxs)(t.p,{children:['The "With" widgets (such as WithCoreData, WithObservationData, WithUserGardenData, WithAllData) are responsible for setting up Riverpod providers that watch the various Firestore collections in various ways, and the populating an instance of each of three classes (ChapterCollection, GardenCollection, and UserCollection) with the data retrieved. Each With widget is responsible for displaying a "loading" icon while it waits for the required data from Firestore to arrive (or display an error if one occurs). Once the data arrives, it invokes a callback function supplied to it, passing the populated ChapterCollection, GardenCollection, and UserCollection instances. As a bonus, if any of the Firestore collections are updated by some other client, the use of Riverpod means that the widget will be automatically rebuilt, refreshing the UI with the changed data automatically. Consult the ',(0,r.jsx)(t.a,{href:"/docs/develop/design/with-widgets",children:'"With" Widgets Design Pattern'})," documentation for more details."]}),"\n",(0,r.jsx)(t.p,{children:"While the With widgets address the issue of asynchronous data retrieval, we must also address the issue of asynchronous data persistence. It's the same problem, in reverse: when the app wants to persist data back to Firestore, the storage request will take an unpredictable amount of time, we'll want the UI to display a loading indicator during that time, and if an unforeseen error occurs, we'll want to show that to the user."}),"\n",(0,r.jsxs)(t.p,{children:["GGC addresses the persistence problem with a single class called MutateController. Whenever UI code wants to update the database, it invokes this controller which takes a large number of optional arguments to support any combination of updates to the underlying entities. Consult the ",(0,r.jsx)(t.a,{href:"/docs/develop/design/data-mutation",children:"Data Mutation Design Pattern"})," documentation for more details."]})]})}function h(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},1151:(e,t,a)=>{a.d(t,{Z:()=>o,a:()=>s});var r=a(7294);const i={},n=r.createContext(i);function s(e){const t=r.useContext(n);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),r.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/f3759001.fcc2bb17.js b/assets/js/f3759001.fcc2bb17.js new file mode 100644 index 000000000..3b3e77aa2 --- /dev/null +++ b/assets/js/f3759001.fcc2bb17.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[7346],{1714:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>n,metadata:()=>o,toc:()=>l});var r=a(5893),i=a(1151);const n={hide_table_of_contents:!1},s="Architecture",o={id:"develop/architecture",title:"Architecture",description:'The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the "Riverpod Architecture". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the architecture module in my mobile application development course.',source:"@site/docs/develop/architecture.md",sourceDirName:"develop",slug:"/develop/architecture",permalink:"/docs/develop/architecture",draft:!1,unlisted:!1,tags:[],version:"current",frontMatter:{hide_table_of_contents:!1},sidebar:"developSidebar",previous:{title:"Coding Standards",permalink:"/docs/develop/coding-standards"},next:{title:"Data Model",permalink:"/docs/develop/data-model"}},c={},l=[{value:"Client-server architecture perspective",id:"client-server-architecture-perspective",level:2},{value:"Layered application architecture perspective",id:"layered-application-architecture-perspective",level:2},{value:"Directory structure perspective",id:"directory-structure-perspective",level:2},{value:"Data flow perspective",id:"data-flow-perspective",level:2}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",p:"p",pre:"pre",strong:"strong",...(0,i.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.header,{children:(0,r.jsx)(t.h1,{id:"architecture",children:"Architecture"})}),"\n",(0,r.jsxs)(t.p,{children:['The GeoGardenClub app (GGC) conforms (most of the time) to the architectural approach advocated by Andreas Bizzotto which he calls the "Riverpod Architecture". If you are not familiar with this approach, it\'s worth spending a few minutes reading through his description, which is available as a set of readings in the ',(0,r.jsx)(t.a,{href:"https://courses.ics.hawaii.edu/mobile-application-development/modules/architecture/",children:"architecture module"})," in my mobile application development course."]}),"\n",(0,r.jsx)(t.h2,{id:"client-server-architecture-perspective",children:"Client-server architecture perspective"}),"\n",(0,r.jsxs)(t.p,{children:["To begin, GGC can be viewed as a simple client-server application: there is a central back-end server (in our case, ",(0,r.jsx)(t.a,{href:"https://firebase.google.com/docs/firestore",children:"Firestore"}),") that communicates with front-end clients (in our case, the Flutter ggc_app application):"]}),"\n",(0,r.jsx)("img",{src:"/img/develop/release-1.0/ggc-architecture.png"}),"\n",(0,r.jsx)(t.h2,{id:"layered-application-architecture-perspective",children:"Layered application architecture perspective"}),"\n",(0,r.jsx)(t.p,{children:"In the above diagram, the ggc_app is represented as four layers. This layering is strict, in that each layer communicates only with the layer above and below it. Let's introduce each layer, moving from bottom to top:"}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Repository Layer"}),". This bottom-most layer implements generic code for communication with Firebase: querying collections for documents; adding, deleting, and modifying documents, and so forth. In this layer, data is represented in JSON format (for entities) or binary format (for images)."]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Data Layer"}),'. The data layer implements "feature-specific" communication with Firebase. For example, the Data Layer code for the Chapter feature implements classes that queries the Chapter collection in appropriate ways. In this layer, data is still represented as JSON or binary.']}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Domain Layer"}),'. The domain layer implements code to translate between the data representations used at the data layer (i.e. JSON and Binary) and the data representations used at the Presentation Layer (i.e. Dart classes for entities and collections). The domain layer also implements the "business logic" of the application as discussed in the ',(0,r.jsx)(t.a,{href:"/docs/develop/data-model#collections-and-business-logic",children:"Collections and business logic"})," section."]}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.strong,{children:"Presentation Layer"}),'. The presentation layer implements the Flutter-based user interface. All the classes at the presentation layer are Widgets. GGC implements many kinds of UI objects; for example, "Screens" and "Views". Screens implement a "top-level" page: they return a Scaffold Widget and they can be routed to. Views are "components": they are the building blocks for Screens and can potentially appear in multiple Screens.']}),"\n",(0,r.jsx)(t.h2,{id:"directory-structure-perspective",children:"Directory structure perspective"}),"\n",(0,r.jsx)(t.p,{children:"There is a relatively straightforward correspondence between the above layers and the directory structure in the ggc_app repository. The top-level of the repo is more or less like any Flutter app. Here is a semi-annotated version of some of the top-level files and directories to give you an idea of the organization:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"ggc_app/\n .github/ # GitHub Actions\n android/ # (Managed by Flutter)\n assets/ # Static data resources\n ios/ # (Managed by Flutter)\n integration_test # Test code \n lib/ # Source code \n linux/ # (Managed by Flutter)\n macos/ # (Managed by Flutter)\n stories/ # Monarch code\n web/ # (Managed by Flutter)\n windows/ # (Managed by Flutter)\n analysis_options.yaml\n pubspec.yaml\n run_build_runner.sh # Example of a GGC script \n"})}),"\n",(0,r.jsx)(t.p,{children:"The lib/ directory is where most of the action is. Here's a semi-annotated perspective of some the top-level of the lib/ directory:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:'lib/\n features/ # Feature-based organization for Data, Domain, and Presentation layers\n repositories/ # Implements the "Repository" layer\n theme/ # UI Theme (fonts, colors, etc.\n main.dart # Main entry point\n main_test_fixture.dart # An entry point that uses the test fixture data.\n router.dart # Implements routes using go_router \n'})}),"\n",(0,r.jsx)(t.p,{children:"Finally, here's a look inside the features/ directory:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"features/\n admin/\n authentication/ # Authentication using firebase_ui_auth.\n /presentation # Implementation only requires UI widgets.\n common/ # Cross-cutting code\n chapter/ # Implementation of Chapter feature\n data/ # Firebase interface\n domain/ # Chapter, ChapterCollection, etc.\n presentation/ # ChapterIndexScreen, ChapterView, etc. \n crop/\n garden/\n gardener/\n home/\n observation/\n :\n :\n"})}),"\n",(0,r.jsx)(t.p,{children:"Each feature can have one or more of the following subdirectories: domain/, data/, and presentation/. The authentication feature only requires a presentation/ subdirectory, while the chapter feature requires all three."}),"\n",(0,r.jsx)(t.h2,{id:"data-flow-perspective",children:"Data flow perspective"}),"\n",(0,r.jsx)(t.p,{children:"A final architectural perspective illustrates the way data flows between external sources (i.e. Firestore) and the app. Here is a diagram that illustrates some of the key components:"}),"\n",(0,r.jsx)("img",{src:"/img/develop/release-1.0/ggc-dataflow-diagram.png"}),"\n",(0,r.jsx)(t.p,{children:'Note that at the Firestore level, data is stored in a set of "flat" collections corresponding in many cases to "features" (i.e. Beds, Chapters, Crops, etc.). For the most part, the data model is similar to the table structure favored by SQL databases, even though Firestore is a document-oriented (NoSQL) database.'}),"\n",(0,r.jsx)(t.p,{children:'This diagram also illustrates the way GGC addresses the issue of communication with external services, which is intrinsically asynchronous. In asynchronous communications, there will be an unpredictable delay between the "request" (i.e. to retrieve or to store data) and the "response" (i.e. the request succeeded or failed). During this time, the UI should show some sort of "loading" indicator rather than freezing, or potentially just as bad, allowing the user to interact further with the UI. Finally, if the request fails (network or service is down), the UI should indicate the failure.'}),"\n",(0,r.jsx)(t.p,{children:'Managing asynchronous communication in code is complex. In GGC, we have encapsulated the complexities of asynchronous communication into two mechanisms: the "With" widgets (for retrieval of data asynchronously), and the MutateController (for persisting data asynchronously). What this provides is the ability to write app code with clearly defined boundaries between asynchronous and synchronous actions, which simplifies the code and reduces the chances for bugs in data storage and retrieval.'}),"\n",(0,r.jsxs)(t.p,{children:['The "With" widgets (such as WithCoreData, WithObservationData, WithUserGardenData, WithAllData) are responsible for setting up Riverpod providers that watch the various Firestore collections in various ways, and the populating an instance of each of three classes (ChapterCollection, GardenCollection, and UserCollection) with the data retrieved. Each With widget is responsible for displaying a "loading" icon while it waits for the required data from Firestore to arrive (or display an error if one occurs). Once the data arrives, it invokes a callback function supplied to it, passing the populated ChapterCollection, GardenCollection, and UserCollection instances. As a bonus, if any of the Firestore collections are updated by some other client, the use of Riverpod means that the widget will be automatically rebuilt, refreshing the UI with the changed data automatically. Consult the ',(0,r.jsx)(t.a,{href:"/docs/develop/design/with-widgets",children:'"With" Widgets Design Pattern'})," documentation for more details."]}),"\n",(0,r.jsx)(t.p,{children:"While the With widgets address the issue of asynchronous data retrieval, we must also address the issue of asynchronous data persistence. It's the same problem, in reverse: when the app wants to persist data back to Firestore, the storage request will take an unpredictable amount of time, we'll want the UI to display a loading indicator during that time, and if an unforeseen error occurs, we'll want to show that to the user."}),"\n",(0,r.jsxs)(t.p,{children:["GGC addresses the persistence problem with a single class called MutateController. Whenever UI code wants to update the database, it invokes this controller which takes a large number of optional arguments to support any combination of updates to the underlying entities. Consult the ",(0,r.jsx)(t.a,{href:"/docs/develop/design/data-mutation",children:"Data Mutation Design Pattern"})," documentation for more details."]})]})}function h(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},1151:(e,t,a)=>{a.d(t,{Z:()=>o,a:()=>s});var r=a(7294);const i={},n=r.createContext(i);function s(e){const t=r.useContext(n);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),r.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/main.2143a0af.js b/assets/js/main.2143a0af.js deleted file mode 100644 index 72b521420..000000000 --- a/assets/js/main.2143a0af.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! For license information please see main.2143a0af.js.LICENSE.txt */ -(self.webpackChunkgeogardenclub_github_io=self.webpackChunkgeogardenclub_github_io||[]).push([[179],{723:(e,t,n)=>{"use strict";n.d(t,{Z:()=>p});n(7294);var r=n(8356),o=n.n(r),a=n(6887);const i={"0058b4c6":[()=>n.e(4088).then(n.t.bind(n,6462,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-175.json",6462],"0bd3a280":[()=>n.e(7222).then(n.bind(n,3581)),"@site/docs/home/food-security.md",3581],"0f1af657":[()=>n.e(7540).then(n.bind(n,9631)),"@site/docs/user-guide/badges.md",9631],"10921c5b":[()=>n.e(1744).then(n.bind(n,4437)),"@site/docs/develop/design/with-widgets.md",4437],"11f6a8a1":[()=>n.e(9866).then(n.bind(n,883)),"@site/blog/2023-02-10-welcome.md?truncated=true",883],"1506d638":[()=>n.e(1549).then(n.bind(n,8420)),"@site/docs/home/sneak-peek.md",8420],17896441:[()=>Promise.all([n.e(532),n.e(2700),n.e(7918)]).then(n.bind(n,9152)),"@theme/DocItem",9152],"17d8eee1":[()=>Promise.all([n.e(5655),n.e(3560)]).then(n.bind(n,3722)),"@site/docs/home/welcome.md",3722],"1863cff0":[()=>n.e(9929).then(n.bind(n,1965)),"@site/docs/user-guide/guided-tour.md",1965],"18fc9463":[()=>n.e(9601).then(n.bind(n,1928)),"@site/docs/user-guide/adding-plantings.md",1928],"1e5c498d":[()=>n.e(1217).then(n.bind(n,798)),"@site/docs/develop/testing.md",798],"1f391b9e":[()=>Promise.all([n.e(532),n.e(2700),n.e(3085)]).then(n.bind(n,4247)),"@theme/MDXPage",4247],"2450005c":[()=>n.e(1585).then(n.bind(n,6651)),"@site/docs/user-guide/overview.md",6651],"2cd8ab24":[()=>n.e(860).then(n.bind(n,7451)),"@site/docs/develop/releases/release-0.0/entrepreneur-feedback.md",7451],"2f9db241":[()=>n.e(6800).then(n.bind(n,8074)),"@site/docs/user-guide/observations.md",8074],"31caa863":[()=>n.e(4063).then(n.bind(n,2592)),"@site/docs/develop/deployment.md",2592],"32cdd552":[()=>n.e(6900).then(n.bind(n,8608)),"@site/docs/develop/managing-firebase-data.md",8608],"3463d78f":[()=>n.e(8294).then(n.bind(n,8067)),"@site/docs/user-guide/adding-vendors-crops-varieties.md",8067],"36994c47":[()=>n.e(9208).then(n.t.bind(n,4468,19)),"@generated/docusaurus-plugin-content-blog/default/__plugin.json",4468],"38346c4b":[()=>n.e(5014).then(n.bind(n,1050)),"@site/docs/user-guide/explore-a-garden.md",1050],"393be207":[()=>n.e(7414).then(n.bind(n,9925)),"@site/src/pages/markdown-page.md",9925],"39838d4e":[()=>n.e(234).then(n.bind(n,8901)),"@site/docs/develop/onboarding.md",8901],"3ad77611":[()=>n.e(403).then(n.bind(n,9657)),"@site/docs/develop/design/badges.md",9657],"3b4579e8":[()=>n.e(9586).then(n.bind(n,3012)),"@site/docs/user-guide/scenarios.md",3012],"3bea7cd1":[()=>n.e(9256).then(n.bind(n,9340)),"@site/docs/business/index.md",9340],"3d832522":[()=>n.e(6414).then(n.bind(n,9797)),"@site/docs/business/roadmap.md",9797],"3e240fbf":[()=>n.e(5857).then(n.bind(n,7541)),"@site/docs/develop/releases/release-0.0/customer-feedback.md",7541],"49882d99":[()=>n.e(1937).then(n.bind(n,6949)),"@site/docs/user-guide/chat-rooms.md",6949],"505d7517":[()=>n.e(196).then(n.bind(n,4230)),"@site/docs/develop/releases/release-1.0/goals.md",4230],"59628a4d":[()=>n.e(6427).then(n.bind(n,6394)),"@site/docs/user-guide/seeds.md",6394],"5e95c892":[()=>n.e(9661).then(n.bind(n,1892)),"@theme/DocsRoot",1892],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,6809)),"@generated/docusaurus.config",6809],"6741c1a9":[()=>n.e(1420).then(n.bind(n,374)),"@site/blog/2023-02-10-welcome.md",374],"6b1fc3de":[()=>n.e(8754).then(n.bind(n,8312)),"@site/docs/develop/installation.md",8312],"772c3429":[()=>n.e(3844).then(n.bind(n,5537)),"@site/docs/user-guide/outcomes.md",5537],"7be5f79d":[()=>n.e(825).then(n.bind(n,6544)),"@site/docs/user-guide/registration.md",6544],"7d1225b6":[()=>n.e(4076).then(n.bind(n,7576)),"@site/docs/develop/backups.md",7576],"7d56ced7":[()=>n.e(9572).then(n.bind(n,616)),"@site/docs/develop/scripts.md",616],"814f3328":[()=>n.e(2535).then(n.t.bind(n,5641,19)),"~blog/default/blog-post-list-prop-default.json",5641],"906ac375":[()=>Promise.all([n.e(5655),n.e(6265)]).then(n.bind(n,8586)),"@site/docs/home/related-work.md",8586],"9140a56a":[()=>n.e(9095).then(n.bind(n,8002)),"@site/docs/develop/dart-analyze.md",8002],"95f4d37c":[()=>n.e(814).then(n.bind(n,4653)),"@site/docs/develop/index.md",4653],"96083bb9":[()=>n.e(183).then(n.bind(n,3473)),"@site/docs/business/market-size.md",3473],"968b4846":[()=>n.e(2743).then(n.bind(n,6729)),"@site/docs/develop/releases/release-1.0/onboarding-feedback.md",6729],"9e4087bc":[()=>n.e(3608).then(n.bind(n,3169)),"@theme/BlogArchivePage",3169],"9ebba4ea":[()=>n.e(6906).then(n.bind(n,1393)),"@site/docs/user-guide/terms-and-conditions.md",1393],a03c61f1:[()=>n.e(9533).then(n.bind(n,551)),"@site/docs/develop/integrity-check.md",551],a1e7621f:[()=>n.e(2889).then(n.bind(n,7920)),"@site/docs/develop/design/data-model.md",7920],a5b8d3e9:[()=>n.e(6957).then(n.bind(n,6058)),"@site/docs/develop/design/data-mutation.md",6058],a6327429:[()=>n.e(200).then(n.bind(n,8082)),"@site/docs/develop/design/features.md",8082],a6aa9e1f:[()=>Promise.all([n.e(532),n.e(2700),n.e(8041),n.e(3089)]).then(n.bind(n,2727)),"@theme/BlogListPage",2727],a7456010:[()=>n.e(5980).then(n.t.bind(n,9365,19)),"@generated/docusaurus-plugin-content-pages/default/__plugin.json",9365],a7bd4aaa:[()=>n.e(8518).then(n.bind(n,4974)),"@theme/DocVersionRoot",4974],a8521eb9:[()=>n.e(186).then(n.bind(n,2522)),"@site/docs/develop/releases/release-1.0/end-of-season-feedback.md",2522],a94703ab:[()=>Promise.all([n.e(532),n.e(4368)]).then(n.bind(n,4547)),"@theme/DocRoot",4547],aba21aa0:[()=>n.e(3629).then(n.t.bind(n,1765,19)),"@generated/docusaurus-plugin-content-docs/default/__plugin.json",1765],acecf23e:[()=>n.e(7393).then(n.t.bind(n,1838,19)),"~blog/default/blogMetadata-default.json",1838],af21c641:[()=>n.e(6974).then(n.bind(n,4998)),"@site/docs/home/innovations.md",4998],afc29949:[()=>Promise.all([n.e(532),n.e(4e3)]).then(n.bind(n,4149)),"@site/docs/user-guide/downloading.md",4149],ba771284:[()=>n.e(9268).then(n.bind(n,7735)),"@site/docs/user-guide/tasks.md",7735],bc03b1b7:[()=>n.e(4713).then(n.bind(n,586)),"@site/docs/user-guide/geobot.md",586],bc1f8660:[()=>n.e(2446).then(n.bind(n,9504)),"@site/docs/develop/design/data-model-old.md",9504],bebdd554:[()=>n.e(6142).then(n.bind(n,1615)),"@site/docs/user-guide/privacy.md",1615],c03baef0:[()=>n.e(4057).then(n.bind(n,3794)),"@site/docs/home/team.md",3794],c15d9823:[()=>n.e(6642).then(n.t.bind(n,2506,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-bd9.json",2506],c401bc0d:[()=>n.e(1666).then(n.bind(n,6988)),"@site/docs/user-guide/explore-a-chapter.md",6988],c48bbb24:[()=>n.e(7937).then(n.bind(n,7683)),"@site/docs/user-guide/define-a-garden.md",7683],c4f5d8e4:[()=>Promise.all([n.e(532),n.e(5655),n.e(4195)]).then(n.bind(n,5629)),"@site/src/pages/index.js",5629],c7c467a1:[()=>n.e(2522).then(n.bind(n,4811)),"@site/docs/develop/coding-standards.md",4811],ccc49370:[()=>Promise.all([n.e(532),n.e(2700),n.e(8041),n.e(6103)]).then(n.bind(n,9209)),"@theme/BlogPostPage",9209],e2299c6d:[()=>n.e(1340).then(n.bind(n,3831)),"@site/docs/develop/releases/release-1.0/cvp.md",3831],e27695c2:[()=>n.e(4524).then(n.bind(n,6538)),"@site/docs/home/serious-gardeners.md",6538],e4eb6786:[()=>n.e(2967).then(n.bind(n,1630)),"@site/docs/develop/releases/release-0.0/design.md",1630],ec0f34d7:[()=>n.e(8653).then(n.bind(n,34)),"@site/docs/business/milestones.md",34],ed0568ab:[()=>n.e(8392).then(n.bind(n,9987)),"@site/docs/develop/design/input-fields.md",9987],f3759001:[()=>n.e(7346).then(n.bind(n,1714)),"@site/docs/develop/architecture.md",1714],f81c1134:[()=>n.e(4031).then(n.t.bind(n,4108,19)),"@generated/docusaurus-plugin-content-blog/default/p/blog-archive-f05.json",4108],ff0b8175:[()=>n.e(1).then(n.bind(n,1350)),"@site/docs/develop/releases/release-0.0/chatgpt-feedback.md",1350]};var s=n(5893);function l(e){let{error:t,retry:n,pastDelay:r}=e;return t?(0,s.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,s.jsx)("p",{children:String(t)}),(0,s.jsx)("div",{children:(0,s.jsx)("button",{type:"button",onClick:n,children:"Retry"})})]}):r?(0,s.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,s.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,s.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,s.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,s.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,s.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,s.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,s.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,s.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var c=n(9670),u=n(226);function d(e,t){if("*"===e)return o()({loading:l,loader:()=>n.e(1772).then(n.bind(n,1772)),modules:["@theme/NotFound"],webpack:()=>[1772],render(e,t){const n=e.default;return(0,s.jsx)(u.z,{value:{plugin:{name:"native",id:"default"}},children:(0,s.jsx)(n,{...t})})}});const r=a[`${e}-${t}`],d={},p=[],f=[],g=(0,c.Z)(r);return Object.entries(g).forEach((e=>{let[t,n]=e;const r=i[n];r&&(d[t]=r[0],p.push(r[1]),f.push(r[2]))})),o().Map({loading:l,loader:d,modules:p,webpack:()=>f,render(t,n){const o=JSON.parse(JSON.stringify(r));Object.entries(t).forEach((t=>{let[n,r]=t;const a=r.default;if(!a)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof a&&"function"!=typeof a||Object.keys(r).filter((e=>"default"!==e)).forEach((e=>{a[e]=r[e]}));let i=o;const s=n.split(".");s.slice(0,-1).forEach((e=>{i=i[e]})),i[s[s.length-1]]=a}));const a=o.__comp;delete o.__comp;const i=o.__context;delete o.__context;const l=o.__props;return delete o.__props,(0,s.jsx)(u.z,{value:i,children:(0,s.jsx)(a,{...o,...l,...n})})}})}const p=[{path:"/blog",component:d("/blog","d00"),exact:!0},{path:"/blog/2023/02/10/welcome",component:d("/blog/2023/02/10/welcome","229"),exact:!0},{path:"/blog/archive",component:d("/blog/archive","182"),exact:!0},{path:"/markdown-page",component:d("/markdown-page","3d7"),exact:!0},{path:"/docs",component:d("/docs","d2b"),routes:[{path:"/docs",component:d("/docs","c94"),routes:[{path:"/docs",component:d("/docs","60d"),routes:[{path:"/docs/business",component:d("/docs/business","896"),exact:!0,sidebar:"businessSidebar"},{path:"/docs/business/market-size",component:d("/docs/business/market-size","3a5"),exact:!0,sidebar:"businessSidebar"},{path:"/docs/business/milestones",component:d("/docs/business/milestones","6b2"),exact:!0,sidebar:"businessSidebar"},{path:"/docs/business/roadmap",component:d("/docs/business/roadmap","2c2"),exact:!0,sidebar:"businessSidebar"},{path:"/docs/develop",component:d("/docs/develop","48a"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/architecture",component:d("/docs/develop/architecture","0cc"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/backups",component:d("/docs/develop/backups","e5f"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/coding-standards",component:d("/docs/develop/coding-standards","001"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/dart-analyze",component:d("/docs/develop/dart-analyze","4cc"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/deployment",component:d("/docs/develop/deployment","87a"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/badges",component:d("/docs/develop/design/badges","fc0"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/data-model",component:d("/docs/develop/design/data-model","c3c"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/data-model-old",component:d("/docs/develop/design/data-model-old","1c8"),exact:!0},{path:"/docs/develop/design/data-mutation",component:d("/docs/develop/design/data-mutation","7ce"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/features",component:d("/docs/develop/design/features","286"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/input-fields",component:d("/docs/develop/design/input-fields","bde"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/design/with-widgets",component:d("/docs/develop/design/with-widgets","9f7"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/installation",component:d("/docs/develop/installation","7f5"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/integrity-check",component:d("/docs/develop/integrity-check","9d1"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/managing-firebase-data",component:d("/docs/develop/managing-firebase-data","a21"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/onboarding",component:d("/docs/develop/onboarding","d33"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-0.0/chatgpt-feedback",component:d("/docs/develop/releases/release-0.0/chatgpt-feedback","596"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-0.0/customer-feedback",component:d("/docs/develop/releases/release-0.0/customer-feedback","fb7"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-0.0/design",component:d("/docs/develop/releases/release-0.0/design","35c"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-0.0/entrepreneur-feedback",component:d("/docs/develop/releases/release-0.0/entrepreneur-feedback","c03"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-1.0/cvp",component:d("/docs/develop/releases/release-1.0/cvp","391"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-1.0/end-of-season-feedback",component:d("/docs/develop/releases/release-1.0/end-of-season-feedback","a2f"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-1.0/goals",component:d("/docs/develop/releases/release-1.0/goals","088"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/releases/release-1.0/onboarding-feedback",component:d("/docs/develop/releases/release-1.0/onboarding-feedback","e62"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/scripts",component:d("/docs/develop/scripts","c76"),exact:!0,sidebar:"developSidebar"},{path:"/docs/develop/testing",component:d("/docs/develop/testing","ed4"),exact:!0,sidebar:"developSidebar"},{path:"/docs/home/food-security",component:d("/docs/home/food-security","05f"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/innovations",component:d("/docs/home/innovations","4fa"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/related-work",component:d("/docs/home/related-work","3cc"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/serious-gardeners",component:d("/docs/home/serious-gardeners","e3e"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/sneak-peek",component:d("/docs/home/sneak-peek","e8b"),exact:!0},{path:"/docs/home/team",component:d("/docs/home/team","322"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/home/welcome",component:d("/docs/home/welcome","e24"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/adding-plantings",component:d("/docs/user-guide/adding-plantings","36e"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/adding-vendors-crops-varieties",component:d("/docs/user-guide/adding-vendors-crops-varieties","949"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/badges",component:d("/docs/user-guide/badges","e10"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/chat-rooms",component:d("/docs/user-guide/chat-rooms","655"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/define-a-garden",component:d("/docs/user-guide/define-a-garden","e94"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/downloading",component:d("/docs/user-guide/downloading","791"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/explore-a-chapter",component:d("/docs/user-guide/explore-a-chapter","271"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/explore-a-garden",component:d("/docs/user-guide/explore-a-garden","ed5"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/geobot",component:d("/docs/user-guide/geobot","9e0"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/guided-tour",component:d("/docs/user-guide/guided-tour","cc9"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/observations",component:d("/docs/user-guide/observations","dfc"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/outcomes",component:d("/docs/user-guide/outcomes","d0c"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/overview",component:d("/docs/user-guide/overview","b62"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/privacy",component:d("/docs/user-guide/privacy","f6a"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/registration",component:d("/docs/user-guide/registration","0ae"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/scenarios",component:d("/docs/user-guide/scenarios","5f9"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/seeds",component:d("/docs/user-guide/seeds","5ba"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/tasks",component:d("/docs/user-guide/tasks","7c3"),exact:!0,sidebar:"homeSidebar"},{path:"/docs/user-guide/terms-and-conditions",component:d("/docs/user-guide/terms-and-conditions","bed"),exact:!0,sidebar:"homeSidebar"}]}]}]},{path:"/",component:d("/","2e1"),exact:!0},{path:"*",component:d("*")}]},8934:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,t:()=>i});var r=n(7294),o=n(5893);const a=r.createContext(!1);function i(e){let{children:t}=e;const[n,i]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{i(!0)}),[]),(0,o.jsx)(a.Provider,{value:n,children:t})}},2849:(e,t,n)=>{"use strict";var r=n(7294),o=n(745),a=n(405),i=n(3727),s=n(6809),l=n(412);const c=[n(2497),n(3310),n(8320),n(2295)];var u=n(723),d=n(6550),p=n(8790),f=n(5893);function g(e){let{children:t}=e;return(0,f.jsx)(f.Fragment,{children:t})}var h=n(5742),m=n(2263),b=n(4996),y=n(6668),v=n(8264),w=n(4711),k=n(9727);const x="default";var S=n(8780),_=n(197);function E(){const{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,m.Z)(),r=(0,w.l)(),o=n[e].htmlLang,a=e=>e.replace("-","_");return(0,f.jsxs)(h.Z,{children:[Object.entries(n).map((e=>{let[t,{htmlLang:n}]=e;return(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:n},t)})),(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,f.jsx)("meta",{property:"og:locale",content:a(o)}),Object.values(n).filter((e=>o!==e.htmlLang)).map((e=>(0,f.jsx)("meta",{property:"og:locale:alternate",content:a(e.htmlLang)},`meta-og-${e.htmlLang}`)))]})}function C(e){let{permalink:t}=e;const{siteConfig:{url:n}}=(0,m.Z)(),r=function(){const{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,m.Z)(),{pathname:r}=(0,d.TH)();return e+(0,S.Do)((0,b.ZP)(r),{trailingSlash:n,baseUrl:t})}(),o=t?`${n}${t}`:r;return(0,f.jsxs)(h.Z,{children:[(0,f.jsx)("meta",{property:"og:url",content:o}),(0,f.jsx)("link",{rel:"canonical",href:o})]})}function T(){const{i18n:{currentLocale:e}}=(0,m.Z)(),{metadata:t,image:n}=(0,y.L)();return(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(h.Z,{children:[(0,f.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,f.jsx)("body",{className:k.h})]}),n&&(0,f.jsx)(v.d,{image:n}),(0,f.jsx)(C,{}),(0,f.jsx)(E,{}),(0,f.jsx)(_.Z,{tag:x,locale:e}),(0,f.jsx)(h.Z,{children:t.map(((e,t)=>(0,f.jsx)("meta",{...e},t)))})]})}const j=new Map;var N=n(8934),P=n(8940),A=n(469);function L(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{const r=t.default?.[e]??t[e];return r?.(...n)}));return()=>o.forEach((e=>e?.()))}const O=function(e){let{children:t,location:n,previousLocation:r}=e;return(0,A.Z)((()=>{r!==n&&(!function(e){let{location:t,previousLocation:n}=e;if(!n)return;const r=t.pathname===n.pathname,o=t.hash===n.hash,a=t.search===n.search;if(r&&o&&!a)return;const{hash:i}=t;if(i){const e=decodeURIComponent(i.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:n,previousLocation:r}),L("onRouteDidUpdate",{previousLocation:r,location:n}))}),[r,n]),t};function R(e){const t=Array.from(new Set([e,decodeURI(e)])).map((e=>(0,p.f)(u.Z,e))).flat();return Promise.all(t.map((e=>e.route.component.preload?.())))}class I extends r.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=l.Z.canUseDOM?L("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=L("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),R(n.pathname).then((()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})})).catch((e=>{console.warn(e),window.location.reload()})),!1}render(){const{children:e,location:t}=this.props;return(0,f.jsx)(O,{previousLocation:this.previousLocation,location:t,children:(0,f.jsx)(d.AW,{location:t,render:()=>e})})}}const F=I,D="__docusaurus-base-url-issue-banner-container",M="__docusaurus-base-url-issue-banner",z="__docusaurus-base-url-issue-banner-suggestion-container";function B(e){return`\ndocument.addEventListener('DOMContentLoaded', function maybeInsertBanner() {\n var shouldInsert = typeof window['docusaurus'] === 'undefined';\n shouldInsert && insertBanner();\n});\n\nfunction insertBanner() {\n var bannerContainer = document.createElement('div');\n bannerContainer.id = '${D}';\n var bannerHtml = ${JSON.stringify(function(e){return`\n
\n

Your Docusaurus site did not load properly.

\n

A very common reason is a wrong site baseUrl configuration.

\n

Current configured baseUrl = ${e} ${"/"===e?" (default value)":""}

\n

We suggest trying baseUrl =

\n
\n`}(e)).replace(/{let{route:t}=e;return!0===t.exact})))return j.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return j.set(e.pathname,t),{...e,pathname:t}}((0,d.TH)());return(0,f.jsx)(F,{location:e,children:G})}function Y(){return(0,f.jsx)(H.Z,{children:(0,f.jsx)(P.M,{children:(0,f.jsxs)(N.t,{children:[(0,f.jsxs)(g,{children:[(0,f.jsx)(Z,{}),(0,f.jsx)(T,{}),(0,f.jsx)(U,{}),(0,f.jsx)(q,{})]}),(0,f.jsx)(W,{})]})})})}var Q=n(6887);const K=function(e){try{return document.createElement("link").relList.supports(e)}catch{return!1}}("prefetch")?function(e){return new Promise(((t,n)=>{if("undefined"==typeof document)return void n();const r=document.createElement("link");r.setAttribute("rel","prefetch"),r.setAttribute("href",e),r.onload=()=>t(),r.onerror=()=>n();const o=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;o?.appendChild(r)}))}:function(e){return new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)}))};var X=n(9670);const J=new Set,ee=new Set,te=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ne={prefetch:e=>{if(!(e=>!te()&&!ee.has(e)&&!J.has(e))(e))return!1;J.add(e);const t=(0,p.f)(u.Z,e).flatMap((e=>{return t=e.route.path,Object.entries(Q).filter((e=>{let[n]=e;return n.replace(/-[^-]+$/,"")===t})).flatMap((e=>{let[,t]=e;return Object.values((0,X.Z)(t))}));var t}));return Promise.all(t.map((e=>{const t=n.gca(e);return t&&!t.includes("undefined")?K(t).catch((()=>{})):Promise.resolve()})))},preload:e=>!!(e=>!te()&&!ee.has(e))(e)&&(ee.add(e),R(e))},re=Object.freeze(ne);function oe(e){let{children:t}=e;return"hash"===s.default.future.experimental_router?(0,f.jsx)(i.UT,{children:t}):(0,f.jsx)(i.VK,{children:t})}const ae=Boolean(!0);if(l.Z.canUseDOM){window.docusaurus=re;const e=document.getElementById("__docusaurus"),t=(0,f.jsx)(a.B6,{children:(0,f.jsx)(oe,{children:(0,f.jsx)(Y,{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},i=()=>{if(window.docusaurusRoot)window.docusaurusRoot.render(t);else if(ae)window.docusaurusRoot=o.hydrateRoot(e,t,{onRecoverableError:n});else{const r=o.createRoot(e,{onRecoverableError:n});r.render(t),window.docusaurusRoot=r}};R(window.location.pathname).then((()=>{(0,r.startTransition)(i)}))}},8940:(e,t,n)=>{"use strict";n.d(t,{_:()=>d,M:()=>p});var r=n(7294),o=n(6809);const a=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/docs","versions":[{"name":"current","label":"Next","isLast":true,"path":"/docs","mainDocId":"home/welcome","docs":[{"id":"business/index","path":"/docs/business/","sidebar":"businessSidebar"},{"id":"business/market-size","path":"/docs/business/market-size","sidebar":"businessSidebar"},{"id":"business/milestones","path":"/docs/business/milestones","sidebar":"businessSidebar"},{"id":"business/roadmap","path":"/docs/business/roadmap","sidebar":"businessSidebar"},{"id":"develop/architecture","path":"/docs/develop/architecture","sidebar":"developSidebar"},{"id":"develop/backups","path":"/docs/develop/backups","sidebar":"developSidebar"},{"id":"develop/coding-standards","path":"/docs/develop/coding-standards","sidebar":"developSidebar"},{"id":"develop/dart-analyze","path":"/docs/develop/dart-analyze","sidebar":"developSidebar"},{"id":"develop/deployment","path":"/docs/develop/deployment","sidebar":"developSidebar"},{"id":"develop/design/badges","path":"/docs/develop/design/badges","sidebar":"developSidebar"},{"id":"develop/design/data-model","path":"/docs/develop/design/data-model","sidebar":"developSidebar"},{"id":"develop/design/data-model-old","path":"/docs/develop/design/data-model-old"},{"id":"develop/design/data-mutation","path":"/docs/develop/design/data-mutation","sidebar":"developSidebar"},{"id":"develop/design/features","path":"/docs/develop/design/features","sidebar":"developSidebar"},{"id":"develop/design/input-fields","path":"/docs/develop/design/input-fields","sidebar":"developSidebar"},{"id":"develop/design/with-widgets","path":"/docs/develop/design/with-widgets","sidebar":"developSidebar"},{"id":"develop/index","path":"/docs/develop/","sidebar":"developSidebar"},{"id":"develop/installation","path":"/docs/develop/installation","sidebar":"developSidebar"},{"id":"develop/integrity-check","path":"/docs/develop/integrity-check","sidebar":"developSidebar"},{"id":"develop/managing-firebase-data","path":"/docs/develop/managing-firebase-data","sidebar":"developSidebar"},{"id":"develop/onboarding","path":"/docs/develop/onboarding","sidebar":"developSidebar"},{"id":"develop/releases/release-0.0/chatgpt-feedback","path":"/docs/develop/releases/release-0.0/chatgpt-feedback","sidebar":"developSidebar"},{"id":"develop/releases/release-0.0/customer-feedback","path":"/docs/develop/releases/release-0.0/customer-feedback","sidebar":"developSidebar"},{"id":"develop/releases/release-0.0/design","path":"/docs/develop/releases/release-0.0/design","sidebar":"developSidebar"},{"id":"develop/releases/release-0.0/entrepreneur-feedback","path":"/docs/develop/releases/release-0.0/entrepreneur-feedback","sidebar":"developSidebar"},{"id":"develop/releases/release-1.0/cvp","path":"/docs/develop/releases/release-1.0/cvp","sidebar":"developSidebar"},{"id":"develop/releases/release-1.0/end-of-season-feedback","path":"/docs/develop/releases/release-1.0/end-of-season-feedback","sidebar":"developSidebar"},{"id":"develop/releases/release-1.0/goals","path":"/docs/develop/releases/release-1.0/goals","sidebar":"developSidebar"},{"id":"develop/releases/release-1.0/onboarding-feedback","path":"/docs/develop/releases/release-1.0/onboarding-feedback","sidebar":"developSidebar"},{"id":"develop/scripts","path":"/docs/develop/scripts","sidebar":"developSidebar"},{"id":"develop/testing","path":"/docs/develop/testing","sidebar":"developSidebar"},{"id":"home/food-security","path":"/docs/home/food-security","sidebar":"homeSidebar"},{"id":"home/innovations","path":"/docs/home/innovations","sidebar":"homeSidebar"},{"id":"home/related-work","path":"/docs/home/related-work","sidebar":"homeSidebar"},{"id":"home/serious-gardeners","path":"/docs/home/serious-gardeners","sidebar":"homeSidebar"},{"id":"home/sneak-peek","path":"/docs/home/sneak-peek"},{"id":"home/team","path":"/docs/home/team","sidebar":"homeSidebar"},{"id":"home/welcome","path":"/docs/home/welcome","sidebar":"homeSidebar"},{"id":"user-guide/adding-plantings","path":"/docs/user-guide/adding-plantings","sidebar":"homeSidebar"},{"id":"user-guide/adding-vendors-crops-varieties","path":"/docs/user-guide/adding-vendors-crops-varieties","sidebar":"homeSidebar"},{"id":"user-guide/badges","path":"/docs/user-guide/badges","sidebar":"homeSidebar"},{"id":"user-guide/chat-rooms","path":"/docs/user-guide/chat-rooms","sidebar":"homeSidebar"},{"id":"user-guide/define-a-garden","path":"/docs/user-guide/define-a-garden","sidebar":"homeSidebar"},{"id":"user-guide/downloading","path":"/docs/user-guide/downloading","sidebar":"homeSidebar"},{"id":"user-guide/explore-a-chapter","path":"/docs/user-guide/explore-a-chapter","sidebar":"homeSidebar"},{"id":"user-guide/explore-a-garden","path":"/docs/user-guide/explore-a-garden","sidebar":"homeSidebar"},{"id":"user-guide/geobot","path":"/docs/user-guide/geobot","sidebar":"homeSidebar"},{"id":"user-guide/guided-tour","path":"/docs/user-guide/guided-tour","sidebar":"homeSidebar"},{"id":"user-guide/observations","path":"/docs/user-guide/observations","sidebar":"homeSidebar"},{"id":"user-guide/outcomes","path":"/docs/user-guide/outcomes","sidebar":"homeSidebar"},{"id":"user-guide/overview","path":"/docs/user-guide/overview","sidebar":"homeSidebar"},{"id":"user-guide/privacy","path":"/docs/user-guide/privacy","sidebar":"homeSidebar"},{"id":"user-guide/registration","path":"/docs/user-guide/registration","sidebar":"homeSidebar"},{"id":"user-guide/scenarios","path":"/docs/user-guide/scenarios","sidebar":"homeSidebar"},{"id":"user-guide/seeds","path":"/docs/user-guide/seeds","sidebar":"homeSidebar"},{"id":"user-guide/tasks","path":"/docs/user-guide/tasks","sidebar":"homeSidebar"},{"id":"user-guide/terms-and-conditions","path":"/docs/user-guide/terms-and-conditions","sidebar":"homeSidebar"}],"draftIds":[],"sidebars":{"homeSidebar":{"link":{"path":"/docs/home/welcome","label":"home/welcome"}},"businessSidebar":{"link":{"path":"/docs/business/","label":"business/index"}},"developSidebar":{"link":{"path":"/docs/develop/","label":"develop/index"}}}}],"breadcrumbs":true}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"}}}');var s=n(7529);const l=JSON.parse('{"docusaurusVersion":"3.5.2","siteVersion":"1.0.0","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.5.2"},"docusaurus-plugin-content-blog":{"type":"package","name":"@docusaurus/plugin-content-blog","version":"3.5.2"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.5.2"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.5.2"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.5.2"}}}');var c=n(5893);const u={siteConfig:o.default,siteMetadata:l,globalData:a,i18n:i,codeTranslations:s},d=r.createContext(u);function p(e){let{children:t}=e;return(0,c.jsx)(d.Provider,{value:u,children:t})}},4763:(e,t,n)=>{"use strict";n.d(t,{Z:()=>h});var r=n(7294),o=n(412),a=n(5742),i=n(8780),s=n(6040),l=n(226),c=n(5893);function u(e){let{error:t,tryAgain:n}=e;return(0,c.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,c.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,c.jsx)("button",{type:"button",onClick:n,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,c.jsx)(d,{error:t})]})}function d(e){let{error:t}=e;const n=(0,i.BN)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,c.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:n})}function p(e){let{children:t}=e;return(0,c.jsx)(l.z,{value:{plugin:{name:"docusaurus-core-error-boundary",id:"default"}},children:t})}function f(e){let{error:t,tryAgain:n}=e;return(0,c.jsx)(p,{children:(0,c.jsxs)(h,{fallback:()=>(0,c.jsx)(u,{error:t,tryAgain:n}),children:[(0,c.jsx)(a.Z,{children:(0,c.jsx)("title",{children:"Page Error"})}),(0,c.jsx)(s.Z,{children:(0,c.jsx)(u,{error:t,tryAgain:n})})]})})}const g=e=>(0,c.jsx)(f,{...e});class h extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){o.Z.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??g)(e)}return e??null}}},412:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});const r="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,o={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},5742:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(7294);var r=n(405),o=n(5893);function a(e){return(0,o.jsx)(r.ql,{...e})}},3692:(e,t,n)=>{"use strict";n.d(t,{Z:()=>f});var r=n(7294),o=n(3727),a=n(8780),i=n(2263),s=n(3919),l=n(412),c=n(8138),u=n(4996),d=n(5893);function p(e,t){let{isNavLink:n,to:p,href:f,activeClassName:g,isActive:h,"data-noBrokenLinkCheck":m,autoAddBaseUrl:b=!0,...y}=e;const{siteConfig:v}=(0,i.Z)(),{trailingSlash:w,baseUrl:k}=v,x=v.future.experimental_router,{withBaseUrl:S}=(0,u.Cg)(),_=(0,c.Z)(),E=(0,r.useRef)(null);(0,r.useImperativeHandle)(t,(()=>E.current));const C=p||f;const T=(0,s.Z)(C),j=C?.replace("pathname://","");let N=void 0!==j?(P=j,b&&(e=>e.startsWith("/"))(P)?S(P):P):void 0;var P;"hash"===x&&N?.startsWith("./")&&(N=N?.slice(1)),N&&T&&(N=(0,a.Do)(N,{trailingSlash:w,baseUrl:k}));const A=(0,r.useRef)(!1),L=n?o.OL:o.rU,O=l.Z.canUseIntersectionObserver,R=(0,r.useRef)(),I=()=>{A.current||null==N||(window.docusaurus.preload(N),A.current=!0)};(0,r.useEffect)((()=>(!O&&T&&l.Z.canUseDOM&&null!=N&&window.docusaurus.prefetch(N),()=>{O&&R.current&&R.current.disconnect()})),[R,N,O,T]);const F=N?.startsWith("#")??!1,D=!y.target||"_self"===y.target,M=!N||!T||!D||F&&"hash"!==x;m||!F&&M||_.collectLink(N),y.id&&_.collectAnchor(y.id);const z={};return M?(0,d.jsx)("a",{ref:E,href:N,...C&&!T&&{target:"_blank",rel:"noopener noreferrer"},...y,...z}):(0,d.jsx)(L,{...y,onMouseEnter:I,onTouchStart:I,innerRef:e=>{E.current=e,O&&e&&T&&(R.current=new window.IntersectionObserver((t=>{t.forEach((t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(R.current.unobserve(e),R.current.disconnect(),null!=N&&window.docusaurus.prefetch(N))}))})),R.current.observe(e))},to:N,...n&&{isActive:h,activeClassName:g},...z})}const f=r.forwardRef(p)},1875:(e,t,n)=>{"use strict";n.d(t,{Z:()=>r});const r=()=>null},5999:(e,t,n)=>{"use strict";n.d(t,{Z:()=>c,I:()=>l});var r=n(7294),o=n(5893);function a(e,t){const n=e.split(/(\{\w+\})/).map(((e,n)=>{if(n%2==1){const n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e}));return n.some((e=>(0,r.isValidElement)(e)))?n.map(((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e)).filter((e=>""!==e)):n.join("")}var i=n(7529);function s(e){let{id:t,message:n}=e;if(void 0===t&&void 0===n)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return i[t??n]??n??t}function l(e,t){let{message:n,id:r}=e;return a(s({message:n,id:r}),t)}function c(e){let{children:t,id:n,values:r}=e;if(t&&"string"!=typeof t)throw console.warn("Illegal children",t),new Error("The Docusaurus component only accept simple string values");const i=s({message:t,id:n});return(0,o.jsx)(o.Fragment,{children:a(i,r)})}},9935:(e,t,n)=>{"use strict";n.d(t,{m:()=>r});const r="default"},3919:(e,t,n)=>{"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function o(e){return void 0!==e&&!r(e)}n.d(t,{Z:()=>o,b:()=>r})},4996:(e,t,n)=>{"use strict";n.d(t,{Cg:()=>i,ZP:()=>s});var r=n(7294),o=n(2263),a=n(3919);function i(){const{siteConfig:e}=(0,o.Z)(),{baseUrl:t,url:n}=e,i=e.future.experimental_router,s=(0,r.useCallback)(((e,r)=>function(e){let{siteUrl:t,baseUrl:n,url:r,options:{forcePrependBaseUrl:o=!1,absolute:i=!1}={},router:s}=e;if(!r||r.startsWith("#")||(0,a.b)(r))return r;if("hash"===s)return r.startsWith("/")?`.${r}`:`./${r}`;if(o)return n+r.replace(/^\//,"");if(r===n.replace(/\/$/,""))return n;const l=r.startsWith(n)?r:n+r.replace(/^\//,"");return i?t+l:l}({siteUrl:n,baseUrl:t,url:e,options:r,router:i})),[n,t,i]);return{withBaseUrl:s}}function s(e,t){void 0===t&&(t={});const{withBaseUrl:n}=i();return n(e,t)}},8138:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var r=n(7294);n(5893);const o=r.createContext({collectAnchor:()=>{},collectLink:()=>{}}),a=()=>(0,r.useContext)(o);function i(){return a()}},2263:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(7294),o=n(8940);function a(){return(0,r.useContext)(o._)}},2389:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(7294),o=n(8934);function a(){return(0,r.useContext)(o._)}},469:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var r=n(7294);const o=n(412).Z.canUseDOM?r.useLayoutEffect:r.useEffect},5102:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(7294),o=n(226);function a(){const e=r.useContext(o._);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}},9670:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});const r=e=>"object"==typeof e&&!!e&&Object.keys(e).length>0;function o(e){const t={};return function e(n,o){Object.entries(n).forEach((n=>{let[a,i]=n;const s=o?`${o}.${a}`:a;r(i)?e(i,s):t[s]=i}))}(e),t}},226:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,z:()=>i});var r=n(7294),o=n(5893);const a=r.createContext(null);function i(e){let{children:t,value:n}=e;const i=r.useContext(a),s=(0,r.useMemo)((()=>function(e){let{parent:t,value:n}=e;if(!t){if(!n)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in n))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return n}const r={...t.data,...n?.data};return{plugin:t.plugin,data:r}}({parent:i,value:n})),[i,n]);return(0,o.jsx)(a.Provider,{value:s,children:t})}},298:(e,t,n)=>{"use strict";n.d(t,{J:()=>y,L5:()=>m});var r=n(7294),o=n(143),a=n(9935),i=n(6668),s=n(812),l=n(902),c=n(5893);const u=e=>`docs-preferred-version-${e}`,d={save:(e,t,n)=>{(0,s.WA)(u(e),{persistence:t}).set(n)},read:(e,t)=>(0,s.WA)(u(e),{persistence:t}).get(),clear:(e,t)=>{(0,s.WA)(u(e),{persistence:t}).del()}},p=e=>Object.fromEntries(e.map((e=>[e,{preferredVersionName:null}])));const f=r.createContext(null);function g(){const e=(0,o._r)(),t=(0,i.L)().docs.versionPersistence,n=(0,r.useMemo)((()=>Object.keys(e)),[e]),[a,s]=(0,r.useState)((()=>p(n)));(0,r.useEffect)((()=>{s(function(e){let{pluginIds:t,versionPersistence:n,allDocsData:r}=e;function o(e){const t=d.read(e,n);return r[e].versions.some((e=>e.name===t))?{preferredVersionName:t}:(d.clear(e,n),{preferredVersionName:null})}return Object.fromEntries(t.map((e=>[e,o(e)])))}({allDocsData:e,versionPersistence:t,pluginIds:n}))}),[e,t,n]);return[a,(0,r.useMemo)((()=>({savePreferredVersion:function(e,n){d.save(e,t,n),s((t=>({...t,[e]:{preferredVersionName:n}})))}})),[t])]}function h(e){let{children:t}=e;const n=g();return(0,c.jsx)(f.Provider,{value:n,children:t})}function m(e){let{children:t}=e;return(0,c.jsx)(h,{children:t})}function b(){const e=(0,r.useContext)(f);if(!e)throw new l.i6("DocsPreferredVersionContextProvider");return e}function y(e){void 0===e&&(e=a.m);const t=(0,o.zh)(e),[n,i]=b(),{preferredVersionName:s}=n[e];return{preferredVersion:t.versions.find((e=>e.name===s))??null,savePreferredVersionName:(0,r.useCallback)((t=>{i.savePreferredVersion(e,t)}),[i,e])}}},4731:(e,t,n)=>{"use strict";n.d(t,{V:()=>c,b:()=>l});var r=n(7294),o=n(902),a=n(5893);const i=Symbol("EmptyContext"),s=r.createContext(i);function l(e){let{children:t,name:n,items:o}=e;const i=(0,r.useMemo)((()=>n&&o?{name:n,items:o}:null),[n,o]);return(0,a.jsx)(s.Provider,{value:i,children:t})}function c(){const e=(0,r.useContext)(s);if(e===i)throw new o.i6("DocsSidebarProvider");return e}},9690:(e,t,n)=>{"use strict";n.d(t,{LM:()=>p,SN:()=>S,_F:()=>h,f:()=>b,lO:()=>w,oz:()=>k,s1:()=>v,vY:()=>x});var r=n(7294),o=n(6550),a=n(8790),i=n(143),s=n(8596),l=n(7392),c=n(298),u=n(3797),d=n(4731);function p(e){return"link"!==e.type||e.unlisted?"category"===e.type?function(e){if(e.href&&!e.linkUnlisted)return e.href;for(const t of e.items){const e=p(t);if(e)return e}}(e):void 0:e.href}const f=(e,t)=>void 0!==e&&(0,s.Mg)(e,t),g=(e,t)=>e.some((e=>h(e,t)));function h(e,t){return"link"===e.type?f(e.href,t):"category"===e.type&&(f(e.href,t)||g(e.items,t))}function m(e,t){switch(e.type){case"category":return h(e,t)||e.items.some((e=>m(e,t)));case"link":return!e.unlisted||h(e,t);default:return!0}}function b(e,t){return(0,r.useMemo)((()=>e.filter((e=>m(e,t)))),[e,t])}function y(e){let{sidebarItems:t,pathname:n,onlyCategories:r=!1}=e;const o=[];return function e(t){for(const a of t)if("category"===a.type&&((0,s.Mg)(a.href,n)||e(a.items))||"link"===a.type&&(0,s.Mg)(a.href,n)){return r&&"category"!==a.type||o.unshift(a),!0}return!1}(t),o}function v(){const e=(0,d.V)(),{pathname:t}=(0,o.TH)(),n=(0,i.gA)()?.pluginData.breadcrumbs;return!1!==n&&e?y({sidebarItems:e.items,pathname:t}):null}function w(e){const{activeVersion:t}=(0,i.Iw)(e),{preferredVersion:n}=(0,c.J)(e),o=(0,i.yW)(e);return(0,r.useMemo)((()=>(0,l.jj)([t,n,o].filter(Boolean))),[t,n,o])}function k(e,t){const n=w(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.sidebars?Object.entries(e.sidebars):[])),r=t.find((t=>t[0]===e));if(!r)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\nAvailable sidebar ids are:\n- ${t.map((e=>e[0])).join("\n- ")}`);return r[1]}),[e,n])}function x(e,t){const n=w(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.docs)),r=t.find((t=>t.id===e));if(!r){if(n.flatMap((e=>e.draftIds)).includes(e))return null;throw new Error(`Couldn't find any doc with id "${e}" in version${n.length>1?"s":""} "${n.map((e=>e.name)).join(", ")}".\nAvailable doc ids are:\n- ${(0,l.jj)(t.map((e=>e.id))).join("\n- ")}`)}return r}),[e,n])}function S(e){let{route:t}=e;const n=(0,o.TH)(),r=(0,u.E)(),i=t.routes,s=i.find((e=>(0,o.LX)(n.pathname,e)));if(!s)return null;const l=s.sidebar,c=l?r.docsSidebars[l]:void 0;return{docElement:(0,a.H)(i),sidebarName:l,sidebarItems:c}}},3797:(e,t,n)=>{"use strict";n.d(t,{E:()=>l,q:()=>s});var r=n(7294),o=n(902),a=n(5893);const i=r.createContext(null);function s(e){let{children:t,version:n}=e;return(0,a.jsx)(i.Provider,{value:n,children:t})}function l(){const e=(0,r.useContext)(i);if(null===e)throw new o.i6("DocsVersionProvider");return e}},143:(e,t,n)=>{"use strict";n.d(t,{Iw:()=>h,gA:()=>p,_r:()=>u,Jo:()=>m,zh:()=>d,yW:()=>g,gB:()=>f});var r=n(6550),o=n(2263),a=n(9935);function i(e,t){void 0===t&&(t={});const n=function(){const{globalData:e}=(0,o.Z)();return e}()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}const s=e=>e.versions.find((e=>e.isLast));function l(e,t){const n=function(e,t){return[...e.versions].sort(((e,t)=>e.path===t.path?0:e.path.includes(t.path)?-1:t.path.includes(e.path)?1:0)).find((e=>!!(0,r.LX)(t,{path:e.path,exact:!1,strict:!1})))}(e,t),o=n?.docs.find((e=>!!(0,r.LX)(t,{path:e.path,exact:!0,strict:!1})));return{activeVersion:n,activeDoc:o,alternateDocVersions:o?function(t){const n={};return e.versions.forEach((e=>{e.docs.forEach((r=>{r.id===t&&(n[e.name]=r)}))})),n}(o.id):{}}}const c={},u=()=>i("docusaurus-plugin-content-docs")??c,d=e=>{try{return function(e,t,n){void 0===t&&(t=a.m),void 0===n&&(n={});const r=i(e),o=r?.[t];if(!o&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return o}("docusaurus-plugin-content-docs",e,{failfast:!0})}catch(t){throw new Error("You are using a feature of the Docusaurus docs plugin, but this plugin does not seem to be enabled"+("Default"===e?"":` (pluginId=${e}`),{cause:t})}};function p(e){void 0===e&&(e={});const t=u(),{pathname:n}=(0,r.TH)();return function(e,t,n){void 0===n&&(n={});const o=Object.entries(e).sort(((e,t)=>t[1].path.localeCompare(e[1].path))).find((e=>{let[,n]=e;return!!(0,r.LX)(t,{path:n.path,exact:!1,strict:!1})})),a=o?{pluginId:o[0],pluginData:o[1]}:void 0;if(!a&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map((e=>e.path)).join(", ")}`);return a}(t,n,e)}function f(e){return d(e).versions}function g(e){const t=d(e);return s(t)}function h(e){const t=d(e),{pathname:n}=(0,r.TH)();return l(t,n)}function m(e){const t=d(e),{pathname:n}=(0,r.TH)();return function(e,t){const n=s(e);return{latestDocSuggestion:l(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},8320:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(4865),o=n.n(r);o().configure({showSpinner:!1});const a={onRouteUpdate(e){let{location:t,previousLocation:n}=e;if(n&&t.pathname!==n.pathname){const e=window.setTimeout((()=>{o().start()}),200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){o().done()}}},3310:(e,t,n)=>{"use strict";n.r(t);var r=n(4798),o=n(6809);!function(e){const{themeConfig:{prism:t}}=o.default,{additionalLanguages:r}=t;globalThis.Prism=e,r.forEach((e=>{"php"===e&&n(6854),n(6046)(`./prism-${e}`)})),delete globalThis.Prism}(r.p1)},2503:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});n(7294);var r=n(6905),o=n(5999),a=n(6668),i=n(3692),s=n(8138);const l={anchorWithStickyNavbar:"anchorWithStickyNavbar_LWe7",anchorWithHideOnScrollNavbar:"anchorWithHideOnScrollNavbar_WYt5"};var c=n(5893);function u(e){let{as:t,id:n,...u}=e;const d=(0,s.Z)(),{navbar:{hideOnScroll:p}}=(0,a.L)();if("h1"===t||!n)return(0,c.jsx)(t,{...u,id:void 0});d.collectAnchor(n);const f=(0,o.I)({id:"theme.common.headingLinkTitle",message:"Direct link to {heading}",description:"Title for link to heading"},{heading:"string"==typeof u.children?u.children:n});return(0,c.jsxs)(t,{...u,className:(0,r.Z)("anchor",p?l.anchorWithHideOnScrollNavbar:l.anchorWithStickyNavbar,u.className),id:n,children:[u.children,(0,c.jsx)(i.Z,{className:"hash-link",to:`#${n}`,"aria-label":f,title:f,children:"\u200b"})]})}},9471:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(7294);const r={iconExternalLink:"iconExternalLink_nPIU"};var o=n(5893);function a(e){let{width:t=13.5,height:n=13.5}=e;return(0,o.jsx)("svg",{width:t,height:n,"aria-hidden":"true",viewBox:"0 0 24 24",className:r.iconExternalLink,children:(0,o.jsx)("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"})})}},6040:(e,t,n)=>{"use strict";n.d(t,{Z:()=>ft});var r=n(7294),o=n(6905),a=n(4763),i=n(8264),s=n(6550),l=n(5999),c=n(5936),u=n(5893);const d="__docusaurus_skipToContent_fallback";function p(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}function f(){const e=(0,r.useRef)(null),{action:t}=(0,s.k6)(),n=(0,r.useCallback)((e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.getElementById(d);t&&p(t)}),[]);return(0,c.S)((n=>{let{location:r}=n;e.current&&!r.hash&&"PUSH"===t&&p(e.current)})),{containerRef:e,onClick:n}}const g=(0,l.I)({id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",message:"Skip to main content"});function h(e){const t=e.children??g,{containerRef:n,onClick:r}=f();return(0,u.jsx)("div",{ref:n,role:"region","aria-label":g,children:(0,u.jsx)("a",{...e,href:`#${d}`,onClick:r,children:t})})}var m=n(5281),b=n(9727);const y={skipToContent:"skipToContent_fXgn"};function v(){return(0,u.jsx)(h,{className:y.skipToContent})}var w=n(6668),k=n(9689);function x(e){let{width:t=21,height:n=21,color:r="currentColor",strokeWidth:o=1.2,className:a,...i}=e;return(0,u.jsx)("svg",{viewBox:"0 0 15 15",width:t,height:n,...i,children:(0,u.jsx)("g",{stroke:r,strokeWidth:o,children:(0,u.jsx)("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})})})}const S={closeButton:"closeButton_CVFx"};function _(e){return(0,u.jsx)("button",{type:"button","aria-label":(0,l.I)({id:"theme.AnnouncementBar.closeButtonAriaLabel",message:"Close",description:"The ARIA label for close button of announcement bar"}),...e,className:(0,o.Z)("clean-btn close",S.closeButton,e.className),children:(0,u.jsx)(x,{width:14,height:14,strokeWidth:3.1})})}const E={content:"content_knG7"};function C(e){const{announcementBar:t}=(0,w.L)(),{content:n}=t;return(0,u.jsx)("div",{...e,className:(0,o.Z)(E.content,e.className),dangerouslySetInnerHTML:{__html:n}})}const T={announcementBar:"announcementBar_mb4j",announcementBarPlaceholder:"announcementBarPlaceholder_vyr4",announcementBarClose:"announcementBarClose_gvF7",announcementBarContent:"announcementBarContent_xLdY"};function j(){const{announcementBar:e}=(0,w.L)(),{isActive:t,close:n}=(0,k.n)();if(!t)return null;const{backgroundColor:r,textColor:o,isCloseable:a}=e;return(0,u.jsxs)("div",{className:T.announcementBar,style:{backgroundColor:r,color:o},role:"banner",children:[a&&(0,u.jsx)("div",{className:T.announcementBarPlaceholder}),(0,u.jsx)(C,{className:T.announcementBarContent}),a&&(0,u.jsx)(_,{onClick:n,className:T.announcementBarClose})]})}var N=n(3163),P=n(2466);var A=n(902),L=n(3102);const O=r.createContext(null);function R(e){let{children:t}=e;const n=function(){const e=(0,N.e)(),t=(0,L.HY)(),[n,o]=(0,r.useState)(!1),a=null!==t.component,i=(0,A.D9)(a);return(0,r.useEffect)((()=>{a&&!i&&o(!0)}),[a,i]),(0,r.useEffect)((()=>{a?e.shown||o(!0):o(!1)}),[e.shown,a]),(0,r.useMemo)((()=>[n,o]),[n])}();return(0,u.jsx)(O.Provider,{value:n,children:t})}function I(e){if(e.component){const t=e.component;return(0,u.jsx)(t,{...e.props})}}function F(){const e=(0,r.useContext)(O);if(!e)throw new A.i6("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,o=(0,r.useCallback)((()=>n(!1)),[n]),a=(0,L.HY)();return(0,r.useMemo)((()=>({shown:t,hide:o,content:I(a)})),[o,a,t])}function D(e){let{header:t,primaryMenu:n,secondaryMenu:r}=e;const{shown:a}=F();return(0,u.jsxs)("div",{className:"navbar-sidebar",children:[t,(0,u.jsxs)("div",{className:(0,o.Z)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":a}),children:[(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:n}),(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:r})]})]})}var M=n(2949),z=n(2389);function B(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"})})}function $(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"})})}const U={toggle:"toggle_vylO",toggleButton:"toggleButton_gllP",darkToggleIcon:"darkToggleIcon_wfgR",lightToggleIcon:"lightToggleIcon_pyhR",toggleButtonDisabled:"toggleButtonDisabled_aARS"};function Z(e){let{className:t,buttonClassName:n,value:r,onChange:a}=e;const i=(0,z.Z)(),s=(0,l.I)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===r?(0,l.I)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,l.I)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return(0,u.jsx)("div",{className:(0,o.Z)(U.toggle,t),children:(0,u.jsxs)("button",{className:(0,o.Z)("clean-btn",U.toggleButton,!i&&U.toggleButtonDisabled,n),type:"button",onClick:()=>a("dark"===r?"light":"dark"),disabled:!i,title:s,"aria-label":s,"aria-live":"polite",children:[(0,u.jsx)(B,{className:(0,o.Z)(U.toggleIcon,U.lightToggleIcon)}),(0,u.jsx)($,{className:(0,o.Z)(U.toggleIcon,U.darkToggleIcon)})]})})}const H=r.memo(Z),V={darkNavbarColorModeToggle:"darkNavbarColorModeToggle_X3D1"};function W(e){let{className:t}=e;const n=(0,w.L)().navbar.style,r=(0,w.L)().colorMode.disableSwitch,{colorMode:o,setColorMode:a}=(0,M.I)();return r?null:(0,u.jsx)(H,{className:t,buttonClassName:"dark"===n?V.darkNavbarColorModeToggle:void 0,value:o,onChange:a})}var G=n(1327);function q(){return(0,u.jsx)(G.Z,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}function Y(){const e=(0,N.e)();return(0,u.jsx)("button",{type:"button","aria-label":(0,l.I)({id:"theme.docs.sidebar.closeSidebarButtonAriaLabel",message:"Close navigation bar",description:"The ARIA label for close button of mobile sidebar"}),className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle(),children:(0,u.jsx)(x,{color:"var(--ifm-color-emphasis-600)"})})}function Q(){return(0,u.jsxs)("div",{className:"navbar-sidebar__brand",children:[(0,u.jsx)(q,{}),(0,u.jsx)(W,{className:"margin-right--md"}),(0,u.jsx)(Y,{})]})}var K=n(3692),X=n(4996),J=n(3919);function ee(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}var te=n(9471);function ne(e){let{activeBasePath:t,activeBaseRegex:n,to:r,href:o,label:a,html:i,isDropdownLink:s,prependBaseUrlToHref:l,...c}=e;const d=(0,X.ZP)(r),p=(0,X.ZP)(t),f=(0,X.ZP)(o,{forcePrependBaseUrl:!0}),g=a&&o&&!(0,J.Z)(o),h=i?{dangerouslySetInnerHTML:{__html:i}}:{children:(0,u.jsxs)(u.Fragment,{children:[a,g&&(0,u.jsx)(te.Z,{...s&&{width:12,height:12}})]})};return o?(0,u.jsx)(K.Z,{href:l?f:o,...c,...h}):(0,u.jsx)(K.Z,{to:d,isNavLink:!0,...(t||n)&&{isActive:(e,t)=>n?ee(n,t.pathname):t.pathname.startsWith(p)},...c,...h})}function re(e){let{className:t,isDropdownItem:n=!1,...r}=e;const a=(0,u.jsx)(ne,{className:(0,o.Z)(n?"dropdown__link":"navbar__item navbar__link",t),isDropdownLink:n,...r});return n?(0,u.jsx)("li",{children:a}):a}function oe(e){let{className:t,isDropdownItem:n,...r}=e;return(0,u.jsx)("li",{className:"menu__list-item",children:(0,u.jsx)(ne,{className:(0,o.Z)("menu__link",t),...r})})}function ae(e){let{mobile:t=!1,position:n,...r}=e;const o=t?oe:re;return(0,u.jsx)(o,{...r,activeClassName:r.activeClassName??(t?"menu__link--active":"navbar__link--active")})}var ie=n(6043),se=n(8596),le=n(2263);const ce={dropdownNavbarItemMobile:"dropdownNavbarItemMobile_S0Fm"};function ue(e,t){return e.some((e=>function(e,t){return!!(0,se.Mg)(e.to,t)||!!ee(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t)))}function de(e){let{items:t,position:n,className:a,onClick:i,...s}=e;const l=(0,r.useRef)(null),[c,d]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{const e=e=>{l.current&&!l.current.contains(e.target)&&d(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),document.addEventListener("focusin",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e),document.removeEventListener("focusin",e)}}),[l]),(0,u.jsxs)("div",{ref:l,className:(0,o.Z)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===n,"dropdown--show":c}),children:[(0,u.jsx)(ne,{"aria-haspopup":"true","aria-expanded":c,role:"button",href:s.to?void 0:"#",className:(0,o.Z)("navbar__link",a),...s,onClick:s.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),d(!c))},children:s.children??s.label}),(0,u.jsx)("ul",{className:"dropdown__menu",children:t.map(((e,t)=>(0,r.createElement)(Ee,{isDropdownItem:!0,activeClassName:"dropdown__link--active",...e,key:t})))})]})}function pe(e){let{items:t,className:n,position:a,onClick:i,...l}=e;const c=function(){const{siteConfig:{baseUrl:e}}=(0,le.Z)(),{pathname:t}=(0,s.TH)();return t.replace(e,"/")}(),d=ue(t,c),{collapsed:p,toggleCollapsed:f,setCollapsed:g}=(0,ie.u)({initialState:()=>!d});return(0,r.useEffect)((()=>{d&&g(!d)}),[c,d,g]),(0,u.jsxs)("li",{className:(0,o.Z)("menu__list-item",{"menu__list-item--collapsed":p}),children:[(0,u.jsx)(ne,{role:"button",className:(0,o.Z)(ce.dropdownNavbarItemMobile,"menu__link menu__link--sublist menu__link--sublist-caret",n),...l,onClick:e=>{e.preventDefault(),f()},children:l.children??l.label}),(0,u.jsx)(ie.z,{lazy:!0,as:"ul",className:"menu__list",collapsed:p,children:t.map(((e,t)=>(0,r.createElement)(Ee,{mobile:!0,isDropdownItem:!0,onClick:i,activeClassName:"menu__link--active",...e,key:t})))})]})}function fe(e){let{mobile:t=!1,...n}=e;const r=t?pe:de;return(0,u.jsx)(r,{...n})}var ge=n(4711);function he(e){let{width:t=20,height:n=20,...r}=e;return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:t,height:n,"aria-hidden":!0,...r,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"})})}const me="iconLanguage_nlXk";var be=n(1875);const ye={navbarSearchContainer:"navbarSearchContainer_Bca1"};function ve(e){let{children:t,className:n}=e;return(0,u.jsx)("div",{className:(0,o.Z)(n,ye.navbarSearchContainer),children:t})}var we=n(143),ke=n(9690);var xe=n(298);function Se(e,t){return t.alternateDocVersions[e.name]??function(e){return e.docs.find((t=>t.id===e.mainDocId))}(e)}const _e={default:ae,localeDropdown:function(e){let{mobile:t,dropdownItemsBefore:n,dropdownItemsAfter:r,queryString:o="",...a}=e;const{i18n:{currentLocale:i,locales:c,localeConfigs:d}}=(0,le.Z)(),p=(0,ge.l)(),{search:f,hash:g}=(0,s.TH)(),h=[...n,...c.map((e=>{const n=`${`pathname://${p.createUrl({locale:e,fullyQualified:!1})}`}${f}${g}${o}`;return{label:d[e].label,lang:d[e].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:e===i?t?"menu__link--active":"dropdown__link--active":""}})),...r],m=t?(0,l.I)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):d[i].label;return(0,u.jsx)(fe,{...a,mobile:t,label:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(he,{className:me}),m]}),items:h})},search:function(e){let{mobile:t,className:n}=e;return t?null:(0,u.jsx)(ve,{className:n,children:(0,u.jsx)(be.Z,{})})},dropdown:fe,html:function(e){let{value:t,className:n,mobile:r=!1,isDropdownItem:a=!1}=e;const i=a?"li":"div";return(0,u.jsx)(i,{className:(0,o.Z)({navbar__item:!r&&!a,"menu__list-item":r},n),dangerouslySetInnerHTML:{__html:t}})},doc:function(e){let{docId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,we.Iw)(r),i=(0,ke.vY)(t,r),s=a?.path===i?.path;return null===i||i.unlisted&&!s?null:(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>s||!!a?.sidebar&&a.sidebar===i.sidebar,label:n??i.id,to:i.path})},docSidebar:function(e){let{sidebarId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,we.Iw)(r),i=(0,ke.oz)(t,r).link;if(!i)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${t}" doesn't have anything to be linked to.`);return(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>a?.sidebar===t,label:n??i.label,to:i.path})},docsVersion:function(e){let{label:t,to:n,docsPluginId:r,...o}=e;const a=(0,ke.lO)(r)[0],i=t??a.label,s=n??(e=>e.docs.find((t=>t.id===e.mainDocId)))(a).path;return(0,u.jsx)(ae,{...o,label:i,to:s})},docsVersionDropdown:function(e){let{mobile:t,docsPluginId:n,dropdownActiveClassDisabled:r,dropdownItemsBefore:o,dropdownItemsAfter:a,...i}=e;const{search:c,hash:d}=(0,s.TH)(),p=(0,we.Iw)(n),f=(0,we.gB)(n),{savePreferredVersionName:g}=(0,xe.J)(n),h=[...o,...f.map((function(e){const t=Se(e,p);return{label:e.label,to:`${t.path}${c}${d}`,isActive:()=>e===p.activeVersion,onClick:()=>g(e.name)}})),...a],m=(0,ke.lO)(n)[0],b=t&&h.length>1?(0,l.I)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):m.label,y=t&&h.length>1?void 0:Se(m,p).path;return h.length<=1?(0,u.jsx)(ae,{...i,mobile:t,label:b,to:y,isActive:r?()=>!1:void 0}):(0,u.jsx)(fe,{...i,mobile:t,label:b,to:y,items:h,isActive:r?()=>!1:void 0})}};function Ee(e){let{type:t,...n}=e;const r=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(t,n),o=_e[r];if(!o)throw new Error(`No NavbarItem component found for type "${t}".`);return(0,u.jsx)(o,{...n})}function Ce(){const e=(0,N.e)(),t=(0,w.L)().navbar.items;return(0,u.jsx)("ul",{className:"menu__list",children:t.map(((t,n)=>(0,r.createElement)(Ee,{mobile:!0,...t,onClick:()=>e.toggle(),key:n})))})}function Te(e){return(0,u.jsx)("button",{...e,type:"button",className:"clean-btn navbar-sidebar__back",children:(0,u.jsx)(l.Z,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)",children:"\u2190 Back to main menu"})})}function je(){const e=0===(0,w.L)().navbar.items.length,t=F();return(0,u.jsxs)(u.Fragment,{children:[!e&&(0,u.jsx)(Te,{onClick:()=>t.hide()}),t.content]})}function Ne(){const e=(0,N.e)();var t;return void 0===(t=e.shown)&&(t=!0),(0,r.useEffect)((()=>(document.body.style.overflow=t?"hidden":"visible",()=>{document.body.style.overflow="visible"})),[t]),e.shouldRender?(0,u.jsx)(D,{header:(0,u.jsx)(Q,{}),primaryMenu:(0,u.jsx)(Ce,{}),secondaryMenu:(0,u.jsx)(je,{})}):null}const Pe={navbarHideable:"navbarHideable_m1mJ",navbarHidden:"navbarHidden_jGov"};function Ae(e){return(0,u.jsx)("div",{role:"presentation",...e,className:(0,o.Z)("navbar-sidebar__backdrop",e.className)})}function Le(e){let{children:t}=e;const{navbar:{hideOnScroll:n,style:a}}=(0,w.L)(),i=(0,N.e)(),{navbarRef:s,isNavbarVisible:d}=function(e){const[t,n]=(0,r.useState)(e),o=(0,r.useRef)(!1),a=(0,r.useRef)(0),i=(0,r.useCallback)((e=>{null!==e&&(a.current=e.getBoundingClientRect().height)}),[]);return(0,P.RF)(((t,r)=>{let{scrollY:i}=t;if(!e)return;if(i=s?n(!1):i+c{if(!e)return;const r=t.location.hash;if(r?document.getElementById(r.substring(1)):void 0)return o.current=!0,void n(!1);n(!0)})),{navbarRef:i,isNavbarVisible:t}}(n);return(0,u.jsxs)("nav",{ref:s,"aria-label":(0,l.I)({id:"theme.NavBar.navAriaLabel",message:"Main",description:"The ARIA label for the main navigation"}),className:(0,o.Z)("navbar","navbar--fixed-top",n&&[Pe.navbarHideable,!d&&Pe.navbarHidden],{"navbar--dark":"dark"===a,"navbar--primary":"primary"===a,"navbar-sidebar--show":i.shown}),children:[t,(0,u.jsx)(Ae,{onClick:i.toggle}),(0,u.jsx)(Ne,{})]})}var Oe=n(8780);const Re={errorBoundaryError:"errorBoundaryError_a6uf",errorBoundaryFallback:"errorBoundaryFallback_VBag"};function Ie(e){return(0,u.jsx)("button",{type:"button",...e,children:(0,u.jsx)(l.Z,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again rendering when the React error boundary captures an error",children:"Try again"})})}function Fe(e){let{error:t}=e;const n=(0,Oe.BN)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,u.jsx)("p",{className:Re.errorBoundaryError,children:n})}class De extends r.Component{componentDidCatch(e,t){throw this.props.onError(e,t)}render(){return this.props.children}}const Me="right";function ze(e){let{width:t=30,height:n=30,className:r,...o}=e;return(0,u.jsx)("svg",{className:r,width:t,height:n,viewBox:"0 0 30 30","aria-hidden":"true",...o,children:(0,u.jsx)("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"})})}function Be(){const{toggle:e,shown:t}=(0,N.e)();return(0,u.jsx)("button",{onClick:e,"aria-label":(0,l.I)({id:"theme.docs.sidebar.toggleSidebarButtonAriaLabel",message:"Toggle navigation bar",description:"The ARIA label for hamburger menu button of mobile navigation"}),"aria-expanded":t,className:"navbar__toggle clean-btn",type:"button",children:(0,u.jsx)(ze,{})})}const $e={colorModeToggle:"colorModeToggle_DEke"};function Ue(e){let{items:t}=e;return(0,u.jsx)(u.Fragment,{children:t.map(((e,t)=>(0,u.jsx)(De,{onError:t=>new Error(`A theme navbar item failed to render.\nPlease double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:\n${JSON.stringify(e,null,2)}`,{cause:t}),children:(0,u.jsx)(Ee,{...e})},t)))})}function Ze(e){let{left:t,right:n}=e;return(0,u.jsxs)("div",{className:"navbar__inner",children:[(0,u.jsx)("div",{className:"navbar__items",children:t}),(0,u.jsx)("div",{className:"navbar__items navbar__items--right",children:n})]})}function He(){const e=(0,N.e)(),t=(0,w.L)().navbar.items,[n,r]=function(e){function t(e){return"left"===(e.position??Me)}return[e.filter(t),e.filter((e=>!t(e)))]}(t),o=t.find((e=>"search"===e.type));return(0,u.jsx)(Ze,{left:(0,u.jsxs)(u.Fragment,{children:[!e.disabled&&(0,u.jsx)(Be,{}),(0,u.jsx)(q,{}),(0,u.jsx)(Ue,{items:n})]}),right:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(Ue,{items:r}),(0,u.jsx)(W,{className:$e.colorModeToggle}),!o&&(0,u.jsx)(ve,{children:(0,u.jsx)(be.Z,{})})]})})}function Ve(){return(0,u.jsx)(Le,{children:(0,u.jsx)(He,{})})}function We(e){let{item:t}=e;const{to:n,href:r,label:o,prependBaseUrlToHref:a,...i}=t,s=(0,X.ZP)(n),l=(0,X.ZP)(r,{forcePrependBaseUrl:!0});return(0,u.jsxs)(K.Z,{className:"footer__link-item",...r?{href:a?l:r}:{to:s},...i,children:[o,r&&!(0,J.Z)(r)&&(0,u.jsx)(te.Z,{})]})}function Ge(e){let{item:t}=e;return t.html?(0,u.jsx)("li",{className:"footer__item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)("li",{className:"footer__item",children:(0,u.jsx)(We,{item:t})},t.href??t.to)}function qe(e){let{column:t}=e;return(0,u.jsxs)("div",{className:"col footer__col",children:[(0,u.jsx)("div",{className:"footer__title",children:t.title}),(0,u.jsx)("ul",{className:"footer__items clean-list",children:t.items.map(((e,t)=>(0,u.jsx)(Ge,{item:e},t)))})]})}function Ye(e){let{columns:t}=e;return(0,u.jsx)("div",{className:"row footer__links",children:t.map(((e,t)=>(0,u.jsx)(qe,{column:e},t)))})}function Qe(){return(0,u.jsx)("span",{className:"footer__link-separator",children:"\xb7"})}function Ke(e){let{item:t}=e;return t.html?(0,u.jsx)("span",{className:"footer__link-item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)(We,{item:t})}function Xe(e){let{links:t}=e;return(0,u.jsx)("div",{className:"footer__links text--center",children:(0,u.jsx)("div",{className:"footer__links",children:t.map(((e,n)=>(0,u.jsxs)(r.Fragment,{children:[(0,u.jsx)(Ke,{item:e}),t.length!==n+1&&(0,u.jsx)(Qe,{})]},n)))})})}function Je(e){let{links:t}=e;return function(e){return"title"in e[0]}(t)?(0,u.jsx)(Ye,{columns:t}):(0,u.jsx)(Xe,{links:t})}var et=n(9965);const tt={footerLogoLink:"footerLogoLink_BH7S"};function nt(e){let{logo:t}=e;const{withBaseUrl:n}=(0,X.Cg)(),r={light:n(t.src),dark:n(t.srcDark??t.src)};return(0,u.jsx)(et.Z,{className:(0,o.Z)("footer__logo",t.className),alt:t.alt,sources:r,width:t.width,height:t.height,style:t.style})}function rt(e){let{logo:t}=e;return t.href?(0,u.jsx)(K.Z,{href:t.href,className:tt.footerLogoLink,target:t.target,children:(0,u.jsx)(nt,{logo:t})}):(0,u.jsx)(nt,{logo:t})}function ot(e){let{copyright:t}=e;return(0,u.jsx)("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:t}})}function at(e){let{style:t,links:n,logo:r,copyright:a}=e;return(0,u.jsx)("footer",{className:(0,o.Z)("footer",{"footer--dark":"dark"===t}),children:(0,u.jsxs)("div",{className:"container container-fluid",children:[n,(r||a)&&(0,u.jsxs)("div",{className:"footer__bottom text--center",children:[r&&(0,u.jsx)("div",{className:"margin-bottom--sm",children:r}),a]})]})})}function it(){const{footer:e}=(0,w.L)();if(!e)return null;const{copyright:t,links:n,logo:r,style:o}=e;return(0,u.jsx)(at,{style:o,links:n&&n.length>0&&(0,u.jsx)(Je,{links:n}),logo:r&&(0,u.jsx)(rt,{logo:r}),copyright:t&&(0,u.jsx)(ot,{copyright:t})})}const st=r.memo(it),lt=(0,A.Qc)([M.S,k.p,P.OC,xe.L5,i.VC,function(e){let{children:t}=e;return(0,u.jsx)(L.n2,{children:(0,u.jsx)(N.M,{children:(0,u.jsx)(R,{children:t})})})}]);function ct(e){let{children:t}=e;return(0,u.jsx)(lt,{children:t})}var ut=n(2503);function dt(e){let{error:t,tryAgain:n}=e;return(0,u.jsx)("main",{className:"container margin-vert--xl",children:(0,u.jsx)("div",{className:"row",children:(0,u.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,u.jsx)(ut.Z,{as:"h1",className:"hero__title",children:(0,u.jsx)(l.Z,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed",children:"This page crashed."})}),(0,u.jsx)("div",{className:"margin-vert--lg",children:(0,u.jsx)(Ie,{onClick:n,className:"button button--primary shadow--lw"})}),(0,u.jsx)("hr",{}),(0,u.jsx)("div",{className:"margin-vert--md",children:(0,u.jsx)(Fe,{error:t})})]})})})}const pt={mainWrapper:"mainWrapper_z2l0"};function ft(e){const{children:t,noFooter:n,wrapperClassName:r,title:s,description:l}=e;return(0,b.t)(),(0,u.jsxs)(ct,{children:[(0,u.jsx)(i.d,{title:s,description:l}),(0,u.jsx)(v,{}),(0,u.jsx)(j,{}),(0,u.jsx)(Ve,{}),(0,u.jsx)("div",{id:d,className:(0,o.Z)(m.k.wrapper.main,pt.mainWrapper,r),children:(0,u.jsx)(a.Z,{fallback:e=>(0,u.jsx)(dt,{...e}),children:t})}),!n&&(0,u.jsx)(st,{})]})}},1327:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});n(7294);var r=n(3692),o=n(4996),a=n(2263),i=n(6668),s=n(9965),l=n(5893);function c(e){let{logo:t,alt:n,imageClassName:r}=e;const a={light:(0,o.ZP)(t.src),dark:(0,o.ZP)(t.srcDark||t.src)},i=(0,l.jsx)(s.Z,{className:t.className,sources:a,height:t.height,width:t.width,alt:n,style:t.style});return r?(0,l.jsx)("div",{className:r,children:i}):i}function u(e){const{siteConfig:{title:t}}=(0,a.Z)(),{navbar:{title:n,logo:s}}=(0,i.L)(),{imageClassName:u,titleClassName:d,...p}=e,f=(0,o.ZP)(s?.href||"/"),g=n?"":t,h=s?.alt??g;return(0,l.jsxs)(r.Z,{to:f,...p,...s?.target&&{target:s.target},children:[s&&(0,l.jsx)(c,{logo:s,alt:h,imageClassName:u}),null!=n&&(0,l.jsx)("b",{className:d,children:n})]})}},197:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(7294);var r=n(5742),o=n(5893);function a(e){let{locale:t,version:n,tag:a}=e;const i=t;return(0,o.jsxs)(r.Z,{children:[t&&(0,o.jsx)("meta",{name:"docusaurus_locale",content:t}),n&&(0,o.jsx)("meta",{name:"docusaurus_version",content:n}),a&&(0,o.jsx)("meta",{name:"docusaurus_tag",content:a}),i&&(0,o.jsx)("meta",{name:"docsearch:language",content:i}),n&&(0,o.jsx)("meta",{name:"docsearch:version",content:n}),a&&(0,o.jsx)("meta",{name:"docsearch:docusaurus_tag",content:a})]})}},9965:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});var r=n(7294),o=n(788),a=n(2389),i=n(2949);const s={themedComponent:"themedComponent_mlkZ","themedComponent--light":"themedComponent--light_NVdE","themedComponent--dark":"themedComponent--dark_xIcU"};var l=n(5893);function c(e){let{className:t,children:n}=e;const c=(0,a.Z)(),{colorMode:u}=(0,i.I)();return(0,l.jsx)(l.Fragment,{children:(c?"dark"===u?["dark"]:["light"]:["light","dark"]).map((e=>{const a=n({theme:e,className:(0,o.Z)(t,s.themedComponent,s[`themedComponent--${e}`])});return(0,l.jsx)(r.Fragment,{children:a},e)}))})}function u(e){const{sources:t,className:n,alt:r,...o}=e;return(0,l.jsx)(c,{className:n,children:e=>{let{theme:n,className:a}=e;return(0,l.jsx)("img",{src:t[n],alt:r,className:a,...o})}})}},6043:(e,t,n)=>{"use strict";n.d(t,{u:()=>c,z:()=>b});var r=n(7294),o=n(412),a=n(469),i=n(1442),s=n(5893);const l="ease-in-out";function c(e){let{initialState:t}=e;const[n,o]=(0,r.useState)(t??!1),a=(0,r.useCallback)((()=>{o((e=>!e))}),[]);return{collapsed:n,setCollapsed:o,toggleCollapsed:a}}const u={display:"none",overflow:"hidden",height:"0px"},d={display:"block",overflow:"visible",height:"auto"};function p(e,t){const n=t?u:d;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function f(e){let{collapsibleRef:t,collapsed:n,animation:o}=e;const a=(0,r.useRef)(!1);(0,r.useEffect)((()=>{const e=t.current;function r(){const t=e.scrollHeight,n=o?.duration??function(e){if((0,i.n)())return 1;const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(t);return{transition:`height ${n}ms ${o?.easing??l}`,height:`${t}px`}}function s(){const t=r();e.style.transition=t.transition,e.style.height=t.height}if(!a.current)return p(e,n),void(a.current=!0);return e.style.willChange="height",function(){const t=requestAnimationFrame((()=>{n?(s(),requestAnimationFrame((()=>{e.style.height=u.height,e.style.overflow=u.overflow}))):(e.style.display="block",requestAnimationFrame((()=>{s()})))}));return()=>cancelAnimationFrame(t)}()}),[t,n,o])}function g(e){if(!o.Z.canUseDOM)return e?u:d}function h(e){let{as:t="div",collapsed:n,children:o,animation:a,onCollapseTransitionEnd:i,className:l,disableSSRStyle:c}=e;const u=(0,r.useRef)(null);return f({collapsibleRef:u,collapsed:n,animation:a}),(0,s.jsx)(t,{ref:u,style:c?void 0:g(n),onTransitionEnd:e=>{"height"===e.propertyName&&(p(u.current,n),i?.(n))},className:l,children:o})}function m(e){let{collapsed:t,...n}=e;const[o,i]=(0,r.useState)(!t),[l,c]=(0,r.useState)(t);return(0,a.Z)((()=>{t||i(!0)}),[t]),(0,a.Z)((()=>{o&&c(t)}),[o,t]),o?(0,s.jsx)(h,{...n,collapsed:l}):null}function b(e){let{lazy:t,...n}=e;const r=t?m:h;return(0,s.jsx)(r,{...n})}},9689:(e,t,n)=>{"use strict";n.d(t,{n:()=>h,p:()=>g});var r=n(7294),o=n(2389),a=n(812),i=n(902),s=n(6668),l=n(5893);const c=(0,a.WA)("docusaurus.announcement.dismiss"),u=(0,a.WA)("docusaurus.announcement.id"),d=()=>"true"===c.get(),p=e=>c.set(String(e)),f=r.createContext(null);function g(e){let{children:t}=e;const n=function(){const{announcementBar:e}=(0,s.L)(),t=(0,o.Z)(),[n,a]=(0,r.useState)((()=>!!t&&d()));(0,r.useEffect)((()=>{a(d())}),[]);const i=(0,r.useCallback)((()=>{p(!0),a(!0)}),[]);return(0,r.useEffect)((()=>{if(!e)return;const{id:t}=e;let n=u.get();"annoucement-bar"===n&&(n="announcement-bar");const r=t!==n;u.set(t),r&&p(!1),!r&&d()||a(!1)}),[e]),(0,r.useMemo)((()=>({isActive:!!e&&!n,close:i})),[e,n,i])}();return(0,l.jsx)(f.Provider,{value:n,children:t})}function h(){const e=(0,r.useContext)(f);if(!e)throw new i.i6("AnnouncementBarProvider");return e}},2949:(e,t,n)=>{"use strict";n.d(t,{I:()=>b,S:()=>m});var r=n(7294),o=n(412),a=n(902),i=n(812),s=n(6668),l=n(5893);const c=r.createContext(void 0),u="theme",d=(0,i.WA)(u),p={light:"light",dark:"dark"},f=e=>e===p.dark?p.dark:p.light,g=e=>o.Z.canUseDOM?f(document.documentElement.getAttribute("data-theme")):f(e),h=e=>{d.set(f(e))};function m(e){let{children:t}=e;const n=function(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,s.L)(),[o,a]=(0,r.useState)(g(e));(0,r.useEffect)((()=>{t&&d.del()}),[t]);const i=(0,r.useCallback)((function(t,r){void 0===r&&(r={});const{persist:o=!0}=r;t?(a(t),o&&h(t)):(a(n?window.matchMedia("(prefers-color-scheme: dark)").matches?p.dark:p.light:e),d.del())}),[n,e]);(0,r.useEffect)((()=>{document.documentElement.setAttribute("data-theme",f(o))}),[o]),(0,r.useEffect)((()=>{if(t)return;const e=e=>{if(e.key!==u)return;const t=d.get();null!==t&&i(f(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)}),[t,i]);const l=(0,r.useRef)(!1);return(0,r.useEffect)((()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),r=()=>{window.matchMedia("print").matches||l.current?l.current=window.matchMedia("print").matches:i(null)};return e.addListener(r),()=>e.removeListener(r)}),[i,t,n]),(0,r.useMemo)((()=>({colorMode:o,setColorMode:i,get isDarkTheme(){return o===p.dark},setLightTheme(){i(p.light)},setDarkTheme(){i(p.dark)}})),[o,i])}();return(0,l.jsx)(c.Provider,{value:n,children:t})}function b(){const e=(0,r.useContext)(c);if(null==e)throw new a.i6("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},3163:(e,t,n)=>{"use strict";n.d(t,{M:()=>p,e:()=>f});var r=n(7294),o=n(3102),a=n(7524),i=n(1980),s=n(6668),l=n(902),c=n(5893);const u=r.createContext(void 0);function d(){const e=function(){const e=(0,o.HY)(),{items:t}=(0,s.L)().navbar;return 0===t.length&&!e.component}(),t=(0,a.i)(),n=!e&&"mobile"===t,[l,c]=(0,r.useState)(!1);(0,i.Rb)((()=>{if(l)return c(!1),!1}));const u=(0,r.useCallback)((()=>{c((e=>!e))}),[]);return(0,r.useEffect)((()=>{"desktop"===t&&c(!1)}),[t]),(0,r.useMemo)((()=>({disabled:e,shouldRender:n,toggle:u,shown:l})),[e,n,u,l])}function p(e){let{children:t}=e;const n=d();return(0,c.jsx)(u.Provider,{value:n,children:t})}function f(){const e=r.useContext(u);if(void 0===e)throw new l.i6("NavbarMobileSidebarProvider");return e}},3102:(e,t,n)=>{"use strict";n.d(t,{HY:()=>l,Zo:()=>c,n2:()=>s});var r=n(7294),o=n(902),a=n(5893);const i=r.createContext(null);function s(e){let{children:t}=e;const n=(0,r.useState)({component:null,props:null});return(0,a.jsx)(i.Provider,{value:n,children:t})}function l(){const e=(0,r.useContext)(i);if(!e)throw new o.i6("NavbarSecondaryMenuContentProvider");return e[0]}function c(e){let{component:t,props:n}=e;const a=(0,r.useContext)(i);if(!a)throw new o.i6("NavbarSecondaryMenuContentProvider");const[,s]=a,l=(0,o.Ql)(n);return(0,r.useEffect)((()=>{s({component:t,props:l})}),[s,t,l]),(0,r.useEffect)((()=>()=>s({component:null,props:null})),[s]),null}},9727:(e,t,n)=>{"use strict";n.d(t,{h:()=>o,t:()=>a});var r=n(7294);const o="navigation-with-keyboard";function a(){(0,r.useEffect)((()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(o),"mousedown"===e.type&&document.body.classList.remove(o)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(o),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}}),[])}},7524:(e,t,n)=>{"use strict";n.d(t,{i:()=>s});var r=n(7294),o=n(412);const a={desktop:"desktop",mobile:"mobile",ssr:"ssr"},i=996;function s(e){let{desktopBreakpoint:t=i}=void 0===e?{}:e;const[n,s]=(0,r.useState)((()=>"ssr"));return(0,r.useEffect)((()=>{function e(){s(function(e){if(!o.Z.canUseDOM)throw new Error("getWindowSize() should only be called after React hydration");return window.innerWidth>e?a.desktop:a.mobile}(t))}return e(),window.addEventListener("resize",e),()=>{window.removeEventListener("resize",e)}}),[t]),n}},5281:(e,t,n)=>{"use strict";n.d(t,{k:()=>r});const r={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",blogAuthorsListPage:"blog-authors-list-page",blogAuthorsPostsPage:"blog-authors-posts-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",unlistedBanner:"theme-unlisted-banner",draftBanner:"theme-draft-banner",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{blogFooterTagsRow:"theme-blog-footer-tags-row",blogFooterEditMetaRow:"theme-blog-footer-edit-meta-row"},pages:{pageFooterEditMetaRow:"theme-pages-footer-edit-meta-row"}}},1442:(e,t,n)=>{"use strict";function r(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}n.d(t,{n:()=>r})},1980:(e,t,n)=>{"use strict";n.d(t,{Rb:()=>i,_X:()=>l});var r=n(7294),o=n(6550),a=n(902);function i(e){!function(e){const t=(0,o.k6)(),n=(0,a.zX)(e);(0,r.useEffect)((()=>t.block(((e,t)=>n(e,t)))),[t,n])}(((t,n)=>{if("POP"===n)return e(t,n)}))}function s(e){const t=(0,o.k6)();return(0,r.useSyncExternalStore)(t.listen,(()=>e(t)),(()=>e(t)))}function l(e){return s((t=>null===e?null:new URLSearchParams(t.location.search).get(e)))}},7392:(e,t,n)=>{"use strict";function r(e,t){return void 0===t&&(t=(e,t)=>e===t),e.filter(((n,r)=>e.findIndex((e=>t(e,n)))!==r))}function o(e){return Array.from(new Set(e))}function a(e,t){const n={};let r=0;for(const o of e){const e=t(o,r);n[e]??=[],n[e].push(o),r+=1}return n}n.d(t,{jj:()=>o,lx:()=>r,vM:()=>a})},8264:(e,t,n)=>{"use strict";n.d(t,{FG:()=>p,d:()=>u,VC:()=>f});var r=n(7294),o=n(788),a=n(5742),i=n(5102),s=n(4996),l=n(2263);var c=n(5893);function u(e){let{title:t,description:n,keywords:r,image:o,children:i}=e;const u=function(e){const{siteConfig:t}=(0,l.Z)(),{title:n,titleDelimiter:r}=t;return e?.trim().length?`${e.trim()} ${r} ${n}`:n}(t),{withBaseUrl:d}=(0,s.Cg)(),p=o?d(o,{absolute:!0}):void 0;return(0,c.jsxs)(a.Z,{children:[t&&(0,c.jsx)("title",{children:u}),t&&(0,c.jsx)("meta",{property:"og:title",content:u}),n&&(0,c.jsx)("meta",{name:"description",content:n}),n&&(0,c.jsx)("meta",{property:"og:description",content:n}),r&&(0,c.jsx)("meta",{name:"keywords",content:Array.isArray(r)?r.join(","):r}),p&&(0,c.jsx)("meta",{property:"og:image",content:p}),p&&(0,c.jsx)("meta",{name:"twitter:image",content:p}),i]})}const d=r.createContext(void 0);function p(e){let{className:t,children:n}=e;const i=r.useContext(d),s=(0,o.Z)(i,t);return(0,c.jsxs)(d.Provider,{value:s,children:[(0,c.jsx)(a.Z,{children:(0,c.jsx)("html",{className:s})}),n]})}function f(e){let{children:t}=e;const n=(0,i.Z)(),r=`plugin-${n.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const a=`plugin-id-${n.plugin.id}`;return(0,c.jsx)(p,{className:(0,o.Z)(r,a),children:t})}},902:(e,t,n)=>{"use strict";n.d(t,{D9:()=>s,Qc:()=>u,Ql:()=>c,i6:()=>l,zX:()=>i});var r=n(7294),o=n(469),a=n(5893);function i(e){const t=(0,r.useRef)(e);return(0,o.Z)((()=>{t.current=e}),[e]),(0,r.useCallback)((function(){return t.current(...arguments)}),[])}function s(e){const t=(0,r.useRef)();return(0,o.Z)((()=>{t.current=e})),t.current}class l extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?\w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function c(e){const t=Object.entries(e);return t.sort(((e,t)=>e[0].localeCompare(t[0]))),(0,r.useMemo)((()=>e),t.flat())}function u(e){return t=>{let{children:n}=t;return(0,a.jsx)(a.Fragment,{children:e.reduceRight(((e,t)=>(0,a.jsx)(t,{children:e})),n)})}}},8596:(e,t,n)=>{"use strict";n.d(t,{Mg:()=>i,Ns:()=>s});var r=n(7294),o=n(723),a=n(2263);function i(e,t){const n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function s(){const{baseUrl:e}=(0,a.Z)().siteConfig;return(0,r.useMemo)((()=>function(e){let{baseUrl:t,routes:n}=e;function r(e){return e.path===t&&!0===e.exact}function o(e){return e.path===t&&!e.exact}return function e(t){if(0===t.length)return;return t.find(r)||e(t.filter(o).flatMap((e=>e.routes??[])))}(n)}({routes:o.Z,baseUrl:e})),[e])}},2466:(e,t,n)=>{"use strict";n.d(t,{Ct:()=>h,OC:()=>u,RF:()=>f,o5:()=>g});var r=n(7294),o=n(412),a=n(2389),i=n(469),s=n(902),l=n(5893);const c=r.createContext(void 0);function u(e){let{children:t}=e;const n=function(){const e=(0,r.useRef)(!0);return(0,r.useMemo)((()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}})),[])}();return(0,l.jsx)(c.Provider,{value:n,children:t})}function d(){const e=(0,r.useContext)(c);if(null==e)throw new s.i6("ScrollControllerProvider");return e}const p=()=>o.Z.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function f(e,t){void 0===t&&(t=[]);const{scrollEventsEnabledRef:n}=d(),o=(0,r.useRef)(p()),a=(0,s.zX)(e);(0,r.useEffect)((()=>{const e=()=>{if(!n.current)return;const e=p();a(e,o.current),o.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)}),[a,n,...t])}function g(){const e=d(),t=function(){const e=(0,r.useRef)({elem:null,top:0}),t=(0,r.useCallback)((t=>{e.current={elem:t,top:t.getBoundingClientRect().top}}),[]),n=(0,r.useCallback)((()=>{const{current:{elem:t,top:n}}=e;if(!t)return{restored:!1};const r=t.getBoundingClientRect().top-n;return r&&window.scrollBy({left:0,top:r}),e.current={elem:null,top:0},{restored:0!==r}}),[]);return(0,r.useMemo)((()=>({save:t,restore:n})),[n,t])}(),n=(0,r.useRef)(void 0),o=(0,r.useCallback)((r=>{t.save(r),e.disableScrollEvents(),n.current=()=>{const{restored:r}=t.restore();if(n.current=void 0,r){const t=()=>{e.enableScrollEvents(),window.removeEventListener("scroll",t)};window.addEventListener("scroll",t)}else e.enableScrollEvents()}}),[e,t]);return(0,i.Z)((()=>{queueMicrotask((()=>n.current?.()))})),{blockElementScrollPositionUntilNextRender:o}}function h(){const e=(0,r.useRef)(null),t=(0,a.Z)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function r(){const o=document.documentElement.scrollTop;(n&&o>e||!n&&ot&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>e.current?.()}}},812:(e,t,n)=>{"use strict";n.d(t,{WA:()=>u,Nk:()=>d});var r=n(7294);const o=JSON.parse('{"d":"localStorage","u":""}'),a=o.d;function i(e){let{key:t,oldValue:n,newValue:r,storage:o}=e;if(n===r)return;const a=document.createEvent("StorageEvent");a.initStorageEvent("storage",!1,!1,t,n,r,window.location.href,o),window.dispatchEvent(a)}function s(e){if(void 0===e&&(e=a),"undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,l||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),l=!0),null}var t}let l=!1;const c={get:()=>null,set:()=>{},del:()=>{},listen:()=>()=>{}};function u(e,t){const n=`${e}${o.u}`;if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t,listen:t}}(n);const r=s(t?.persistence);return null===r?c:{get:()=>{try{return r.getItem(n)}catch(e){return console.error(`Docusaurus storage error, can't get key=${n}`,e),null}},set:e=>{try{const t=r.getItem(n);r.setItem(n,e),i({key:n,oldValue:t,newValue:e,storage:r})}catch(t){console.error(`Docusaurus storage error, can't set ${n}=${e}`,t)}},del:()=>{try{const e=r.getItem(n);r.removeItem(n),i({key:n,oldValue:e,newValue:null,storage:r})}catch(e){console.error(`Docusaurus storage error, can't delete key=${n}`,e)}},listen:e=>{try{const t=t=>{t.storageArea===r&&t.key===n&&e(t)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)}catch(t){return console.error(`Docusaurus storage error, can't listen for changes of key=${n}`,t),()=>{}}}}}function d(e,t){const n=(0,r.useRef)((()=>null===e?c:u(e,t))).current(),o=(0,r.useCallback)((e=>"undefined"==typeof window?()=>{}:n.listen(e)),[n]);return[(0,r.useSyncExternalStore)(o,(()=>"undefined"==typeof window?null:n.get()),(()=>null)),n]}},4711:(e,t,n)=>{"use strict";n.d(t,{l:()=>i});var r=n(2263),o=n(6550),a=n(8780);function i(){const{siteConfig:{baseUrl:e,url:t,trailingSlash:n},i18n:{defaultLocale:i,currentLocale:s}}=(0,r.Z)(),{pathname:l}=(0,o.TH)(),c=(0,a.Do)(l,{trailingSlash:n,baseUrl:e}),u=s===i?e:e.replace(`/${s}/`,"/"),d=c.replace(e,"");return{createUrl:function(e){let{locale:n,fullyQualified:r}=e;return`${r?t:""}${function(e){return e===i?`${u}`:`${u}${e}/`}(n)}${d}`}}}},5936:(e,t,n)=>{"use strict";n.d(t,{S:()=>i});var r=n(7294),o=n(6550),a=n(902);function i(e){const t=(0,o.TH)(),n=(0,a.D9)(t),i=(0,a.zX)(e);(0,r.useEffect)((()=>{n&&t!==n&&i({location:t,previousLocation:n})}),[i,t,n])}},6668:(e,t,n)=>{"use strict";n.d(t,{L:()=>o});var r=n(2263);function o(){return(0,r.Z)().siteConfig.themeConfig}},8802:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addTrailingSlash=o,t.default=function(e,t){const{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[i]=e.split(/[#?]/),s="/"===i||i===r?i:(l=i,c=n,c?o(l):a(l));var l,c;return e.replace(i,s)},t.addLeadingSlash=function(e){return(0,r.addPrefix)(e,"/")},t.removeTrailingSlash=a;const r=n(5913);function o(e){return e.endsWith("/")?e:`${e}/`}function a(e){return(0,r.removeSuffix)(e,"/")}},4143:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=function e(t){if(t.cause)return[t,...e(t.cause)];return[t]}},8780:(e,t,n)=>{"use strict";t.BN=t.Do=t.uR=void 0;const r=n(7582);t.uR="__blog-post-container";var o=n(8802);Object.defineProperty(t,"Do",{enumerable:!0,get:function(){return r.__importDefault(o).default}});var a=n(5913);var i=n(4143);Object.defineProperty(t,"BN",{enumerable:!0,get:function(){return i.getErrorCausalChain}})},5913:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addPrefix=function(e,t){return e.startsWith(t)?e:`${t}${e}`},t.removeSuffix=function(e,t){if(""===t)return e;return e.endsWith(t)?e.slice(0,-t.length):e},t.addSuffix=function(e,t){return e.endsWith(t)?e:`${e}${t}`},t.removePrefix=function(e,t){return e.startsWith(t)?e.slice(t.length):e}},9318:(e,t,n)=>{"use strict";n.d(t,{lX:()=>w,q_:()=>C,ob:()=>f,PP:()=>j,Ep:()=>p});var r=n(7462);function o(e){return"/"===e.charAt(0)}function a(e,t){for(var n=t,r=n+1,o=e.length;r=0;p--){var f=i[p];"."===f?a(i,p):".."===f?(a(i,p),d++):d&&(a(i,p),d--)}if(!c)for(;d--;d)i.unshift("..");!c||""===i[0]||i[0]&&o(i[0])||i.unshift("");var g=i.join("/");return n&&"/"!==g.substr(-1)&&(g+="/"),g};var s=n(8776);function l(e){return"/"===e.charAt(0)?e:"/"+e}function c(e){return"/"===e.charAt(0)?e.substr(1):e}function u(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function p(e){var t=e.pathname,n=e.search,r=e.hash,o=t||"/";return n&&"?"!==n&&(o+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(o+="#"===r.charAt(0)?r:"#"+r),o}function f(e,t,n,o){var a;"string"==typeof e?(a=function(e){var t=e||"/",n="",r="",o=t.indexOf("#");-1!==o&&(r=t.substr(o),t=t.substr(0,o));var a=t.indexOf("?");return-1!==a&&(n=t.substr(a),t=t.substr(0,a)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}}(e),a.state=t):(void 0===(a=(0,r.Z)({},e)).pathname&&(a.pathname=""),a.search?"?"!==a.search.charAt(0)&&(a.search="?"+a.search):a.search="",a.hash?"#"!==a.hash.charAt(0)&&(a.hash="#"+a.hash):a.hash="",void 0!==t&&void 0===a.state&&(a.state=t));try{a.pathname=decodeURI(a.pathname)}catch(s){throw s instanceof URIError?new URIError('Pathname "'+a.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):s}return n&&(a.key=n),o?a.pathname?"/"!==a.pathname.charAt(0)&&(a.pathname=i(a.pathname,o.pathname)):a.pathname=o.pathname:a.pathname||(a.pathname="/"),a}function g(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,o){if(null!=e){var a="function"==typeof e?e(t,n):e;"string"==typeof a?"function"==typeof r?r(a,o):o(!0):o(!1!==a)}else o(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter((function(e){return e!==r}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),r=0;rt?n.splice(t,n.length-t,o):n.push(o),d({action:r,location:o,index:t,entries:n})}}))},replace:function(e,t){var r="REPLACE",o=f(e,t,h(),w.location);u.confirmTransitionTo(o,r,n,(function(e){e&&(w.entries[w.index]=o,d({action:r,location:o}))}))},go:v,goBack:function(){v(-1)},goForward:function(){v(1)},canGo:function(e){var t=w.index+e;return t>=0&&t{"use strict";var r=n(9864),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},a={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},s={};function l(e){return r.isMemo(e)?i:s[e.$$typeof]||o}s[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},s[r.Memo]=i;var c=Object.defineProperty,u=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,p=Object.getOwnPropertyDescriptor,f=Object.getPrototypeOf,g=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(g){var o=f(n);o&&o!==g&&e(t,o,r)}var i=u(n);d&&(i=i.concat(d(n)));for(var s=l(t),h=l(n),m=0;m{"use strict";e.exports=function(e,t,n,r,o,a,i,s){if(!e){var l;if(void 0===t)l=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,o,a,i,s],u=0;(l=new Error(t.replace(/%s/g,(function(){return c[u++]})))).name="Invariant Violation"}throw l.framesToPop=1,l}}},5826:e=>{e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},2497:(e,t,n)=>{"use strict";n.r(t)},2295:(e,t,n)=>{"use strict";n.r(t)},4865:function(e,t,n){var r,o;r=function(){var e,t,n={version:"0.2.0"},r=n.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
'};function o(e,t,n){return en?n:e}function a(e){return 100*(-1+e)}function i(e,t,n){var o;return(o="translate3d"===r.positionUsing?{transform:"translate3d("+a(e)+"%,0,0)"}:"translate"===r.positionUsing?{transform:"translate("+a(e)+"%,0)"}:{"margin-left":a(e)+"%"}).transition="all "+t+"ms "+n,o}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(r[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=o(e,r.minimum,1),n.status=1===e?null:e;var a=n.render(!t),c=a.querySelector(r.barSelector),u=r.speed,d=r.easing;return a.offsetWidth,s((function(t){""===r.positionUsing&&(r.positionUsing=n.getPositioningCSS()),l(c,i(e,u,d)),1===e?(l(a,{transition:"none",opacity:1}),a.offsetWidth,setTimeout((function(){l(a,{transition:"all "+u+"ms linear",opacity:0}),setTimeout((function(){n.remove(),t()}),u)}),u)):setTimeout(t,u)})),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout((function(){n.status&&(n.trickle(),e())}),r.trickleSpeed)};return r.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*o(Math.random()*t,.1,.95)),t=o(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*r.trickleRate)},e=0,t=0,n.promise=function(r){return r&&"resolved"!==r.state()?(0===t&&n.start(),e++,t++,r.always((function(){0==--t?(e=0,n.done()):n.set((e-t)/e)})),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=r.template;var o,i=t.querySelector(r.barSelector),s=e?"-100":a(n.status||0),c=document.querySelector(r.parent);return l(i,{transition:"all 0 linear",transform:"translate3d("+s+"%,0,0)"}),r.showSpinner||(o=t.querySelector(r.spinnerSelector))&&f(o),c!=document.body&&u(c,"nprogress-custom-parent"),c.appendChild(t),t},n.remove=function(){d(document.documentElement,"nprogress-busy"),d(document.querySelector(r.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&f(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var s=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),l=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(function(e,t){return t.toUpperCase()}))}function r(t){var n=document.body.style;if(t in n)return t;for(var r,o=e.length,a=t.charAt(0).toUpperCase()+t.slice(1);o--;)if((r=e[o]+a)in n)return r;return t}function o(e){return e=n(e),t[e]||(t[e]=r(e))}function a(e,t,n){t=o(t),e.style[t]=n}return function(e,t){var n,r,o=arguments;if(2==o.length)for(n in t)void 0!==(r=t[n])&&t.hasOwnProperty(n)&&a(e,n,r);else a(e,o[1],o[2])}}();function c(e,t){return("string"==typeof e?e:p(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=p(e),r=n+t;c(n,t)||(e.className=r.substring(1))}function d(e,t){var n,r=p(e);c(e,t)&&(n=r.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function p(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function f(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(o="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=o)},4779:(e,t,n)=>{var r=n(5826);e.exports=f,e.exports.parse=a,e.exports.compile=function(e,t){return s(a(e,t),t)},e.exports.tokensToFunction=s,e.exports.tokensToRegExp=p;var o=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function a(e,t){for(var n,r=[],a=0,i=0,s="",u=t&&t.delimiter||"/";null!=(n=o.exec(e));){var d=n[0],p=n[1],f=n.index;if(s+=e.slice(i,f),i=f+d.length,p)s+=p[1];else{var g=e[i],h=n[2],m=n[3],b=n[4],y=n[5],v=n[6],w=n[7];s&&(r.push(s),s="");var k=null!=h&&null!=g&&g!==h,x="+"===v||"*"===v,S="?"===v||"*"===v,_=n[2]||u,E=b||y;r.push({name:m||a++,prefix:h||"",delimiter:_,optional:S,repeat:x,partial:k,asterisk:!!w,pattern:E?c(E):w?".*":"[^"+l(_)+"]+?"})}}return i{!function(e){var t=[/\b(?:async|sync|yield)\*/,/\b(?:abstract|assert|async|await|break|case|catch|class|const|continue|covariant|default|deferred|do|dynamic|else|enum|export|extends|extension|external|factory|final|finally|for|get|hide|if|implements|import|in|interface|library|mixin|new|null|on|operator|part|rethrow|return|set|show|static|super|switch|sync|this|throw|try|typedef|var|void|while|with|yield)\b/],n=/(^|[^\w.])(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source,r={pattern:RegExp(n+/[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}}}};e.languages.dart=e.languages.extend("clike",{"class-name":[r,{pattern:RegExp(n+/[A-Z]\w*(?=\s+\w+\s*[;,=()])/.source),lookbehind:!0,inside:r.inside}],keyword:t,operator:/\bis!|\b(?:as|is)\b|\+\+|--|&&|\|\||<<=?|>>=?|~(?:\/=?)?|[+\-*\/%&^|=!<>]=?|\?/}),e.languages.insertBefore("dart","string",{"string-literal":{pattern:/r?(?:("""|''')[\s\S]*?\1|(["'])(?:\\.|(?!\2)[^\\\r\n])*\2(?!\2))/,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$(?:\w+|\{(?:[^{}]|\{[^{}]*\})*\})/,lookbehind:!0,inside:{punctuation:/^\$\{?|\}$/,expression:{pattern:/[\s\S]+/,inside:e.languages.dart}}},string:/[\s\S]+/}},string:void 0}),e.languages.insertBefore("dart","class-name",{metadata:{pattern:/@\w+/,alias:"function"}}),e.languages.insertBefore("dart","class-name",{generics:{pattern:/<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<[\w\s,.&?]*>)*>)*>)*>/,inside:{"class-name":r,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}}})}(Prism)},6854:()=>{!function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,o,a){if(n.language===r){var i=n.tokenStack=[];n.code=n.code.replace(o,(function(e){if("function"==typeof a&&!a(e))return e;for(var o,s=i.length;-1!==n.code.indexOf(o=t(r,s));)++s;return i[s]=e,o})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var o=0,a=Object.keys(n.tokenStack);!function i(s){for(var l=0;l=a.length);l++){var c=s[l];if("string"==typeof c||c.content&&"string"==typeof c.content){var u=a[o],d=n.tokenStack[u],p="string"==typeof c?c:c.content,f=t(r,u),g=p.indexOf(f);if(g>-1){++o;var h=p.substring(0,g),m=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=p.substring(g+f.length),y=[];h&&y.push.apply(y,i([h])),y.push(m),b&&y.push.apply(y,i([b])),"string"==typeof c?s.splice.apply(s,[l,1].concat(y)):c.content=y}}else c.content&&i(c.content)}return s}(n.tokens)}}}})}(Prism)},6046:(e,t,n)=>{var r={"./prism-dart":7065};function o(e){var t=a(e);return n(t)}function a(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}o.keys=function(){return Object.keys(r)},o.resolve=a,e.exports=o,o.id=6046},2703:(e,t,n)=>{"use strict";var r=n(414);function o(){}function a(){}a.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,a,i){if(i!==r){var s=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw s.name="Invariant Violation",s}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:a,resetWarningCache:o};return n.PropTypes=n,n}},5697:(e,t,n)=>{e.exports=n(2703)()},414:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},4448:(e,t,n)=>{"use strict";var r=n(7294),o=n(3840);function a(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n