From baa37947f740812b798786301b8ef9eab12a0dad Mon Sep 17 00:00:00 2001 From: Nicholas Blumberg <41446765+nick-next@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:58:19 -0700 Subject: [PATCH 01/92] Feature/home page react (#1) * Add semicolon to server script * Update .gitignore for IDE * Moved the onload functionality into a separate function and fixed typing. * Conversion of homepage.html to React. The homepage now passes template variables to main.ts, which initializes a React app. All homepage components are now implemented via this React app. Typing files that could potentially be shared later were put into typing files in the shared directory. * The backend is now sending the topics to the homepage as json, removing the need for json treatment on the front-end. * The header and footer of the base template are now React apps. A note that we will have to convert in a subsequent push all existing onLoad calls for existing React Apps to use an event listener to allow multiple apps to be called on one page from different places. There is also room for some refactoring and cleanup. * Converted the extraction of translated label meta-data into a loop rather than having it be explicit. * Conversion of window.onload calls to window.addEventListener. The trigger for this change is the chance that we might get multiple window.onload assignments in different places (with only the last applied assignment being run). This became an issue when the base template was given JavaScript (for the header and footer). It isn't a further issue yet because those are using addEventListener, but by pre-emptively converting all window.onloads, we prevent future clashes. * With the goal both of abstracting the data from the content of the header and footer (the menus) and to facilitate customization, the base components have been reworked. The metadata passed to the front-end via the Jinja templates have been separated out from the main template, and the dictionaries they tie into are no longer strict TypeScript objects, but records. They store as keys the original text, and as a value the translation. The objects themselves are proxies that return the original text back if a translation does not exist. This makes the interaction between the templates, the TypeScript/JavaScript and the React components more flexible. The routing has been reworked in the same way. The header and footer menus themselves are json files located in a data directory inside the base app section. They can be overridden to make custom menus by saving them as "header_custom_dc.json" in the same directory and customizing that file (likewise with footer). The webpack configuration is updated to facilitate these optional imports. * The json that supplies the header and footer menu items is now moved to the backend and injected into the context of every endpoint. We still need to consider a simple way to allow for the overriding of this for custom templates. * Update .gitignore for IDE * Moved the onload functionality into a separate function and fixed typing. * Conversion of homepage.html to React. The homepage now passes template variables to main.ts, which initializes a React app. All homepage components are now implemented via this React app. Typing files that could potentially be shared later were put into typing files in the shared directory. * The backend is now sending the topics to the homepage as json, removing the need for json treatment on the front-end. * The header and footer of the base template are now React apps. A note that we will have to convert in a subsequent push all existing onLoad calls for existing React Apps to use an event listener to allow multiple apps to be called on one page from different places. There is also room for some refactoring and cleanup. * Converted the extraction of translated label meta-data into a loop rather than having it be explicit. * Conversion of window.onload calls to window.addEventListener. The trigger for this change is the chance that we might get multiple window.onload assignments in different places (with only the last applied assignment being run). This became an issue when the base template was given JavaScript (for the header and footer). It isn't a further issue yet because those are using addEventListener, but by pre-emptively converting all window.onloads, we prevent future clashes. * With the goal both of abstracting the data from the content of the header and footer (the menus) and to facilitate customization, the base components have been reworked. The metadata passed to the front-end via the Jinja templates have been separated out from the main template, and the dictionaries they tie into are no longer strict TypeScript objects, but records. They store as keys the original text, and as a value the translation. The objects themselves are proxies that return the original text back if a translation does not exist. This makes the interaction between the templates, the TypeScript/JavaScript and the React components more flexible. The routing has been reworked in the same way. The header and footer menus themselves are json files located in a data directory inside the base app section. They can be overridden to make custom menus by saving them as "header_custom_dc.json" in the same directory and customizing that file (likewise with footer). The webpack configuration is updated to facilitate these optional imports. * The json that supplies the header and footer menu items is now moved to the backend and injected into the context of every endpoint. We still need to consider a simple way to allow for the overriding of this for custom templates. * Updated the home page to use the newer routing flow introduced during work on the base section. * Update Ids set in loop to be set based on a sluggified version of the label. --------- Co-authored-by: Jennifer Blumberg --- .gitignore | 1 + server/__init__.py | 11 +- server/config/base/footer.json | 86 +++++ server/config/base/header.json | 89 ++++++ server/routes/static.py | 2 +- server/templates/auxiliary/labels.html | 56 ++++ server/templates/auxiliary/routes.html | 29 ++ server/templates/base.html | 199 ++---------- server/templates/static/homepage.html | 156 +-------- static/js/admin/main.ts | 4 +- static/js/apps/base/components/Footer.tsx | 123 ++++++++ static/js/apps/base/components/HeaderBar.tsx | 122 +++++++ static/js/apps/base/footerApp.tsx | 53 ++++ static/js/apps/base/headerApp.tsx | 49 +++ static/js/apps/base/main.ts | 79 +++++ static/js/apps/base/utilities/utilities.ts | 82 +++++ static/js/apps/diff/main.ts | 4 +- static/js/apps/disaster_dashboard/main.ts | 4 +- static/js/apps/eval_embeddings/main.ts | 4 +- .../js/apps/eval_retrieval_generation/main.ts | 4 +- .../eval_retrieval_generation/sxs/main.ts | 4 +- static/js/apps/event/main.ts | 4 +- static/js/apps/explore/main.ts | 4 +- static/js/apps/explore_landing/main.ts | 4 +- static/js/apps/homepage/app.tsx | 71 ++++- .../js/apps/homepage/components/DataSize.tsx | 79 +++++ .../js/apps/homepage/components/LearnMore.tsx | 67 ++++ .../js/apps/homepage/components/Partners.tsx | 55 ++++ .../homepage/components/SearchAnimation.tsx | 297 ++++++++++++++++++ static/js/apps/homepage/components/Tools.tsx | 90 ++++++ static/js/apps/homepage/components/Topics.tsx | 63 ++++ static/js/apps/homepage/main.ts | 189 ++--------- static/js/apps/homepage/main_custom_dc.ts | 4 +- static/js/apps/screenshot/main.ts | 4 +- static/js/apps/sustainability/main.ts | 4 +- static/js/apps/topic_page/main.ts | 6 +- static/js/apps/visualization/main.ts | 4 +- static/js/biomedical/disease/disease.ts | 4 +- static/js/biomedical/landing/main.ts | 4 +- static/js/biomedical/protein/protein.ts | 4 +- static/js/browser/browser.ts | 4 +- static/js/dev.ts | 4 +- static/js/import_wizard2/import_wizard.ts | 4 +- static/js/place/dev_place.ts | 4 +- static/js/place/place.ts | 4 +- static/js/place/place_landing.ts | 4 +- static/js/ranking/ranking.ts | 4 +- static/js/search/search.ts | 4 +- static/js/shared/types/base.ts | 22 ++ static/js/shared/types/general.ts | 2 + static/js/shared/types/homepage.ts | 17 + static/js/tools/download/download.ts | 4 +- static/js/tools/map/map.ts | 4 +- static/js/tools/scatter/scatter.ts | 4 +- static/js/tools/stat_var/stat_var.ts | 4 +- static/js/tools/timeline/bulk_download.ts | 4 +- static/js/tools/timeline/timeline.ts | 4 +- static/js/translator/translator.ts | 4 +- static/js/utils/subject_page_utils.ts | 5 +- static/webpack.config.js | 3 + 60 files changed, 1649 insertions(+), 578 deletions(-) create mode 100644 server/config/base/footer.json create mode 100644 server/config/base/header.json create mode 100644 server/templates/auxiliary/labels.html create mode 100644 server/templates/auxiliary/routes.html create mode 100644 static/js/apps/base/components/Footer.tsx create mode 100644 static/js/apps/base/components/HeaderBar.tsx create mode 100644 static/js/apps/base/footerApp.tsx create mode 100644 static/js/apps/base/headerApp.tsx create mode 100644 static/js/apps/base/main.ts create mode 100644 static/js/apps/base/utilities/utilities.ts create mode 100644 static/js/apps/homepage/components/DataSize.tsx create mode 100644 static/js/apps/homepage/components/LearnMore.tsx create mode 100644 static/js/apps/homepage/components/Partners.tsx create mode 100644 static/js/apps/homepage/components/SearchAnimation.tsx create mode 100644 static/js/apps/homepage/components/Tools.tsx create mode 100644 static/js/apps/homepage/components/Topics.tsx create mode 100644 static/js/shared/types/base.ts create mode 100644 static/js/shared/types/general.ts create mode 100644 static/js/shared/types/homepage.ts diff --git a/.gitignore b/.gitignore index 1a797ef28b..f835eab693 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ screenshots/screenshot_url.json # Local env. *.swp .vscode +.idea !.vscode/launch.json .env.list diff --git a/server/__init__.py b/server/__init__.py index d9d85f25cf..7959daceb6 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -439,10 +439,15 @@ def add_language_code(endpoint, values): return values['hl'] = g.locale - # Provides locale parameter in all templates + # Provides locale and other common parameters in all templates @app.context_processor - def inject_locale(): - return dict(locale=get_locale()) + def inject_common_parameters(): + common_variables = { + 'HEADER_MENU': json.dumps(libutil.get_json("config/base/header.json")), + 'FOOTER_MENU': json.dumps(libutil.get_json("config/base/footer.json")) + } + locale_variable = dict(locale=get_locale()) + return {**common_variables, **locale_variable} @app.teardown_request def log_unhandled(e): diff --git a/server/config/base/footer.json b/server/config/base/footer.json new file mode 100644 index 0000000000..f2d1ba1194 --- /dev/null +++ b/server/config/base/footer.json @@ -0,0 +1,86 @@ +[ + { + "label": "Tools", + "subMenu": [ + { + "href": "place.place", + "label": "Place Explorer" + }, + { + "href": "browser.browser_main", + "label": "Knowledge Graph" + }, + { + "href": "{tools.visualization}#visType=timeline", + "label": "Timelines Explorer" + }, + { + "href": "{tools.visualization}#visType=scatter", + "label": "Scatter Plot Explorer" + }, + { + "href": "{tools.visualization}#visType=map", + "label": "Map Explorer" + }, + { + "href": "tools.stat_var", + "label": "Statistical Variable Explorer" + }, + { + "href": "tools.download", + "label": "Data Download Tool" + } + ] + }, + { + "label": "Documentation", + "subMenu": [ + { + "href": "https://docs.datacommons.org", + "label": "Documentation" + }, + { + "href": "https://docs.datacommons.org/api", + "label": "APIs" + }, + { + "hide": true, + "href": "https://docs.datacommons.org/bigquery", + "label": "BigQuery" + }, + { + "href": "https://docs.datacommons.org/tutorials", + "label": "Tutorials" + }, + { + "href": "https://docs.datacommons.org/contributing/", + "label": "Contribute" + } + ] + }, + { + "label": "Data Commons", + "subMenu": [ + { + "href": "static.about", + "label": "About Data Commons" + }, + { + "href": "https://blog.datacommons.org/", + "label": "Blog" + }, + { + "href": "https://docs.datacommons.org/datasets/", + "label": "Data Sources" + }, + { + "href": "static.feedback", + "label": "Feedback" + }, + { + "href": "static.faq", + "label": "Frequently Asked Questions" + } + ] + } +] \ No newline at end of file diff --git a/server/config/base/header.json b/server/config/base/header.json new file mode 100644 index 0000000000..0a4cd87384 --- /dev/null +++ b/server/config/base/header.json @@ -0,0 +1,89 @@ +[ + { + "label": "Explore", + "ariaLabel": "Show exploration tools", + "subMenu": [ + { + "href": "place.place", + "label": "Place Explorer" + }, + { + "href": "browser.browser_main", + "label": "Knowledge Graph" + }, + { + "href": "{tools.visualization}#visType=timeline", + "label": "Timelines Explorer" + }, + { + "href": "{tools.visualization}#visType=scatter", + "label": "Scatter Plot Explorer" + }, + { + "href": "{tools.visualization}#visType=map", + "label": "Map Explorer" + }, + { + "href": "tools.stat_var", + "label": "Statistical Variable Explorer" + }, + { + "href": "tools.download", + "label": "Data Download Tool" + } + ] + }, + { + "label": "Documentation", + "ariaLabel": "Show documentation links", + "subMenu": [ + { + "href": "https://docs.datacommons.org", + "label": "Documentation" + }, + { + "href": "https://docs.datacommons.org/api", + "label": "APIs" + }, + { + "hide": true, + "href": "https://docs.datacommons.org/bigquery", + "label": "BigQuery" + }, + { + "href": "https://docs.datacommons.org/tutorials", + "label": "Tutorials" + }, + { + "href": "https://docs.datacommons.org/contributing/", + "label": "Contribute" + } + ] + }, + { + "label": "About", + "ariaLabel": "Show about links", + "subMenu": [ + { + "href": "static.about", + "label": "About Data Commons" + }, + { + "href": "https://blog.datacommons.org/", + "label": "Blog" + }, + { + "href": "https://docs.datacommons.org/datasets/", + "label": "Data Sources" + }, + { + "href": "static.faq", + "label": "FAQ" + }, + { + "href": "static.feedback", + "label": "Feedback" + } + ] + } +] \ No newline at end of file diff --git a/server/routes/static.py b/server/routes/static.py index 7be5a99091..01a4bd98b4 100644 --- a/server/routes/static.py +++ b/server/routes/static.py @@ -39,7 +39,7 @@ def homepage(): return lib_render.render_page( "static/homepage.html", "homepage.html", - topics=current_app.config.get('HOMEPAGE_TOPICS', []), + topics=json.dumps(current_app.config.get('HOMEPAGE_TOPICS', [])), partners_list=current_app.config.get('HOMEPAGE_PARTNERS', []), partners=json.dumps(current_app.config.get('HOMEPAGE_PARTNERS', []))) diff --git a/server/templates/auxiliary/labels.html b/server/templates/auxiliary/labels.html new file mode 100644 index 0000000000..66616878e7 --- /dev/null +++ b/server/templates/auxiliary/labels.html @@ -0,0 +1,56 @@ +{#- +Copyright 2024 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-#} + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diff --git a/server/templates/auxiliary/routes.html b/server/templates/auxiliary/routes.html new file mode 100644 index 0000000000..15258a1162 --- /dev/null +++ b/server/templates/auxiliary/routes.html @@ -0,0 +1,29 @@ +{#- +Copyright 2024 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +-#} + +
+
+
+
+
+
+
+
+
+
+
+
+ diff --git a/server/templates/base.html b/server/templates/base.html index 3595405925..bee6d4f648 100644 --- a/server/templates/base.html +++ b/server/templates/base.html @@ -94,190 +94,31 @@ -
-
- -
+
+
+ +{% include 'auxiliary/labels.html' %} +{% include 'auxiliary/routes.html' %} + +
+
{% block content %} {% endblock %}
- - +
{# Compile this down (or manually implement). Used only for nav bar so far #} diff --git a/server/templates/static/homepage.html b/server/templates/static/homepage.html index 1545d896cc..6057506d5c 100644 --- a/server/templates/static/homepage.html +++ b/server/templates/static/homepage.html @@ -1,5 +1,5 @@ {# - Copyright 2023 Google LLC + Copyright 2024 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,155 +31,13 @@ {% block content %} -
-
-
-
- -
-
-
-
-

Data tells interesting stories

- - -
-
- -
-
-
- keyboard_double_arrow_up +
-
-
-
-
-

Data from

-
    -
  • 193 countries -
  • 110,000 cities -
  • 5,000 states and provinces -
-
- -
-
-
-
-
-
- -
- -
-
-

Explore the Data

-
- {% for topic in topics %} -
-
-
-
-
-
{{ topic['title'] }}
-
{{ topic['description'] }}
- -
-
- {% endfor %} -
-
- -
-
-
-
-

Data Commons Tools

-

Explore the public database through these tools

-
- -
- -
-
-
-
-
-
-
-

Learn More

-

Data forms the foundation of science, policy, and journalism, but its full potential is often limited. Data Commons addresses this by offering cloud-based APIs to access and integrate cleaned datasets, boosting their usability across different domains.

- -
-
-
-
- -
-
-
-

Our Partners

-
- {% for partner in partners_list %} - - - - {% endfor %} -
-
-
-
+
+ {% endblock %} diff --git a/static/js/admin/main.ts b/static/js/admin/main.ts index e329763606..bcca30ed28 100644 --- a/static/js/admin/main.ts +++ b/static/js/admin/main.ts @@ -21,7 +21,7 @@ import { Page } from "./page"; const domId = "main-section"; -window.onload = () => { +window.addEventListener("load", (): void => { const pageElem = document.getElementById(domId); ReactDOM.render(React.createElement(Page), pageElem); -}; +}); diff --git a/static/js/apps/base/components/Footer.tsx b/static/js/apps/base/components/Footer.tsx new file mode 100644 index 0000000000..c6d7c837e5 --- /dev/null +++ b/static/js/apps/base/components/Footer.tsx @@ -0,0 +1,123 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement, useMemo } from "react"; + +import { FooterMenu } from "../../../shared/types/base"; +import { Labels, Routes } from "../../../shared/types/general"; +import { resolveHref } from "../utilities/utilities"; + +interface FooterProps { + hideFullFooter: boolean; + hideSubFooter: boolean; + subFooterExtra: string; + brandLogoLight: boolean; + footerMenu: FooterMenu[]; + labels: Labels; + routes: Routes; +} + +const Footer = ({ + hideFullFooter, + hideSubFooter, + subFooterExtra, + brandLogoLight, + footerMenu, + labels, + routes, +}: FooterProps): ReactElement => { + const visibleFooterMenu = useMemo(() => { + return footerMenu.map((footerMenuItem) => ({ + ...footerMenuItem, + subMenu: footerMenuItem.subMenu.filter( + (filterSubMenuItem) => !filterSubMenuItem.hide + ), + })); + }, [footerMenu]); + + return ( + + ); +}; + +export default Footer; diff --git a/static/js/apps/base/components/HeaderBar.tsx b/static/js/apps/base/components/HeaderBar.tsx new file mode 100644 index 0000000000..e9a0fcdf7b --- /dev/null +++ b/static/js/apps/base/components/HeaderBar.tsx @@ -0,0 +1,122 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement, useMemo } from "react"; + +import { HeaderMenu } from "../../../shared/types/base"; +import { Labels, Routes } from "../../../shared/types/general"; +import { resolveHref, slugify } from "../utilities/utilities"; + +interface HeaderBarProps { + name: string; + logoPath: string; + menu: HeaderMenu[]; + labels: Labels; + routes: Routes; +} + +const HeaderBar = ({ + name, + logoPath, + menu, + labels, + routes, +}: HeaderBarProps): ReactElement => { + const visibleMenu = useMemo(() => { + return menu.map((menuItem) => ({ + ...menuItem, + subMenu: menuItem.subMenu.filter((subMenuItem) => !subMenuItem.hide), + })); + }, [menu]); + + return ( +
+ +
+ ); +}; + +export default HeaderBar; diff --git a/static/js/apps/base/footerApp.tsx b/static/js/apps/base/footerApp.tsx new file mode 100644 index 0000000000..6083d2ab1d --- /dev/null +++ b/static/js/apps/base/footerApp.tsx @@ -0,0 +1,53 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +import { FooterMenu } from "../../shared/types/base"; +import { Labels, Routes } from "../../shared/types/general"; +import Footer from "./components/Footer"; + +interface FooterAppProps { + hideFullFooter: boolean; + hideSubFooter: boolean; + subFooterExtra: string; + brandLogoLight: boolean; + footerMenu: FooterMenu[]; + labels: Labels; + routes: Routes; +} + +export function FooterApp({ + hideFullFooter, + hideSubFooter, + subFooterExtra, + brandLogoLight, + footerMenu, + labels, + routes, +}: FooterAppProps): ReactElement { + return ( +
+ ); +} diff --git a/static/js/apps/base/headerApp.tsx b/static/js/apps/base/headerApp.tsx new file mode 100644 index 0000000000..7cb90d4309 --- /dev/null +++ b/static/js/apps/base/headerApp.tsx @@ -0,0 +1,49 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +import { HeaderMenu } from "../../shared/types/base"; +import { Labels, Routes } from "../../shared/types/general"; +import HeaderBar from "./components/HeaderBar"; + +interface HeaderAppProps { + name: string; + logoPath: string; + headerMenu: HeaderMenu[]; + labels: Labels; + routes: Routes; +} + +export function HeaderApp({ + name, + logoPath, + headerMenu, + labels, + routes, +}: HeaderAppProps): ReactElement { + return ( + <> + + + ); +} diff --git a/static/js/apps/base/main.ts b/static/js/apps/base/main.ts new file mode 100644 index 0000000000..47f964ea9c --- /dev/null +++ b/static/js/apps/base/main.ts @@ -0,0 +1,79 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react"; +import ReactDOM from "react-dom"; + +import { loadLocaleData } from "../../i18n/i18n"; +import { FooterMenu, HeaderMenu } from "../../shared/types/base"; +import { FooterApp } from "./footerApp"; +import { HeaderApp } from "./headerApp"; +import { getLabels, getRoutes } from "./utilities/utilities"; + +window.addEventListener("load", (): void => { + loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( + () => { + renderPage(); + } + ); +}); + +function renderPage(): void { + const headerMenu = JSON.parse( + document.getElementById("metadata-base").dataset.header + ) as HeaderMenu[]; + const footerMenu = JSON.parse( + document.getElementById("metadata-base").dataset.footer + ) as FooterMenu[]; + + const name = document.getElementById("metadata-base").dataset.name; + const logoPath = document.getElementById("metadata-base").dataset.logoPath; + const hideFullFooter = + document.getElementById("metadata-base").dataset.hideFullFooter === "true"; + const hideSubFooter = + document.getElementById("metadata-base").dataset.hideSubFooter === "true"; + const subFooterExtra = + document.getElementById("metadata-base").dataset.subfooterExtra; + const brandLogoLight = + document.getElementById("metadata-base").dataset.brandLogoLight === "true"; + + const labels = getLabels(); + const routes = getRoutes(); + + ReactDOM.render( + React.createElement(HeaderApp, { + name, + logoPath, + headerMenu, + labels, + routes, + }), + document.getElementById("app-header-container") + ); + + ReactDOM.render( + React.createElement(FooterApp, { + hideFullFooter, + hideSubFooter, + subFooterExtra, + brandLogoLight, + footerMenu, + labels, + routes, + }), + document.getElementById("app-footer-container") + ); +} diff --git a/static/js/apps/base/utilities/utilities.ts b/static/js/apps/base/utilities/utilities.ts new file mode 100644 index 0000000000..dd5c5b9802 --- /dev/null +++ b/static/js/apps/base/utilities/utilities.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Labels, Routes } from "../../../shared/types/general"; + +export const resolveHref = (href: string, routes: Routes): string => { + const regex = /{([^}]+)}/; + const match = href.match(regex); + + if (match) { + const routeKey = match[1]; + const resolvedRoute = routes[routeKey] || ""; + return href.replace(regex, resolvedRoute); + } else { + return routes[href] || href; + } +}; + +export const slugify = (text: string): string => { + return text + .toString() + .toLowerCase() + .trim() + .replace(/\s+/g, "-") + .replace(/[^\w-]+/g, "") + .replace(/--+/g, "-"); +}; + +export const getRoutes = (elementId = "metadata-routes"): Routes => { + const routeElements = document.getElementById(elementId)?.children; + const routes: Routes = new Proxy( + {}, + { + get: (target, prop): string => { + return prop in target ? target[prop] : (prop as string); + }, + } + ); + + if (routeElements) { + Array.from(routeElements).forEach((element) => { + const routeTag = element.getAttribute("data-route"); + routes[routeTag] = element.getAttribute("data-value"); + }); + } + + return routes; +}; + +export const getLabels = (elementId = "metadata-labels"): Labels => { + const labelElements = document.getElementById(elementId)?.children; + const labels: Labels = new Proxy( + {}, + { + get: (target, prop): string => { + return prop in target ? target[prop] : (prop as string); + }, + } + ); + + if (labelElements) { + Array.from(labelElements).forEach((element) => { + const labelTag = element.getAttribute("data-label"); + labels[labelTag] = element.getAttribute("data-value"); + }); + } + + return labels; +}; diff --git a/static/js/apps/diff/main.ts b/static/js/apps/diff/main.ts index 9bb44cff97..383d56ae16 100644 --- a/static/js/apps/diff/main.ts +++ b/static/js/apps/diff/main.ts @@ -23,9 +23,9 @@ import ReactDOM from "react-dom"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { ReactDOM.render( React.createElement(App), document.getElementById("diff-page") ); -}; +}); diff --git a/static/js/apps/disaster_dashboard/main.ts b/static/js/apps/disaster_dashboard/main.ts index 317f97b820..7519808707 100644 --- a/static/js/apps/disaster_dashboard/main.ts +++ b/static/js/apps/disaster_dashboard/main.ts @@ -28,13 +28,13 @@ import { initSearchAutocomplete } from "../../shared/place_autocomplete"; import { loadSubjectPageMetadataFromPage } from "../../utils/subject_page_utils"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { renderPage(); } ); -}; +}); function renderPage(): void { const metadata = loadSubjectPageMetadataFromPage(); diff --git a/static/js/apps/eval_embeddings/main.ts b/static/js/apps/eval_embeddings/main.ts index 9899c53631..a7fbf415a6 100644 --- a/static/js/apps/eval_embeddings/main.ts +++ b/static/js/apps/eval_embeddings/main.ts @@ -23,9 +23,9 @@ import ReactDOM from "react-dom"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { renderPage(); -}; +}); function renderPage(): void { const serverConfig = JSON.parse( diff --git a/static/js/apps/eval_retrieval_generation/main.ts b/static/js/apps/eval_retrieval_generation/main.ts index e96dd23f2a..308bf8d61e 100644 --- a/static/js/apps/eval_retrieval_generation/main.ts +++ b/static/js/apps/eval_retrieval_generation/main.ts @@ -24,9 +24,9 @@ import ReactDOM from "react-dom"; import { App } from "./app"; import { SessionContextProvider } from "./context"; -window.onload = () => { +window.addEventListener("load", (): void => { renderPage(); -}; +}); function renderPage(): void { const sheetId = document.getElementById("metadata").dataset.sheetId; diff --git a/static/js/apps/eval_retrieval_generation/sxs/main.ts b/static/js/apps/eval_retrieval_generation/sxs/main.ts index b25204fe27..c3d1307fea 100644 --- a/static/js/apps/eval_retrieval_generation/sxs/main.ts +++ b/static/js/apps/eval_retrieval_generation/sxs/main.ts @@ -24,9 +24,9 @@ import ReactDOM from "react-dom"; import { App } from "./app"; import { SessionContextProvider } from "./context"; -window.onload = () => { +window.addEventListener("load", (): void => { renderPage(); -}; +}); function renderPage(): void { let sheetIdA = document.getElementById("metadata").dataset.sheetIdA; diff --git a/static/js/apps/event/main.ts b/static/js/apps/event/main.ts index f6db059515..d9cf2f2bbf 100644 --- a/static/js/apps/event/main.ts +++ b/static/js/apps/event/main.ts @@ -27,13 +27,13 @@ import { loadLocaleData } from "../../i18n/i18n"; import { getFilteredParentPlaces } from "../../utils/app/disaster_dashboard_utils"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { renderPage(); } ); -}; +}); function renderPage(): void { // Event diff --git a/static/js/apps/explore/main.ts b/static/js/apps/explore/main.ts index 865d90389f..a3e95ac4bf 100644 --- a/static/js/apps/explore/main.ts +++ b/static/js/apps/explore/main.ts @@ -26,13 +26,13 @@ import { URL_HASH_PARAMS } from "../../constants/app/explore_constants"; import { loadLocaleData } from "../../i18n/i18n"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { renderPage(); } ); -}; +}); function renderPage(): void { const hashParams = queryString.parse(window.location.hash); diff --git a/static/js/apps/explore_landing/main.ts b/static/js/apps/explore_landing/main.ts index f5cef7ef46..7a63550338 100644 --- a/static/js/apps/explore_landing/main.ts +++ b/static/js/apps/explore_landing/main.ts @@ -24,13 +24,13 @@ import ReactDOM from "react-dom"; import { loadLocaleData } from "../../i18n/i18n"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { renderPage(); } ); -}; +}); function renderPage(): void { ReactDOM.render( diff --git a/static/js/apps/homepage/app.tsx b/static/js/apps/homepage/app.tsx index 374f283410..3b470e1058 100644 --- a/static/js/apps/homepage/app.tsx +++ b/static/js/apps/homepage/app.tsx @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,10 @@ */ /** - * Main component for homnepage. + * Main component for homepage. */ -import React from "react"; + +import React, { ReactElement } from "react"; import { NlSearchBar } from "../../components/nl_search_bar"; import { @@ -27,24 +28,60 @@ import { GA_VALUE_SEARCH_SOURCE_HOMEPAGE, triggerGAEvent, } from "../../shared/ga_events"; +import { Routes } from "../../shared/types/general"; +import { Partner, Topic } from "../../shared/types/homepage"; +import DataSize from "./components/DataSize"; +import LearnMore from "./components/LearnMore"; +import Partners from "./components/Partners"; +import SearchAnimation from "./components/SearchAnimation"; +import Tools from "./components/Tools"; +import Topics from "./components/Topics"; + +interface AppProps { + topics: Topic[]; + partners: Partner[]; + routes: Routes; +} /** * Application container */ -export function App(): JSX.Element { +export function App({ topics, partners, routes }: AppProps): ReactElement { return ( - { - triggerGAEvent(GA_EVENT_NL_SEARCH, { - [GA_PARAM_QUERY]: q, - [GA_PARAM_SOURCE]: GA_VALUE_SEARCH_SOURCE_HOMEPAGE, - }); - window.location.href = `/explore#q=${encodeURIComponent(q)}`; - }} - placeholder={"Enter a question to explore"} - initialValue={""} - shouldAutoFocus={false} - /> + <> +
+
+ { + triggerGAEvent(GA_EVENT_NL_SEARCH, { + [GA_PARAM_QUERY]: q, + [GA_PARAM_SOURCE]: GA_VALUE_SEARCH_SOURCE_HOMEPAGE, + }); + window.location.href = `/explore#q=${encodeURIComponent(q)}`; + }} + placeholder={"Enter a question to explore"} + initialValue={""} + shouldAutoFocus={false} + /> +
+
+ + + + + +
+ +
+ +
+ +
+ + + +
+ ); } diff --git a/static/js/apps/homepage/components/DataSize.tsx b/static/js/apps/homepage/components/DataSize.tsx new file mode 100644 index 0000000000..e2ad900307 --- /dev/null +++ b/static/js/apps/homepage/components/DataSize.tsx @@ -0,0 +1,79 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +const DataSize = (): ReactElement => { + return ( +
+
+
+
+

Data from

+
    +
  • 193 countries
  • +
  • 110,000 cities
  • +
  • 5,000 states and provinces
  • +
+
+ +
+
+
+
+
+
+ ); +}; + +export default DataSize; diff --git a/static/js/apps/homepage/components/LearnMore.tsx b/static/js/apps/homepage/components/LearnMore.tsx new file mode 100644 index 0000000000..cc64597992 --- /dev/null +++ b/static/js/apps/homepage/components/LearnMore.tsx @@ -0,0 +1,67 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +import { Routes } from "../../../shared/types/general"; +import { resolveHref } from "../../base/utilities/utilities"; + +interface LearnMoreProps { + routes: Routes; +} + +const LearnMore = ({ routes }: LearnMoreProps): ReactElement => { + return ( +
+
+
+
+
+
+
+

Learn More

+

+ Data forms the foundation of science, policy, and journalism, but + its full potential is often limited. Data Commons addresses this + by offering cloud-based APIs to access and integrate cleaned + datasets, boosting their usability across different domains. +

+ +
+
+
+
+ ); +}; + +export default LearnMore; diff --git a/static/js/apps/homepage/components/Partners.tsx b/static/js/apps/homepage/components/Partners.tsx new file mode 100644 index 0000000000..c3ce5001fb --- /dev/null +++ b/static/js/apps/homepage/components/Partners.tsx @@ -0,0 +1,55 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +import { Partner } from "../../../shared/types/homepage"; + +interface PartnersProps { + partners: Partner[]; +} + +const Partners = ({ partners }: PartnersProps): ReactElement => { + return ( +
+
+
+

Our Partners

+
+ {partners.map((partner) => ( + +
+
+ ))} +
+
+
+
+ ); +}; + +export default Partners; diff --git a/static/js/apps/homepage/components/SearchAnimation.tsx b/static/js/apps/homepage/components/SearchAnimation.tsx new file mode 100644 index 0000000000..90893258c2 --- /dev/null +++ b/static/js/apps/homepage/components/SearchAnimation.tsx @@ -0,0 +1,297 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement, useEffect, useRef } from "react"; + +const SearchAnimation = (): ReactElement => { + const CHARACTER_INPUT_INTERVAL_MS = 45; + const NEXT_PROMPT_DELAY_MS = 5000; + const INITIAL_MISSION_ON_SCREEN_DELAY_MS = 2000; + const INITIAL_MISSION_FADE_IN_DELAY_MS = 1000; + const ANSWER_DELAY_MS = 2000; + const FADE_OUT_MS = 800; + const FADE_OUT_CLASS = "fade-out"; + const HIDDEN_CLASS = "hidden"; + const SLIDE_DOWN_CLASS = "slide-down"; + const INVISIBLE_CLASS = "invisible"; + const FADE_IN_CLASS = "fade-in"; + const ANIMATION_TOGGLE_COOKIE_NAME = "keepAnimationClosed"; + const ANIMATION_TOGGLE_MARGIN = "6px"; + const MAX_COOKIE_AGE = 60 * 60 * 24; + + const currentPromptIndex = useRef(0); + const currentPrompt = useRef(null); + const inputIntervalTimer = useRef>(); + const nextInputTimer = useRef>(); + const inputEl = useRef(null); + const searchAnimationContainer = useRef(null); + const searchSequenceContainer = useRef(null); + const defaultTextContainer = useRef(null); + const svgDiv = useRef(null); + const promptDiv = useRef(null); + const missionDiv = useRef(null); + const resultsElList = useRef>(); + + useEffect(() => { + resultsElList.current = document.querySelectorAll("#result-svg .result"); + + if ( + !resultsElList.current || + !promptDiv.current || + !svgDiv.current || + !missionDiv.current || + !defaultTextContainer.current || + !searchSequenceContainer.current + ) { + return; + } + + const startNextPrompt = (): void => { + let inputLength = 0; + const prompt = resultsElList.current.item( + currentPromptIndex.current + ) as HTMLElement; + currentPrompt.current = prompt; + + if (currentPromptIndex.current >= resultsElList.current.length) { + setTimeout(() => { + defaultTextContainer.current.classList.remove(FADE_OUT_CLASS); + }, FADE_OUT_MS); + searchSequenceContainer.current.classList.add(FADE_OUT_CLASS); + clearInterval(nextInputTimer.current); + return; + } + + if (currentPromptIndex.current === 0) { + defaultTextContainer.current.classList.add(FADE_OUT_CLASS); + searchSequenceContainer.current.classList.remove(HIDDEN_CLASS); + } else { + resultsElList.current + .item(currentPromptIndex.current - 1) + .classList.add(FADE_OUT_CLASS); + } + + setTimeout(() => { + if (currentPromptIndex.current === 0) { + defaultTextContainer.current.classList.add(FADE_OUT_CLASS); + svgDiv.current.classList.remove(HIDDEN_CLASS); + promptDiv.current.classList.add(HIDDEN_CLASS); + missionDiv.current.classList.remove(HIDDEN_CLASS); + } + prompt.classList.remove(HIDDEN_CLASS); + prompt.classList.add(SLIDE_DOWN_CLASS); + + if (currentPromptIndex.current > 0) { + resultsElList.current + .item(currentPromptIndex.current - 1) + .classList.add(HIDDEN_CLASS); + } + currentPromptIndex.current++; + }, ANSWER_DELAY_MS); + + inputIntervalTimer.current = setInterval(() => { + if (inputLength <= prompt.dataset.query.length) { + if (inputEl.current) { + inputEl.current.value = + prompt.dataset.query?.substring(0, inputLength) || ""; + inputEl.current.scrollLeft = inputEl.current.scrollWidth; + } + inputLength++; + } else { + clearInterval(inputIntervalTimer.current); + } + }, CHARACTER_INPUT_INTERVAL_MS); + }; + + setTimeout(() => { + promptDiv.current.classList.remove(INVISIBLE_CLASS); + promptDiv.current.classList.add(FADE_IN_CLASS); + setTimeout(() => { + startNextPrompt(); + nextInputTimer.current = setInterval(() => { + startNextPrompt(); + }, NEXT_PROMPT_DELAY_MS); + }, INITIAL_MISSION_ON_SCREEN_DELAY_MS); + }, INITIAL_MISSION_FADE_IN_DELAY_MS); + + const hideAnimation = (): void => { + searchAnimationContainer.current.setAttribute("style", "display: none;"); + const searchAnimationToggle = document.getElementById( + "search-animation-toggle" + ); + searchAnimationToggle.classList.add(HIDDEN_CLASS); + searchAnimationToggle.innerHTML = + "keyboard_double_arrow_down"; + searchAnimationToggle.setAttribute( + "style", + `margin-top: ${ANIMATION_TOGGLE_MARGIN};` + ); + document.cookie = `${ANIMATION_TOGGLE_COOKIE_NAME}=true;max-age=${MAX_COOKIE_AGE};`; + }; + + const showAnimation = (): void => { + searchAnimationContainer.current.setAttribute( + "style", + "display: visible;" + ); + const searchAnimationToggle = document.getElementById( + "search-animation-toggle" + ); + searchAnimationToggle.classList.remove(HIDDEN_CLASS); + searchAnimationToggle.innerHTML = + "keyboard_double_arrow_up"; + searchAnimationToggle.setAttribute( + "style", + `margin-bottom: ${ANIMATION_TOGGLE_MARGIN};` + ); + document.cookie = `${ANIMATION_TOGGLE_COOKIE_NAME}=;max-age=0;`; + }; + + const searchAnimationToggle = document.getElementById( + "search-animation-toggle" + ); + searchAnimationToggle.addEventListener("click", () => { + if (searchAnimationToggle.classList.contains(HIDDEN_CLASS)) { + showAnimation(); + } else { + hideAnimation(); + } + }); + + if ( + document.cookie + .split(";") + .some((item) => item.includes(`${ANIMATION_TOGGLE_COOKIE_NAME}=true`)) + ) { + hideAnimation(); + } + + return () => { + clearInterval(nextInputTimer.current); + clearInterval(inputIntervalTimer.current); + }; + }, [MAX_COOKIE_AGE]); + + const handleSearchSequenceClick = (): void => { + if (currentPrompt.current) { + const query = currentPrompt.current.dataset.query; + if (query) { + window.location.href = `/explore#q=${encodeURIComponent(query)}`; + } + } + }; + + return ( + <> +
+
+
+
+

Data tells interesting stories

+

+ Ask a question like... +

+

+ Data Commons, an initiative from Google, +
+ organizes the world’s publicly available data +
+ and makes it more accessible and useful +

+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + keyboard_double_arrow_up + +
+ + ); +}; + +export default SearchAnimation; diff --git a/static/js/apps/homepage/components/Tools.tsx b/static/js/apps/homepage/components/Tools.tsx new file mode 100644 index 0000000000..3bc569c91e --- /dev/null +++ b/static/js/apps/homepage/components/Tools.tsx @@ -0,0 +1,90 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +import { resolveHref } from "../../base/utilities/utilities"; + +interface ToolsProps { + routes: Record; +} + +const Tools = ({ routes }: ToolsProps): ReactElement => { + return ( +
+
+
+

Data Commons Tools

+

+ Explore the public database through these tools +

+
+ +
+
+ ); +}; + +export default Tools; diff --git a/static/js/apps/homepage/components/Topics.tsx b/static/js/apps/homepage/components/Topics.tsx new file mode 100644 index 0000000000..aa8b48e448 --- /dev/null +++ b/static/js/apps/homepage/components/Topics.tsx @@ -0,0 +1,63 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +import { Topic } from "../../../shared/types/homepage"; + +interface TopicsProps { + topics: Topic[]; +} + +const Topics = ({ topics }: TopicsProps): ReactElement => { + return ( +
+

Explore the Data

+
+ {topics.map((topic) => ( +
{ + window.location.href = topic.browseUrl; + }} + > +
+
+
+
+
{topic.title}
+
{topic.description}
+ +
+
+ ))} +
+
+ ); +}; + +export default Topics; diff --git a/static/js/apps/homepage/main.ts b/static/js/apps/homepage/main.ts index bc17c64bb7..ffe65376a2 100644 --- a/static/js/apps/homepage/main.ts +++ b/static/js/apps/homepage/main.ts @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,178 +13,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /** * Entrypoint file for homepage. */ + import React from "react"; import ReactDOM from "react-dom"; +import { loadLocaleData } from "../../i18n/i18n"; +import { Topic } from "../../shared/types/homepage"; +import { getRoutes } from "../base/utilities/utilities"; import { App } from "./app"; -window.onload = () => { - // Homepage animation. - const CHARACTER_INPUT_INTERVAL_MS = 45; - const NEXT_PROMPT_DELAY_MS = 5000; - const INITIAL_MISSION_ON_SCREEN_DELAY_MS = 2000; - const INITIAL_MISSION_FADE_IN_DELAY_MS = 1000; - const ANSWER_DELAY_MS = 2000; - const FADE_OUT_MS = 800; - const FADE_OUT_CLASS = "fade-out"; - const HIDDEN_CLASS = "hidden"; - const SLIDE_DOWN_CLASS = "slide-down"; - const INVISIBLE_CLASS = "invisible"; - const FADE_IN_CLASS = "fade-in"; - // Name of the cookie tracking wether to hide the search animation - const ANIMATION_TOGGLE_COOKIE_NAME = "keepAnimationClosed"; - // Distance from edges to place toggle - const ANIMATION_TOGGLE_MARGIN = "6px"; - // Maximum age of cookie in seconds - const MAX_COOKIE_AGE = 60 * 60 * 24; // 24hrs - - let inputIntervalTimer, nextInputTimer: ReturnType; - let currentPromptIndex = 0; - let prompt; - const inputEl: HTMLInputElement = ( - document.getElementById("animation-search-input") - ); - const searchSequenceContainer: HTMLDivElement = ( - document.getElementById("search-sequence") - ); - const defaultTextContainer: HTMLDivElement = ( - document.getElementById("default-text") - ); - const svgDiv: HTMLDivElement = ( - document.getElementById("result-svg") - ); - const promptDiv: HTMLDivElement = ( - document.getElementById("header-prompt") - ); - const missionDiv: HTMLDivElement = ( - document.getElementById("header-mission") - ); - const resultsElList = svgDiv.getElementsByClassName("result"); - - searchSequenceContainer.onclick = () => { - if (prompt) { - window.location.href = `/explore#q=${encodeURIComponent( - prompt.dataset.query - )}`; - } - }; - - function startNextPrompt() { - let inputLength = 0; - if (currentPromptIndex < resultsElList.length) { - prompt = resultsElList.item(currentPromptIndex); - } else { - // End the animation - setTimeout(() => { - defaultTextContainer.classList.remove(FADE_OUT_CLASS); - }, FADE_OUT_MS); - searchSequenceContainer.classList.add(FADE_OUT_CLASS); - clearInterval(nextInputTimer); - nextInputTimer = undefined; - return; - } - // Fade out the previous query - if (currentPromptIndex == 0) { - defaultTextContainer.classList.add(FADE_OUT_CLASS); - searchSequenceContainer.classList.remove(HIDDEN_CLASS); - } else { - resultsElList.item(currentPromptIndex - 1).classList.add(FADE_OUT_CLASS); +window.addEventListener("load", (): void => { + loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( + () => { + renderPage(); } - setTimeout(() => { - if (currentPromptIndex == 0) { - defaultTextContainer.classList.add(FADE_OUT_CLASS); - svgDiv.classList.remove(HIDDEN_CLASS); - promptDiv.classList.add(HIDDEN_CLASS); - missionDiv.classList.remove(HIDDEN_CLASS); - } - prompt.classList.remove(HIDDEN_CLASS); - prompt.classList.add(SLIDE_DOWN_CLASS); - if (currentPromptIndex > 0) { - resultsElList.item(currentPromptIndex - 1).classList.add(HIDDEN_CLASS); - } - currentPromptIndex++; - }, ANSWER_DELAY_MS); - - inputIntervalTimer = setInterval(() => { - // Start typing animation - if (inputLength <= prompt.dataset.query.length) { - inputEl.value = prompt.dataset.query.substring(0, inputLength); - // Set scrollLeft so we always see the full input even on narrow screens - inputEl.scrollLeft = inputEl.scrollWidth; - inputLength++; - } else { - // Slide in the answer - clearInterval(inputIntervalTimer); - } - }, CHARACTER_INPUT_INTERVAL_MS); - } - - setTimeout(() => { - promptDiv.classList.remove(INVISIBLE_CLASS); - promptDiv.classList.add(FADE_IN_CLASS); - setTimeout(() => { - startNextPrompt(); - nextInputTimer = setInterval(() => { - startNextPrompt(); - }, NEXT_PROMPT_DELAY_MS); - }, INITIAL_MISSION_ON_SCREEN_DELAY_MS); - }, INITIAL_MISSION_FADE_IN_DELAY_MS); - - // Initialize search box. - ReactDOM.render( - React.createElement(App), - document.getElementById("search-container") ); +}); - // Add toggle button and behavior to search animation - const searchAnimationToggle: HTMLDivElement = ( - document.getElementById("search-animation-toggle") +function renderPage(): void { + const topics = JSON.parse( + document.getElementById("metadata").dataset.topics + ) as Topic[]; + const partners = JSON.parse( + document.getElementById("metadata").dataset.partners ); - const searchAnimationContainer: HTMLDivElement = ( - document.getElementById("search-animation-container") - ); - - function hideAnimation(): void { - searchAnimationContainer.setAttribute("style", "display: none;"); - searchAnimationToggle.classList.add(HIDDEN_CLASS); - searchAnimationToggle.innerHTML = - "keyboard_double_arrow_down"; - searchAnimationToggle.setAttribute( - "style", - `margin-top: ${ANIMATION_TOGGLE_MARGIN};` - ); - document.cookie = `${ANIMATION_TOGGLE_COOKIE_NAME}=true;max-age=${MAX_COOKIE_AGE};`; - } - function showAnimation(): void { - searchAnimationContainer.setAttribute("style", "display: visible;"); - searchAnimationToggle.classList.remove(HIDDEN_CLASS); - searchAnimationToggle.innerHTML = - "keyboard_double_arrow_up"; - searchAnimationToggle.setAttribute( - "style", - `margin-bottom: ${ANIMATION_TOGGLE_MARGIN};` - ); - document.cookie = `${ANIMATION_TOGGLE_COOKIE_NAME}=;max-age=0;`; - } + const routes = getRoutes(); - searchAnimationToggle.addEventListener("click", function (): void { - if (searchAnimationToggle.classList.contains(HIDDEN_CLASS)) { - showAnimation(); - } else { - hideAnimation(); - } - }); - - // start with animation hidden if cookie is present - if ( - document.cookie - .split(";") - .some((item) => item.includes(`${ANIMATION_TOGGLE_COOKIE_NAME}=true`)) - ) { - hideAnimation(); - } -}; + ReactDOM.render( + React.createElement(App, { + topics, + partners, + routes, + }), + document.getElementById("app-container") + ); +} diff --git a/static/js/apps/homepage/main_custom_dc.ts b/static/js/apps/homepage/main_custom_dc.ts index ea97b04dea..9bc7630a3d 100644 --- a/static/js/apps/homepage/main_custom_dc.ts +++ b/static/js/apps/homepage/main_custom_dc.ts @@ -21,10 +21,10 @@ import ReactDOM from "react-dom"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { // Initialize search box. ReactDOM.render( React.createElement(App), document.getElementById("search-container") ); -}; +}); diff --git a/static/js/apps/screenshot/main.ts b/static/js/apps/screenshot/main.ts index e35ad32c2f..23e71266cc 100644 --- a/static/js/apps/screenshot/main.ts +++ b/static/js/apps/screenshot/main.ts @@ -19,7 +19,7 @@ import ReactDOM from "react-dom"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { const images1 = JSON.parse( document.getElementById("screenshot-data").dataset.images1 ); @@ -44,4 +44,4 @@ window.onload = () => { React.createElement(App, { data }), document.getElementById("dc-screenshot") ); -}; +}); diff --git a/static/js/apps/sustainability/main.ts b/static/js/apps/sustainability/main.ts index 64cb355969..33c561c328 100644 --- a/static/js/apps/sustainability/main.ts +++ b/static/js/apps/sustainability/main.ts @@ -28,13 +28,13 @@ import { initSearchAutocomplete } from "../../shared/place_autocomplete"; import { loadSubjectPageMetadataFromPage } from "../../utils/subject_page_utils"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { renderPage(); } ); -}; +}); function renderPage(): void { const metadata = loadSubjectPageMetadataFromPage(); diff --git a/static/js/apps/topic_page/main.ts b/static/js/apps/topic_page/main.ts index 3775bdda85..dd552c8d35 100644 --- a/static/js/apps/topic_page/main.ts +++ b/static/js/apps/topic_page/main.ts @@ -27,13 +27,13 @@ import { NamedTypedPlace } from "../../shared/types"; import { TopicsSummary } from "../../types/app/topic_page_types"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { renderPage(); } ); -}; +}); function renderPage(): void { // Get topic and render menu. @@ -55,7 +55,7 @@ function renderPage(): void { // TODO(beets): use locale from URL const locale = "en"; - loadLocaleData(locale, [ + void loadLocaleData(locale, [ import(`../../i18n/compiled-lang/${locale}/place.json`), // TODO(beets): Figure out how to place this where it's used so dependencies can be automatically resolved. import(`../../i18n/compiled-lang/${locale}/stats_var_labels.json`), diff --git a/static/js/apps/visualization/main.ts b/static/js/apps/visualization/main.ts index 423d8b6454..8084181e7c 100644 --- a/static/js/apps/visualization/main.ts +++ b/static/js/apps/visualization/main.ts @@ -24,7 +24,7 @@ import ReactDOM from "react-dom"; import { loadLocaleData } from "../../i18n/i18n"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { ReactDOM.render( @@ -33,4 +33,4 @@ window.onload = () => { ); } ); -}; +}); diff --git a/static/js/biomedical/disease/disease.ts b/static/js/biomedical/disease/disease.ts index 1ad5300aa2..581ca4f7ed 100644 --- a/static/js/biomedical/disease/disease.ts +++ b/static/js/biomedical/disease/disease.ts @@ -19,7 +19,7 @@ import ReactDOM from "react-dom"; import { Page } from "./page"; -window.onload = () => { +window.addEventListener("load", (): void => { const dcid = document.getElementById("node").dataset.dcid; const nodeName = document.getElementById("node").dataset.nn; ReactDOM.render( @@ -29,4 +29,4 @@ window.onload = () => { }), document.getElementById("node") ); -}; +}); diff --git a/static/js/biomedical/landing/main.ts b/static/js/biomedical/landing/main.ts index 44471e017d..c192f235e1 100644 --- a/static/js/biomedical/landing/main.ts +++ b/static/js/biomedical/landing/main.ts @@ -21,9 +21,9 @@ import ReactDOM from "react-dom"; import { App } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { ReactDOM.render( React.createElement(App), document.getElementById("main-content") ); -}; +}); diff --git a/static/js/biomedical/protein/protein.ts b/static/js/biomedical/protein/protein.ts index 233dd0a419..262b2e4bfd 100644 --- a/static/js/biomedical/protein/protein.ts +++ b/static/js/biomedical/protein/protein.ts @@ -19,7 +19,7 @@ import ReactDOM from "react-dom"; import { Page } from "./page"; -window.onload = () => { +window.addEventListener("load", (): void => { const dcid = document.getElementById("node").dataset.dcid; const nodeName = document.getElementById("node").dataset.nn; ReactDOM.render( @@ -29,4 +29,4 @@ window.onload = () => { }), document.getElementById("node") ); -}; +}); diff --git a/static/js/browser/browser.ts b/static/js/browser/browser.ts index 7a9054e3d7..e76aca06bb 100644 --- a/static/js/browser/browser.ts +++ b/static/js/browser/browser.ts @@ -27,7 +27,7 @@ const TYPE_OF_UNKNOWN = "Unknown"; const TYPE_OF_STAT_VAR = "StatisticalVariable"; const TYPE_OF_OBSERVATION = "StatVarObservation"; -window.onload = () => { +window.addEventListener("load", (): void => { const dcid = document.getElementById("node").dataset.dcid; const nodeName = document.getElementById("node").dataset.nn; const urlParams = new URLSearchParams(window.location.search); @@ -75,7 +75,7 @@ window.onload = () => { document.getElementById("node") ); }); -}; +}); function getNodeTypes( dcid: string, diff --git a/static/js/dev.ts b/static/js/dev.ts index 6f77c32724..8faa25fb9c 100644 --- a/static/js/dev.ts +++ b/static/js/dev.ts @@ -24,7 +24,7 @@ import ReactDOM from "react-dom"; import { DevPage } from "./dev_page"; import { loadLocaleData } from "./i18n/i18n"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("./i18n/compiled-lang/en/units.json")]).then( () => { ReactDOM.render( @@ -33,4 +33,4 @@ window.onload = () => { ); } ); -}; +}); diff --git a/static/js/import_wizard2/import_wizard.ts b/static/js/import_wizard2/import_wizard.ts index 868af73a5d..591e344107 100644 --- a/static/js/import_wizard2/import_wizard.ts +++ b/static/js/import_wizard2/import_wizard.ts @@ -18,9 +18,9 @@ import ReactDOM from "react-dom"; import { Page } from "./components/page"; -window.onload = () => { +window.addEventListener("load", (): void => { ReactDOM.render( React.createElement(Page), document.getElementById("main-pane") ); -}; +}); diff --git a/static/js/place/dev_place.ts b/static/js/place/dev_place.ts index 547c45b890..f33bcb009a 100644 --- a/static/js/place/dev_place.ts +++ b/static/js/place/dev_place.ts @@ -27,9 +27,9 @@ import { triggerGAEvent, } from "../shared/ga_events"; -window.onload = () => { +window.addEventListener("load", (): void => { renderPage(); -}; +}); /** * Handler for NL search bar diff --git a/static/js/place/place.ts b/static/js/place/place.ts index 6e67a86557..468b159ed1 100644 --- a/static/js/place/place.ts +++ b/static/js/place/place.ts @@ -50,7 +50,7 @@ const Y_SCROLL_WINDOW_BREAKPOINT = 992; // Margin to apply to the fixed sidebar top. const Y_SCROLL_MARGIN = 100; -window.onload = () => { +window.addEventListener("load", (): void => { try { renderPage(); updatePageLayoutState(); @@ -59,7 +59,7 @@ window.onload = () => { } catch (e) { return; } -}; +}); /** * Make adjustments to sidebar scroll state based on the content. diff --git a/static/js/place/place_landing.ts b/static/js/place/place_landing.ts index e13cb2ef60..597ff9757b 100644 --- a/static/js/place/place_landing.ts +++ b/static/js/place/place_landing.ts @@ -17,11 +17,11 @@ import { loadLocaleData } from "../i18n/i18n"; import { initSearchAutocomplete } from "../shared/place_autocomplete"; -window.onload = () => { +window.addEventListener("load", (): void => { const locale = document.getElementById("locale").dataset.lc; loadLocaleData(locale, [ import(`../i18n/compiled-lang/${locale}/place.json`), ]).then(() => { initSearchAutocomplete("/place"); }); -}; +}); diff --git a/static/js/ranking/ranking.ts b/static/js/ranking/ranking.ts index ce50f4c7c2..0127db9fef 100644 --- a/static/js/ranking/ranking.ts +++ b/static/js/ranking/ranking.ts @@ -21,7 +21,7 @@ import { loadLocaleData } from "../i18n/i18n"; import { renderRankingComponent } from "./component"; -window.onload = () => { +window.addEventListener("load", (): void => { const withinPlace = document.getElementById("within-place-dcid").dataset.pwp; const placeType = document.getElementById("place-type").dataset.pt; const placeName = document.getElementById("place-name").dataset.pn; @@ -51,4 +51,4 @@ window.onload = () => { date, }); }); -}; +}); diff --git a/static/js/search/search.ts b/static/js/search/search.ts index 3ca01fb570..6073ffc6f3 100644 --- a/static/js/search/search.ts +++ b/static/js/search/search.ts @@ -21,7 +21,7 @@ import ReactDOM from "react-dom"; import { AllResults } from "./all_results"; import { SearchInput } from "./search_input"; -window.onload = () => { +window.addEventListener("load", (): void => { const searchParams = new URLSearchParams(location.search); const query = document.getElementById("search-input-container").dataset.query; const selectedPlace = searchParams.get("placeDcid") || ""; @@ -40,4 +40,4 @@ window.onload = () => { document.getElementById("search-results-container") ); } -}; +}); diff --git a/static/js/shared/types/base.ts b/static/js/shared/types/base.ts new file mode 100644 index 0000000000..6537af4bf7 --- /dev/null +++ b/static/js/shared/types/base.ts @@ -0,0 +1,22 @@ +export interface HeaderMenu { + label: string; + ariaLabel: string; + subMenu: HeaderSubMenu[]; +} + +export interface HeaderSubMenu { + href: string; + label: string; + hide?: boolean; +} + +export interface FooterMenu { + label: string; + subMenu: FooterSubMenu[]; +} + +interface FooterSubMenu { + href: string; + label: string; + hide?: boolean; +} diff --git a/static/js/shared/types/general.ts b/static/js/shared/types/general.ts new file mode 100644 index 0000000000..a347b1c73a --- /dev/null +++ b/static/js/shared/types/general.ts @@ -0,0 +1,2 @@ +export type Routes = Record; +export type Labels = Record; diff --git a/static/js/shared/types/homepage.ts b/static/js/shared/types/homepage.ts new file mode 100644 index 0000000000..dd610a53ec --- /dev/null +++ b/static/js/shared/types/homepage.ts @@ -0,0 +1,17 @@ +export interface Partner { + id: string; + title: string; + url: string; + "sprite-index": number; +} + +export interface Topic { + id: string; + title: string; + description: string; + icon: string; + image: string; + url: string; + browseUrl: string; + "sprite-index": number; +} diff --git a/static/js/tools/download/download.ts b/static/js/tools/download/download.ts index fd683ef1da..35ea795ec6 100644 --- a/static/js/tools/download/download.ts +++ b/static/js/tools/download/download.ts @@ -19,8 +19,8 @@ import ReactDOM from "react-dom"; import { Page } from "./page"; -window.onload = () => { +window.addEventListener("load", (): void => { const mainPainElem = document.getElementById("main-pane"); const infoPlaces = JSON.parse(mainPainElem.dataset.infoPlaces) || []; ReactDOM.render(React.createElement(Page, { infoPlaces }), mainPainElem); -}; +}); diff --git a/static/js/tools/map/map.ts b/static/js/tools/map/map.ts index 3c8bd3c699..0ef24622a4 100644 --- a/static/js/tools/map/map.ts +++ b/static/js/tools/map/map.ts @@ -20,7 +20,7 @@ import ReactDOM from "react-dom"; import { loadLocaleData } from "../../i18n/i18n"; import { AppWithContext } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { ReactDOM.render( @@ -29,4 +29,4 @@ window.onload = () => { ); } ); -}; +}); diff --git a/static/js/tools/scatter/scatter.ts b/static/js/tools/scatter/scatter.ts index fce20410a2..54c9484758 100644 --- a/static/js/tools/scatter/scatter.ts +++ b/static/js/tools/scatter/scatter.ts @@ -20,7 +20,7 @@ import ReactDOM from "react-dom"; import { loadLocaleData } from "../../i18n/i18n"; import { AppWithContext } from "./app"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { ReactDOM.render( @@ -29,4 +29,4 @@ window.onload = () => { ); } ); -}; +}); diff --git a/static/js/tools/stat_var/stat_var.ts b/static/js/tools/stat_var/stat_var.ts index 6d497ff595..afc9afbfe4 100644 --- a/static/js/tools/stat_var/stat_var.ts +++ b/static/js/tools/stat_var/stat_var.ts @@ -19,9 +19,9 @@ import ReactDOM from "react-dom"; import { Page } from "./page"; -window.onload = () => { +window.addEventListener("load", (): void => { ReactDOM.render( React.createElement(Page), document.getElementById("main-pane") ); -}; +}); diff --git a/static/js/tools/timeline/bulk_download.ts b/static/js/tools/timeline/bulk_download.ts index 208933a06e..a5a1810540 100644 --- a/static/js/tools/timeline/bulk_download.ts +++ b/static/js/tools/timeline/bulk_download.ts @@ -101,7 +101,7 @@ function saveToCsv( saveToFile("datacommons_data.csv", csv); } -window.onload = function () { +window.addEventListener("load", (): void => { const statVars = Array.from(getTokensFromUrl("statsVar", "__")); const statVarDisplay = document.getElementById("statVars"); statVarDisplay.innerText = statVars.join(", "); @@ -114,4 +114,4 @@ window.onload = function () { downloadBulkData(statVars, ptype, "country/USA"); }); } -}; +}); diff --git a/static/js/tools/timeline/timeline.ts b/static/js/tools/timeline/timeline.ts index 0451e3984f..d270920247 100644 --- a/static/js/tools/timeline/timeline.ts +++ b/static/js/tools/timeline/timeline.ts @@ -24,7 +24,7 @@ import ReactDOM from "react-dom"; import { loadLocaleData } from "../../i18n/i18n"; import { Page } from "./page"; -window.onload = () => { +window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( () => { ReactDOM.render( @@ -33,4 +33,4 @@ window.onload = () => { ); } ); -}; +}); diff --git a/static/js/translator/translator.ts b/static/js/translator/translator.ts index 51f9b4ef91..3e071d2ec6 100644 --- a/static/js/translator/translator.ts +++ b/static/js/translator/translator.ts @@ -27,7 +27,7 @@ import { Page } from "./page"; /** * Update translation results when schema mapping or query changes. */ -window.onload = function () { +window.addEventListener("load", (): void => { ReactDOM.render( React.createElement(Page, { mapping, @@ -35,4 +35,4 @@ window.onload = function () { }), document.getElementById("translator") ); -}; +}); diff --git a/static/js/utils/subject_page_utils.ts b/static/js/utils/subject_page_utils.ts index 1c93f5d844..94800acf56 100644 --- a/static/js/utils/subject_page_utils.ts +++ b/static/js/utils/subject_page_utils.ts @@ -31,7 +31,7 @@ import { ColumnConfig, SubjectPageConfig, } from "../types/subject_page_proto_types"; -import { SubjectPageMetadata } from "./../types/subject_page_types"; +import { SubjectPageMetadata } from "../types/subject_page_types"; import { getFilteredParentPlaces } from "./app/disaster_dashboard_utils"; import { isNlInterface } from "./explore_utils"; @@ -70,7 +70,7 @@ const TITLE_MESSAGES = defineMessages({ * Gets the relative link using the title of a section on the subject page * @param title title of the section to get the relative link for */ -export function getRelLink(title: string) { +export function getRelLink(title: string): string { return title.replace(/ /g, "-"); } @@ -130,6 +130,7 @@ export function getColumnTileClassName(column: ColumnConfig): string { * @param selectedPlace the enclosing place to get geojson data for * @param placeType the place type to get geojson data for * @param parentPlaces parent places of the selected place + * @param apiRoot the stem of the API endpoint */ export function fetchGeoJsonData( selectedPlace: NamedTypedPlace, diff --git a/static/webpack.config.js b/static/webpack.config.js index 9e1afa0e92..6bd8f7da81 100644 --- a/static/webpack.config.js +++ b/static/webpack.config.js @@ -44,6 +44,9 @@ const config = { ], timeline_bulk_download: [__dirname + "/js/tools/timeline/bulk_download.ts"], mcf_playground: __dirname + "/js/mcf_playground.js", + base: [ + __dirname + "/js/apps/base/main.ts", + ], place: [ __dirname + "/js/place/place.ts", __dirname + "/css/place/place_page.scss", From 1738d8cc8ff2fa246cda0e37833623e3e3b1202f Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 19:09:04 -0700 Subject: [PATCH 02/92] This push addresses a number of comments in the PR: - The Github Repository link is added into the footer. - A fix has been applied so that boolean flags pulled from the templates are recognized regardless of the case (this solves the issue where the full footer was always displaying). - TRANSLATORS tags are returned to the templates. Note that some `trans` tags did not have TRANSLATOR tags in the original templates (this is easier to see now that the tags are all in one place). - Comments have been added to functions exported from utilities. - general.ts has been folded into base.ts. --- server/config/base/footer.json | 4 ++ server/templates/auxiliary/labels.html | 47 ++++++++++++++----- static/js/apps/base/components/Footer.tsx | 3 +- static/js/apps/base/components/HeaderBar.tsx | 3 +- static/js/apps/base/footerApp.tsx | 3 +- static/js/apps/base/headerApp.tsx | 3 +- static/js/apps/base/main.ts | 18 ++++--- static/js/apps/base/utilities/utilities.ts | 34 ++++++++++++-- static/js/apps/homepage/app.tsx | 2 +- .../js/apps/homepage/components/LearnMore.tsx | 2 +- static/js/apps/homepage/main.ts | 4 +- static/js/shared/types/base.ts | 3 ++ static/js/shared/types/general.ts | 2 - 13 files changed, 94 insertions(+), 34 deletions(-) delete mode 100644 static/js/shared/types/general.ts diff --git a/server/config/base/footer.json b/server/config/base/footer.json index f2d1ba1194..17eb98a2fb 100644 --- a/server/config/base/footer.json +++ b/server/config/base/footer.json @@ -55,6 +55,10 @@ { "href": "https://docs.datacommons.org/contributing/", "label": "Contribute" + }, + { + "href": "http://github.com/datacommonsorg", + "label": "Github Repository" } ] }, diff --git a/server/templates/auxiliary/labels.html b/server/templates/auxiliary/labels.html index 66616878e7..1ec75eb5c4 100644 --- a/server/templates/auxiliary/labels.html +++ b/server/templates/auxiliary/labels.html @@ -15,42 +15,67 @@ -#}
+ {# TRANSLATORS: The label for a link to informational pages about the Data Commons project. #}
-
-
-
-
-
-
-
-
-
+ {# TRANSLATORS: The name of a tool to browse statistics about a place. #} +
+ {# TRANSLATORS: The name of a tool to browse the Data Commons knowledge graph. #} +
+ {# TRANSLATORS: The name of a tool to explore timeline charts of statistical variables for places. #} +
+ {# TRANSLATORS: The name of a tool to explore scatter plots of statistical variables for places. #} +
+ {# TRANSLATORS: The name of a tool to explore maps of statistical variables for places. #} +
+ {# TRANSLATORS: The name of a tool that provides observation and import information about statistical variables. #} +
+ {# TRANSLATORS: The name of a tool that allows users to download data. #} +
+ {# TRANSLATORS: The name of a dashboard that shows information about natural disasters. #} +
+
+ {# TRANSLATORS: The label for a list of documentation links. #}
+ {# TRANSLATORS: The label for a link to our API documentation. #}
+ {# TRANSLATORS: The label for a link to BigQuery integration starter docs. #}
+ {# TRANSLATORS: The label for a link to our API tutorials. #}
+ {# TRANSLATORS: The label for a link to instructions about contributing to the project. #}
+ {# TRANSLATORS: The label for a link to the project's GitHub repository (for open sourced code). #}
+ {# TRANSLATORS: The label for a link to the project's about page. #}
+ {# TRANSLATORS: The label for a link to the project's blog. #}
+ {# TRANSLATORS: The label for a link to data sources included in the Data Commons knowledge graph. #}
-
+ {# TRANSLATORS: The label for a link to project FAQ page (abbreviated version). #}
+ {# TRANSLATORS: The label for a link to project FAQ page. #}
+ {# TRANSLATORS: The label for a link to instructions about sending feedback. #}
+ {# TRANSLATORS: The label for a collection of exploration tools. #}
+ {# TRANSLATORS: The label for a link to instructions about sending feedback. #}
+ {# TRANSLATORS: The label for the Google branding byline. #}
+ {# TRANSLATORS: The label for a link to site terms and conditions. #}
-
+ {# TRANSLATORS: The label for a link to site privacy policy. #}
+ {# TRANSLATORS: The label for a link to site disclaimers. #}
diff --git a/static/js/apps/base/components/Footer.tsx b/static/js/apps/base/components/Footer.tsx index c6d7c837e5..e30e9d4eef 100644 --- a/static/js/apps/base/components/Footer.tsx +++ b/static/js/apps/base/components/Footer.tsx @@ -16,8 +16,7 @@ import React, { ReactElement, useMemo } from "react"; -import { FooterMenu } from "../../../shared/types/base"; -import { Labels, Routes } from "../../../shared/types/general"; +import { FooterMenu, Labels, Routes } from "../../../shared/types/base"; import { resolveHref } from "../utilities/utilities"; interface FooterProps { diff --git a/static/js/apps/base/components/HeaderBar.tsx b/static/js/apps/base/components/HeaderBar.tsx index e9a0fcdf7b..008aa18cce 100644 --- a/static/js/apps/base/components/HeaderBar.tsx +++ b/static/js/apps/base/components/HeaderBar.tsx @@ -16,8 +16,7 @@ import React, { ReactElement, useMemo } from "react"; -import { HeaderMenu } from "../../../shared/types/base"; -import { Labels, Routes } from "../../../shared/types/general"; +import { HeaderMenu, Labels, Routes } from "../../../shared/types/base"; import { resolveHref, slugify } from "../utilities/utilities"; interface HeaderBarProps { diff --git a/static/js/apps/base/footerApp.tsx b/static/js/apps/base/footerApp.tsx index 6083d2ab1d..c0c63865e2 100644 --- a/static/js/apps/base/footerApp.tsx +++ b/static/js/apps/base/footerApp.tsx @@ -16,8 +16,7 @@ import React, { ReactElement } from "react"; -import { FooterMenu } from "../../shared/types/base"; -import { Labels, Routes } from "../../shared/types/general"; +import { FooterMenu, Labels, Routes } from "../../shared/types/base"; import Footer from "./components/Footer"; interface FooterAppProps { diff --git a/static/js/apps/base/headerApp.tsx b/static/js/apps/base/headerApp.tsx index 7cb90d4309..913d0ad07d 100644 --- a/static/js/apps/base/headerApp.tsx +++ b/static/js/apps/base/headerApp.tsx @@ -16,8 +16,7 @@ import React, { ReactElement } from "react"; -import { HeaderMenu } from "../../shared/types/base"; -import { Labels, Routes } from "../../shared/types/general"; +import { HeaderMenu, Labels, Routes } from "../../shared/types/base"; import HeaderBar from "./components/HeaderBar"; interface HeaderAppProps { diff --git a/static/js/apps/base/main.ts b/static/js/apps/base/main.ts index 47f964ea9c..4f82f87f10 100644 --- a/static/js/apps/base/main.ts +++ b/static/js/apps/base/main.ts @@ -21,7 +21,7 @@ import { loadLocaleData } from "../../i18n/i18n"; import { FooterMenu, HeaderMenu } from "../../shared/types/base"; import { FooterApp } from "./footerApp"; import { HeaderApp } from "./headerApp"; -import { getLabels, getRoutes } from "./utilities/utilities"; +import { extractLabels, extractRoutes } from "./utilities/utilities"; window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( @@ -42,16 +42,22 @@ function renderPage(): void { const name = document.getElementById("metadata-base").dataset.name; const logoPath = document.getElementById("metadata-base").dataset.logoPath; const hideFullFooter = - document.getElementById("metadata-base").dataset.hideFullFooter === "true"; + document + .getElementById("metadata-base") + .dataset.hideFullFooter.toLowerCase() === "true"; const hideSubFooter = - document.getElementById("metadata-base").dataset.hideSubFooter === "true"; + document + .getElementById("metadata-base") + .dataset.hideSubFooter.toLowerCase() === "true"; const subFooterExtra = document.getElementById("metadata-base").dataset.subfooterExtra; const brandLogoLight = - document.getElementById("metadata-base").dataset.brandLogoLight === "true"; + document + .getElementById("metadata-base") + .dataset.brandLogoLight.toLowerCase() === "true"; - const labels = getLabels(); - const routes = getRoutes(); + const labels = extractLabels(); + const routes = extractRoutes(); ReactDOM.render( React.createElement(HeaderApp, { diff --git a/static/js/apps/base/utilities/utilities.ts b/static/js/apps/base/utilities/utilities.ts index dd5c5b9802..bafc06daa9 100644 --- a/static/js/apps/base/utilities/utilities.ts +++ b/static/js/apps/base/utilities/utilities.ts @@ -14,8 +14,16 @@ * limitations under the License. */ -import { Labels, Routes } from "../../../shared/types/general"; +import { Labels, Routes } from "../../../shared/types/base"; +/* + This function takes a string that is either a pure url, a route (such as static.homepage) + or a route wrapped in {} located inside a string (such as {tools.visualization}#visType=timeline), + returns the string converted into a url. + + The purpose of the function is to flexibly resolve strings from sources such as JSON that may contain + either routes or raw URLs and to return the final URL. + */ export const resolveHref = (href: string, routes: Routes): string => { const regex = /{([^}]+)}/; const match = href.match(regex); @@ -29,6 +37,11 @@ export const resolveHref = (href: string, routes: Routes): string => { } }; +/* + This function takes a string that may contain spaces and capital letters and returns a slugged version + of the string in kebab-case. It is used to convert labels into slugs that can be used as part of html + ids (used currently in React components where labels are converted into Ids that previously were hard-coded). + */ export const slugify = (text: string): string => { return text .toString() @@ -39,7 +52,15 @@ export const slugify = (text: string): string => { .replace(/--+/g, "-"); }; -export const getRoutes = (elementId = "metadata-routes"): Routes => { +/* + This function takes the id of a data container div and returns a route dictionary from the pairs in the container. + The referenced data container should be of the form: +
+
+
+
+ */ +export const extractRoutes = (elementId = "metadata-routes"): Routes => { const routeElements = document.getElementById(elementId)?.children; const routes: Routes = new Proxy( {}, @@ -60,7 +81,14 @@ export const getRoutes = (elementId = "metadata-routes"): Routes => { return routes; }; -export const getLabels = (elementId = "metadata-labels"): Labels => { +/* + This function takes the id of a data container div and returns a label dictionary from the pairs in the container. + The referenced data container should be of the form: +
+
+
+ */ +export const extractLabels = (elementId = "metadata-labels"): Labels => { const labelElements = document.getElementById(elementId)?.children; const labels: Labels = new Proxy( {}, diff --git a/static/js/apps/homepage/app.tsx b/static/js/apps/homepage/app.tsx index 3b470e1058..c231bae57f 100644 --- a/static/js/apps/homepage/app.tsx +++ b/static/js/apps/homepage/app.tsx @@ -28,7 +28,7 @@ import { GA_VALUE_SEARCH_SOURCE_HOMEPAGE, triggerGAEvent, } from "../../shared/ga_events"; -import { Routes } from "../../shared/types/general"; +import { Routes } from "../../shared/types/base"; import { Partner, Topic } from "../../shared/types/homepage"; import DataSize from "./components/DataSize"; import LearnMore from "./components/LearnMore"; diff --git a/static/js/apps/homepage/components/LearnMore.tsx b/static/js/apps/homepage/components/LearnMore.tsx index cc64597992..5b412528a2 100644 --- a/static/js/apps/homepage/components/LearnMore.tsx +++ b/static/js/apps/homepage/components/LearnMore.tsx @@ -16,7 +16,7 @@ import React, { ReactElement } from "react"; -import { Routes } from "../../../shared/types/general"; +import { Routes } from "../../../shared/types/base"; import { resolveHref } from "../../base/utilities/utilities"; interface LearnMoreProps { diff --git a/static/js/apps/homepage/main.ts b/static/js/apps/homepage/main.ts index ffe65376a2..463df19e3e 100644 --- a/static/js/apps/homepage/main.ts +++ b/static/js/apps/homepage/main.ts @@ -23,7 +23,7 @@ import ReactDOM from "react-dom"; import { loadLocaleData } from "../../i18n/i18n"; import { Topic } from "../../shared/types/homepage"; -import { getRoutes } from "../base/utilities/utilities"; +import { extractRoutes } from "../base/utilities/utilities"; import { App } from "./app"; window.addEventListener("load", (): void => { @@ -42,7 +42,7 @@ function renderPage(): void { document.getElementById("metadata").dataset.partners ); - const routes = getRoutes(); + const routes = extractRoutes(); ReactDOM.render( React.createElement(App, { diff --git a/static/js/shared/types/base.ts b/static/js/shared/types/base.ts index 6537af4bf7..2224afc2f7 100644 --- a/static/js/shared/types/base.ts +++ b/static/js/shared/types/base.ts @@ -20,3 +20,6 @@ interface FooterSubMenu { label: string; hide?: boolean; } + +export type Routes = Record; +export type Labels = Record; diff --git a/static/js/shared/types/general.ts b/static/js/shared/types/general.ts deleted file mode 100644 index a347b1c73a..0000000000 --- a/static/js/shared/types/general.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type Routes = Record; -export type Labels = Record; From 4393072a74137d90498bfd9250074684958e7d75 Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 19:28:14 -0700 Subject: [PATCH 03/92] File, path and import renaming. --- server/templates/base.html | 4 ++-- server/templates/{auxiliary => metadata}/labels.html | 0 server/templates/{auxiliary => metadata}/routes.html | 0 .../apps/base/components/{Footer.tsx => footer.tsx} | 0 .../components/{HeaderBar.tsx => header_bar.tsx} | 0 .../js/apps/base/{footerApp.tsx => footer_app.tsx} | 2 +- .../js/apps/base/{headerApp.tsx => header_app.tsx} | 2 +- static/js/apps/base/main.ts | 4 ++-- static/js/apps/homepage/app.tsx | 12 ++++++------ .../components/{DataSize.tsx => data_size.tsx} | 0 .../components/{LearnMore.tsx => learn_more.tsx} | 0 .../components/{Partners.tsx => partners.tsx} | 0 .../{SearchAnimation.tsx => search_animation.tsx} | 0 .../homepage/components/{Tools.tsx => tools.tsx} | 0 .../homepage/components/{Topics.tsx => topics.tsx} | 0 15 files changed, 12 insertions(+), 12 deletions(-) rename server/templates/{auxiliary => metadata}/labels.html (100%) rename server/templates/{auxiliary => metadata}/routes.html (100%) rename static/js/apps/base/components/{Footer.tsx => footer.tsx} (100%) rename static/js/apps/base/components/{HeaderBar.tsx => header_bar.tsx} (100%) rename static/js/apps/base/{footerApp.tsx => footer_app.tsx} (96%) rename static/js/apps/base/{headerApp.tsx => header_app.tsx} (95%) rename static/js/apps/homepage/components/{DataSize.tsx => data_size.tsx} (100%) rename static/js/apps/homepage/components/{LearnMore.tsx => learn_more.tsx} (100%) rename static/js/apps/homepage/components/{Partners.tsx => partners.tsx} (100%) rename static/js/apps/homepage/components/{SearchAnimation.tsx => search_animation.tsx} (100%) rename static/js/apps/homepage/components/{Tools.tsx => tools.tsx} (100%) rename static/js/apps/homepage/components/{Topics.tsx => topics.tsx} (100%) diff --git a/server/templates/base.html b/server/templates/base.html index bee6d4f648..1a28c6c351 100644 --- a/server/templates/base.html +++ b/server/templates/base.html @@ -108,8 +108,8 @@ >
-{% include 'auxiliary/labels.html' %} -{% include 'auxiliary/routes.html' %} +{% include 'metadata/labels.html' %} +{% include 'metadata/routes.html' %}
diff --git a/server/templates/auxiliary/labels.html b/server/templates/metadata/labels.html similarity index 100% rename from server/templates/auxiliary/labels.html rename to server/templates/metadata/labels.html diff --git a/server/templates/auxiliary/routes.html b/server/templates/metadata/routes.html similarity index 100% rename from server/templates/auxiliary/routes.html rename to server/templates/metadata/routes.html diff --git a/static/js/apps/base/components/Footer.tsx b/static/js/apps/base/components/footer.tsx similarity index 100% rename from static/js/apps/base/components/Footer.tsx rename to static/js/apps/base/components/footer.tsx diff --git a/static/js/apps/base/components/HeaderBar.tsx b/static/js/apps/base/components/header_bar.tsx similarity index 100% rename from static/js/apps/base/components/HeaderBar.tsx rename to static/js/apps/base/components/header_bar.tsx diff --git a/static/js/apps/base/footerApp.tsx b/static/js/apps/base/footer_app.tsx similarity index 96% rename from static/js/apps/base/footerApp.tsx rename to static/js/apps/base/footer_app.tsx index c0c63865e2..e9d14a9db4 100644 --- a/static/js/apps/base/footerApp.tsx +++ b/static/js/apps/base/footer_app.tsx @@ -17,7 +17,7 @@ import React, { ReactElement } from "react"; import { FooterMenu, Labels, Routes } from "../../shared/types/base"; -import Footer from "./components/Footer"; +import Footer from "./components/footer"; interface FooterAppProps { hideFullFooter: boolean; diff --git a/static/js/apps/base/headerApp.tsx b/static/js/apps/base/header_app.tsx similarity index 95% rename from static/js/apps/base/headerApp.tsx rename to static/js/apps/base/header_app.tsx index 913d0ad07d..cb18586c7c 100644 --- a/static/js/apps/base/headerApp.tsx +++ b/static/js/apps/base/header_app.tsx @@ -17,7 +17,7 @@ import React, { ReactElement } from "react"; import { HeaderMenu, Labels, Routes } from "../../shared/types/base"; -import HeaderBar from "./components/HeaderBar"; +import HeaderBar from "./components/header_bar"; interface HeaderAppProps { name: string; diff --git a/static/js/apps/base/main.ts b/static/js/apps/base/main.ts index 4f82f87f10..755a924193 100644 --- a/static/js/apps/base/main.ts +++ b/static/js/apps/base/main.ts @@ -19,8 +19,8 @@ import ReactDOM from "react-dom"; import { loadLocaleData } from "../../i18n/i18n"; import { FooterMenu, HeaderMenu } from "../../shared/types/base"; -import { FooterApp } from "./footerApp"; -import { HeaderApp } from "./headerApp"; +import { FooterApp } from "./footer_app"; +import { HeaderApp } from "./header_app"; import { extractLabels, extractRoutes } from "./utilities/utilities"; window.addEventListener("load", (): void => { diff --git a/static/js/apps/homepage/app.tsx b/static/js/apps/homepage/app.tsx index c231bae57f..c9f5f16573 100644 --- a/static/js/apps/homepage/app.tsx +++ b/static/js/apps/homepage/app.tsx @@ -30,12 +30,12 @@ import { } from "../../shared/ga_events"; import { Routes } from "../../shared/types/base"; import { Partner, Topic } from "../../shared/types/homepage"; -import DataSize from "./components/DataSize"; -import LearnMore from "./components/LearnMore"; -import Partners from "./components/Partners"; -import SearchAnimation from "./components/SearchAnimation"; -import Tools from "./components/Tools"; -import Topics from "./components/Topics"; +import DataSize from "./components/data_size"; +import LearnMore from "./components/learn_more"; +import Partners from "./components/partners"; +import SearchAnimation from "./components/search_animation"; +import Tools from "./components/tools"; +import Topics from "./components/topics"; interface AppProps { topics: Topic[]; diff --git a/static/js/apps/homepage/components/DataSize.tsx b/static/js/apps/homepage/components/data_size.tsx similarity index 100% rename from static/js/apps/homepage/components/DataSize.tsx rename to static/js/apps/homepage/components/data_size.tsx diff --git a/static/js/apps/homepage/components/LearnMore.tsx b/static/js/apps/homepage/components/learn_more.tsx similarity index 100% rename from static/js/apps/homepage/components/LearnMore.tsx rename to static/js/apps/homepage/components/learn_more.tsx diff --git a/static/js/apps/homepage/components/Partners.tsx b/static/js/apps/homepage/components/partners.tsx similarity index 100% rename from static/js/apps/homepage/components/Partners.tsx rename to static/js/apps/homepage/components/partners.tsx diff --git a/static/js/apps/homepage/components/SearchAnimation.tsx b/static/js/apps/homepage/components/search_animation.tsx similarity index 100% rename from static/js/apps/homepage/components/SearchAnimation.tsx rename to static/js/apps/homepage/components/search_animation.tsx diff --git a/static/js/apps/homepage/components/Tools.tsx b/static/js/apps/homepage/components/tools.tsx similarity index 100% rename from static/js/apps/homepage/components/Tools.tsx rename to static/js/apps/homepage/components/tools.tsx diff --git a/static/js/apps/homepage/components/Topics.tsx b/static/js/apps/homepage/components/topics.tsx similarity index 100% rename from static/js/apps/homepage/components/Topics.tsx rename to static/js/apps/homepage/components/topics.tsx From 8b3ac393d16c5cbfca9311ab09c6ce784e6c2e3c Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 19:44:01 -0700 Subject: [PATCH 04/92] File formatting, tagged the homepage metadata data container as "metadata-hompeage" (for future collision-proofing), added copywrite note and descriptor to typing files that did not have them. --- server/templates/base.html | 32 ++++++++++++++------------- server/templates/static/homepage.html | 2 +- static/js/apps/homepage/main.ts | 4 ++-- static/js/shared/types/base.ts | 20 +++++++++++++++++ static/js/shared/types/homepage.ts | 20 +++++++++++++++++ 5 files changed, 60 insertions(+), 18 deletions(-) diff --git a/server/templates/base.html b/server/templates/base.html index 1a28c6c351..63eba3f566 100644 --- a/server/templates/base.html +++ b/server/templates/base.html @@ -112,21 +112,23 @@ {% include 'metadata/routes.html' %} -
-
-
- {% block content %} - {% endblock %} -
- -
- {# Compile this down (or manually implement). Used only for nav bar so far #} - - - {% block footer %} - {% endblock %} + +
+
+
+ {% block content %} + {% endblock %} +
+ +
+ +{# Compile this down (or manually implement). Used only for nav bar so far #} + + +{% block footer %} +{% endblock %} \ No newline at end of file diff --git a/server/templates/static/homepage.html b/server/templates/static/homepage.html index 6057506d5c..ce7c98f646 100644 --- a/server/templates/static/homepage.html +++ b/server/templates/static/homepage.html @@ -31,7 +31,7 @@ {% block content %} -
diff --git a/static/js/apps/homepage/main.ts b/static/js/apps/homepage/main.ts index 463df19e3e..caf11c3545 100644 --- a/static/js/apps/homepage/main.ts +++ b/static/js/apps/homepage/main.ts @@ -36,10 +36,10 @@ window.addEventListener("load", (): void => { function renderPage(): void { const topics = JSON.parse( - document.getElementById("metadata").dataset.topics + document.getElementById("metadata-homepage").dataset.topics ) as Topic[]; const partners = JSON.parse( - document.getElementById("metadata").dataset.partners + document.getElementById("metadata-homepage").dataset.partners ); const routes = extractRoutes(); diff --git a/static/js/shared/types/base.ts b/static/js/shared/types/base.ts index 2224afc2f7..70bab4558f 100644 --- a/static/js/shared/types/base.ts +++ b/static/js/shared/types/base.ts @@ -1,3 +1,23 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Typing for components and TypeScript associated with the base template. + */ + export interface HeaderMenu { label: string; ariaLabel: string; diff --git a/static/js/shared/types/homepage.ts b/static/js/shared/types/homepage.ts index dd610a53ec..403e8103eb 100644 --- a/static/js/shared/types/homepage.ts +++ b/static/js/shared/types/homepage.ts @@ -1,3 +1,23 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Typing for components and TypeScript associated with the homepage template. + */ + export interface Partner { id: string; title: string; From 547bf5e293a597d4e67b2765f418307aab4e3633 Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 20:21:05 -0700 Subject: [PATCH 05/92] To keep full backwards compatibility with templates using the old home page as the new homepage diverges, we are forking the new homepage to a "_v2". Currently, we are only generating a version two of the JavaScript file, but as we restyle, we will generate a version 2 of the CSS as well. This way, templates that include the old .js and .css files will continue to work as they do now. --- server/templates/static/homepage.html | 2 +- static/js/apps/homepage/main.ts | 187 ++++++++++++++++++++++---- static/webpack.config.js | 3 + 3 files changed, 165 insertions(+), 27 deletions(-) diff --git a/server/templates/static/homepage.html b/server/templates/static/homepage.html index ce7c98f646..f7c4556dc2 100644 --- a/server/templates/static/homepage.html +++ b/server/templates/static/homepage.html @@ -26,7 +26,7 @@ {% block head %} - + {% endblock %} {% block content %} diff --git a/static/js/apps/homepage/main.ts b/static/js/apps/homepage/main.ts index caf11c3545..c7987618b5 100644 --- a/static/js/apps/homepage/main.ts +++ b/static/js/apps/homepage/main.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024 Google LLC + * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,43 +13,178 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /** * Entrypoint file for homepage. */ - import React from "react"; import ReactDOM from "react-dom"; -import { loadLocaleData } from "../../i18n/i18n"; -import { Topic } from "../../shared/types/homepage"; -import { extractRoutes } from "../base/utilities/utilities"; import { App } from "./app"; window.addEventListener("load", (): void => { - loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( - () => { - renderPage(); + // Homepage animation. + const CHARACTER_INPUT_INTERVAL_MS = 45; + const NEXT_PROMPT_DELAY_MS = 5000; + const INITIAL_MISSION_ON_SCREEN_DELAY_MS = 2000; + const INITIAL_MISSION_FADE_IN_DELAY_MS = 1000; + const ANSWER_DELAY_MS = 2000; + const FADE_OUT_MS = 800; + const FADE_OUT_CLASS = "fade-out"; + const HIDDEN_CLASS = "hidden"; + const SLIDE_DOWN_CLASS = "slide-down"; + const INVISIBLE_CLASS = "invisible"; + const FADE_IN_CLASS = "fade-in"; + // Name of the cookie tracking wether to hide the search animation + const ANIMATION_TOGGLE_COOKIE_NAME = "keepAnimationClosed"; + // Distance from edges to place toggle + const ANIMATION_TOGGLE_MARGIN = "6px"; + // Maximum age of cookie in seconds + const MAX_COOKIE_AGE = 60 * 60 * 24; // 24hrs + + let inputIntervalTimer, nextInputTimer: ReturnType; + let currentPromptIndex = 0; + let prompt; + const inputEl: HTMLInputElement = ( + document.getElementById("animation-search-input") + ); + const searchSequenceContainer: HTMLDivElement = ( + document.getElementById("search-sequence") + ); + const defaultTextContainer: HTMLDivElement = ( + document.getElementById("default-text") + ); + const svgDiv: HTMLDivElement = ( + document.getElementById("result-svg") + ); + const promptDiv: HTMLDivElement = ( + document.getElementById("header-prompt") + ); + const missionDiv: HTMLDivElement = ( + document.getElementById("header-mission") + ); + const resultsElList = svgDiv.getElementsByClassName("result"); + + searchSequenceContainer.onclick = () => { + if (prompt) { + window.location.href = `/explore#q=${encodeURIComponent( + prompt.dataset.query + )}`; + } + }; + + function startNextPrompt() { + let inputLength = 0; + if (currentPromptIndex < resultsElList.length) { + prompt = resultsElList.item(currentPromptIndex); + } else { + // End the animation + setTimeout(() => { + defaultTextContainer.classList.remove(FADE_OUT_CLASS); + }, FADE_OUT_MS); + searchSequenceContainer.classList.add(FADE_OUT_CLASS); + clearInterval(nextInputTimer); + nextInputTimer = undefined; + return; } + // Fade out the previous query + if (currentPromptIndex == 0) { + defaultTextContainer.classList.add(FADE_OUT_CLASS); + searchSequenceContainer.classList.remove(HIDDEN_CLASS); + } else { + resultsElList.item(currentPromptIndex - 1).classList.add(FADE_OUT_CLASS); + } + setTimeout(() => { + if (currentPromptIndex == 0) { + defaultTextContainer.classList.add(FADE_OUT_CLASS); + svgDiv.classList.remove(HIDDEN_CLASS); + promptDiv.classList.add(HIDDEN_CLASS); + missionDiv.classList.remove(HIDDEN_CLASS); + } + prompt.classList.remove(HIDDEN_CLASS); + prompt.classList.add(SLIDE_DOWN_CLASS); + if (currentPromptIndex > 0) { + resultsElList.item(currentPromptIndex - 1).classList.add(HIDDEN_CLASS); + } + currentPromptIndex++; + }, ANSWER_DELAY_MS); + + inputIntervalTimer = setInterval(() => { + // Start typing animation + if (inputLength <= prompt.dataset.query.length) { + inputEl.value = prompt.dataset.query.substring(0, inputLength); + // Set scrollLeft so we always see the full input even on narrow screens + inputEl.scrollLeft = inputEl.scrollWidth; + inputLength++; + } else { + // Slide in the answer + clearInterval(inputIntervalTimer); + } + }, CHARACTER_INPUT_INTERVAL_MS); + } + + setTimeout(() => { + promptDiv.classList.remove(INVISIBLE_CLASS); + promptDiv.classList.add(FADE_IN_CLASS); + setTimeout(() => { + startNextPrompt(); + nextInputTimer = setInterval(() => { + startNextPrompt(); + }, NEXT_PROMPT_DELAY_MS); + }, INITIAL_MISSION_ON_SCREEN_DELAY_MS); + }, INITIAL_MISSION_FADE_IN_DELAY_MS); + + // Initialize search box. + ReactDOM.render( + React.createElement(App), + document.getElementById("search-container") ); -}); -function renderPage(): void { - const topics = JSON.parse( - document.getElementById("metadata-homepage").dataset.topics - ) as Topic[]; - const partners = JSON.parse( - document.getElementById("metadata-homepage").dataset.partners + // Add toggle button and behavior to search animation + const searchAnimationToggle: HTMLDivElement = ( + document.getElementById("search-animation-toggle") + ); + const searchAnimationContainer: HTMLDivElement = ( + document.getElementById("search-animation-container") ); - const routes = extractRoutes(); + function hideAnimation(): void { + searchAnimationContainer.setAttribute("style", "display: none;"); + searchAnimationToggle.classList.add(HIDDEN_CLASS); + searchAnimationToggle.innerHTML = + "keyboard_double_arrow_down"; + searchAnimationToggle.setAttribute( + "style", + `margin-top: ${ANIMATION_TOGGLE_MARGIN};` + ); + document.cookie = `${ANIMATION_TOGGLE_COOKIE_NAME}=true;max-age=${MAX_COOKIE_AGE};`; + } - ReactDOM.render( - React.createElement(App, { - topics, - partners, - routes, - }), - document.getElementById("app-container") - ); -} + function showAnimation(): void { + searchAnimationContainer.setAttribute("style", "display: visible;"); + searchAnimationToggle.classList.remove(HIDDEN_CLASS); + searchAnimationToggle.innerHTML = + "keyboard_double_arrow_up"; + searchAnimationToggle.setAttribute( + "style", + `margin-bottom: ${ANIMATION_TOGGLE_MARGIN};` + ); + document.cookie = `${ANIMATION_TOGGLE_COOKIE_NAME}=;max-age=0;`; + } + + searchAnimationToggle.addEventListener("click", function (): void { + if (searchAnimationToggle.classList.contains(HIDDEN_CLASS)) { + showAnimation(); + } else { + hideAnimation(); + } + }); + + // start with animation hidden if cookie is present + if ( + document.cookie + .split(";") + .some((item) => item.includes(`${ANIMATION_TOGGLE_COOKIE_NAME}=true`)) + ) { + hideAnimation(); + } +}); diff --git a/static/webpack.config.js b/static/webpack.config.js index 6bd8f7da81..6f6caac016 100644 --- a/static/webpack.config.js +++ b/static/webpack.config.js @@ -146,6 +146,9 @@ const config = { __dirname + "/js/apps/homepage/main.ts", __dirname + "/css/homepage.scss", ], + homepage_v2: [ + __dirname + "/js/apps/homepage/main_v2.ts", + ], homepage_custom_dc: [ __dirname + "/js/apps/homepage/main_custom_dc.ts", __dirname + "/css/homepage.scss", From 8758913dcb518715a5ecd2d00f458157e6efa923 Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 20:22:00 -0700 Subject: [PATCH 06/92] Following up from the previous commit, the .ts file for v2 (the new version). --- static/js/apps/homepage/main_v2.ts | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 static/js/apps/homepage/main_v2.ts diff --git a/static/js/apps/homepage/main_v2.ts b/static/js/apps/homepage/main_v2.ts new file mode 100644 index 0000000000..caf11c3545 --- /dev/null +++ b/static/js/apps/homepage/main_v2.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Entrypoint file for homepage. + */ + +import React from "react"; +import ReactDOM from "react-dom"; + +import { loadLocaleData } from "../../i18n/i18n"; +import { Topic } from "../../shared/types/homepage"; +import { extractRoutes } from "../base/utilities/utilities"; +import { App } from "./app"; + +window.addEventListener("load", (): void => { + loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( + () => { + renderPage(); + } + ); +}); + +function renderPage(): void { + const topics = JSON.parse( + document.getElementById("metadata-homepage").dataset.topics + ) as Topic[]; + const partners = JSON.parse( + document.getElementById("metadata-homepage").dataset.partners + ); + + const routes = extractRoutes(); + + ReactDOM.render( + React.createElement(App, { + topics, + partners, + routes, + }), + document.getElementById("app-container") + ); +} From 3b788c1a4956742934625d7f6b31d78d219d2b36 Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 20:34:40 -0700 Subject: [PATCH 07/92] Factored out the calls to `document.getElementById` so that it is only called once. --- static/js/apps/base/main.ts | 25 ++++++++++--------------- static/js/apps/homepage/main_v2.ts | 10 ++++------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/static/js/apps/base/main.ts b/static/js/apps/base/main.ts index 755a924193..687eacfe0b 100644 --- a/static/js/apps/base/main.ts +++ b/static/js/apps/base/main.ts @@ -32,29 +32,24 @@ window.addEventListener("load", (): void => { }); function renderPage(): void { + const metadataContainer = document.getElementById("metadata-base"); + const headerMenu = JSON.parse( - document.getElementById("metadata-base").dataset.header + metadataContainer.dataset.header ) as HeaderMenu[]; const footerMenu = JSON.parse( - document.getElementById("metadata-base").dataset.footer + metadataContainer.dataset.footer ) as FooterMenu[]; - const name = document.getElementById("metadata-base").dataset.name; - const logoPath = document.getElementById("metadata-base").dataset.logoPath; + const name = metadataContainer.dataset.name; + const logoPath = metadataContainer.dataset.logoPath; const hideFullFooter = - document - .getElementById("metadata-base") - .dataset.hideFullFooter.toLowerCase() === "true"; + metadataContainer.dataset.hideFullFooter.toLowerCase() === "true"; const hideSubFooter = - document - .getElementById("metadata-base") - .dataset.hideSubFooter.toLowerCase() === "true"; - const subFooterExtra = - document.getElementById("metadata-base").dataset.subfooterExtra; + metadataContainer.dataset.hideSubFooter.toLowerCase() === "true"; + const subFooterExtra = metadataContainer.dataset.subfooterExtra; const brandLogoLight = - document - .getElementById("metadata-base") - .dataset.brandLogoLight.toLowerCase() === "true"; + metadataContainer.dataset.brandLogoLight.toLowerCase() === "true"; const labels = extractLabels(); const routes = extractRoutes(); diff --git a/static/js/apps/homepage/main_v2.ts b/static/js/apps/homepage/main_v2.ts index caf11c3545..f44cdff005 100644 --- a/static/js/apps/homepage/main_v2.ts +++ b/static/js/apps/homepage/main_v2.ts @@ -35,12 +35,10 @@ window.addEventListener("load", (): void => { }); function renderPage(): void { - const topics = JSON.parse( - document.getElementById("metadata-homepage").dataset.topics - ) as Topic[]; - const partners = JSON.parse( - document.getElementById("metadata-homepage").dataset.partners - ); + const metadataContainer = document.getElementById("metadata-homepage"); + + const topics = JSON.parse(metadataContainer.dataset.topics) as Topic[]; + const partners = JSON.parse(metadataContainer.dataset.partners); const routes = extractRoutes(); From d468964e60936aafc5f14ad000ed010f718e7b4f Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 21:23:59 -0700 Subject: [PATCH 08/92] Commenting of interfaces, interface properties and files. --- static/js/apps/base/components/footer.tsx | 11 ++++++++++ static/js/apps/base/components/header_bar.tsx | 9 ++++++++ static/js/apps/base/footer_app.tsx | 11 ++++++++++ static/js/apps/base/header_app.tsx | 9 ++++++++ static/js/apps/base/main.ts | 5 +++++ .../js/apps/homepage/{app.tsx => app_v2.tsx} | 5 ++++- .../js/apps/homepage/components/data_size.tsx | 4 ++++ .../apps/homepage/components/learn_more.tsx | 5 +++++ .../js/apps/homepage/components/partners.tsx | 5 +++++ .../homepage/components/search_animation.tsx | 4 ++++ static/js/apps/homepage/components/tools.tsx | 5 +++++ static/js/apps/homepage/components/topics.tsx | 5 +++++ static/js/apps/homepage/main.ts | 2 ++ static/js/apps/homepage/main_v2.ts | 4 ++-- static/js/shared/types/base.ts | 22 +++++++++++++++++++ static/js/shared/types/homepage.ts | 16 ++++++++++++++ 16 files changed, 119 insertions(+), 3 deletions(-) rename static/js/apps/homepage/{app.tsx => app_v2.tsx} (89%) diff --git a/static/js/apps/base/components/footer.tsx b/static/js/apps/base/components/footer.tsx index e30e9d4eef..af5453d804 100644 --- a/static/js/apps/base/components/footer.tsx +++ b/static/js/apps/base/components/footer.tsx @@ -14,18 +14,29 @@ * limitations under the License. */ +/** + * A component that renders the footer on all pages via the base template. + */ + import React, { ReactElement, useMemo } from "react"; import { FooterMenu, Labels, Routes } from "../../../shared/types/base"; import { resolveHref } from "../utilities/utilities"; interface FooterProps { + //if true, the larger top-level footer will not display hideFullFooter: boolean; + //if true, the smaller sub footer will not display hideSubFooter: boolean; + //extra text (that can be html) that can be injected into the sub-footer. subFooterExtra: string; + //if true, will display an alternate, lighter version of the logo. brandLogoLight: boolean; + //the data that will populate the footer menu. footerMenu: FooterMenu[]; + //the labels dictionary - all labels will be passed through this before being rendered. If no value exists, the dictionary will return the key that was sent. labels: Labels; + //the routes dictionary - this is used to convert routes to resolved urls routes: Routes; } diff --git a/static/js/apps/base/components/header_bar.tsx b/static/js/apps/base/components/header_bar.tsx index 008aa18cce..ceb24ab350 100644 --- a/static/js/apps/base/components/header_bar.tsx +++ b/static/js/apps/base/components/header_bar.tsx @@ -14,16 +14,25 @@ * limitations under the License. */ +/** + * A component that renders the header on all pages via the base template. + */ + import React, { ReactElement, useMemo } from "react"; import { HeaderMenu, Labels, Routes } from "../../../shared/types/base"; import { resolveHref, slugify } from "../utilities/utilities"; interface HeaderBarProps { + //the name of the application (this may not be "Data Commons" in forked versions). name: string; + //a path to the logo to be displayed in the header logoPath: string; + //the data that will populate the header menu. menu: HeaderMenu[]; + //the labels dictionary - all labels will be passed through this before being rendered. If no value exists, the dictionary will return the key that was sent. labels: Labels; + //the routes dictionary - this is used to convert routes to resolved urls routes: Routes; } diff --git a/static/js/apps/base/footer_app.tsx b/static/js/apps/base/footer_app.tsx index e9d14a9db4..ba48b657ac 100644 --- a/static/js/apps/base/footer_app.tsx +++ b/static/js/apps/base/footer_app.tsx @@ -14,18 +14,29 @@ * limitations under the License. */ +/** + * The app that renders the footer component on all pages via the base template + */ + import React, { ReactElement } from "react"; import { FooterMenu, Labels, Routes } from "../../shared/types/base"; import Footer from "./components/footer"; interface FooterAppProps { + //if true, the larger top-level footer will not display hideFullFooter: boolean; + //if true, the smaller sub footer will not display hideSubFooter: boolean; + //extra text (that can be html) that can be injected into the sub-footer. subFooterExtra: string; + //if true, will display an alternate, lighter version of the logo. brandLogoLight: boolean; + //the data that will populate the footer menu. footerMenu: FooterMenu[]; + //the labels dictionary - all labels will be passed through this before being rendered. If no value exists, the dictionary will return the key that was sent. labels: Labels; + //the routes dictionary - this is used to convert routes to resolved urls routes: Routes; } diff --git a/static/js/apps/base/header_app.tsx b/static/js/apps/base/header_app.tsx index cb18586c7c..acea198000 100644 --- a/static/js/apps/base/header_app.tsx +++ b/static/js/apps/base/header_app.tsx @@ -14,16 +14,25 @@ * limitations under the License. */ +/** + * The app that renders the header component on all pages via the base template + */ + import React, { ReactElement } from "react"; import { HeaderMenu, Labels, Routes } from "../../shared/types/base"; import HeaderBar from "./components/header_bar"; interface HeaderAppProps { + //the name of the application (this may not be "Data Commons" in forked versions). name: string; + //a path to the logo to be displayed in the header logoPath: string; + //the data that will populate the header menu. headerMenu: HeaderMenu[]; + //the labels dictionary - all labels will be passed through this before being rendered. If no value exists, the dictionary will return the key that was sent. labels: Labels; + //the routes dictionary - this is used to convert routes to resolved urls routes: Routes; } diff --git a/static/js/apps/base/main.ts b/static/js/apps/base/main.ts index 687eacfe0b..c5d0298b04 100644 --- a/static/js/apps/base/main.ts +++ b/static/js/apps/base/main.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +/** + * Entry point for the base template. This file will render two apps: one for the header and one for the footer. + */ + import React from "react"; import ReactDOM from "react-dom"; @@ -51,6 +55,7 @@ function renderPage(): void { const brandLogoLight = metadataContainer.dataset.brandLogoLight.toLowerCase() === "true"; + //TODO: Move to internationalization library const labels = extractLabels(); const routes = extractRoutes(); diff --git a/static/js/apps/homepage/app.tsx b/static/js/apps/homepage/app_v2.tsx similarity index 89% rename from static/js/apps/homepage/app.tsx rename to static/js/apps/homepage/app_v2.tsx index c9f5f16573..41ebb6a14b 100644 --- a/static/js/apps/homepage/app.tsx +++ b/static/js/apps/homepage/app_v2.tsx @@ -15,7 +15,7 @@ */ /** - * Main component for homepage. + * Main component for Version 2 of the homepage. */ import React, { ReactElement } from "react"; @@ -38,8 +38,11 @@ import Tools from "./components/tools"; import Topics from "./components/topics"; interface AppProps { + //the topics passed from the backend through to the JavaScript via the templates topics: Topic[]; + //the partners passed from the backend through to the JavaScript via the templates partners: Partner[]; + //the routes dictionary - this is used to convert routes to resolved urls routes: Routes; } diff --git a/static/js/apps/homepage/components/data_size.tsx b/static/js/apps/homepage/components/data_size.tsx index e2ad900307..ec37b16302 100644 --- a/static/js/apps/homepage/components/data_size.tsx +++ b/static/js/apps/homepage/components/data_size.tsx @@ -14,6 +14,10 @@ * limitations under the License. */ +/** + * A component that renders the data size section of the home page. + */ + import React, { ReactElement } from "react"; const DataSize = (): ReactElement => { diff --git a/static/js/apps/homepage/components/learn_more.tsx b/static/js/apps/homepage/components/learn_more.tsx index 5b412528a2..56bed171b8 100644 --- a/static/js/apps/homepage/components/learn_more.tsx +++ b/static/js/apps/homepage/components/learn_more.tsx @@ -14,12 +14,17 @@ * limitations under the License. */ +/** + * A component that renders the learn more section of the home page. + */ + import React, { ReactElement } from "react"; import { Routes } from "../../../shared/types/base"; import { resolveHref } from "../../base/utilities/utilities"; interface LearnMoreProps { + //the routes dictionary - this is used to convert routes to resolved urls routes: Routes; } diff --git a/static/js/apps/homepage/components/partners.tsx b/static/js/apps/homepage/components/partners.tsx index c3ce5001fb..b4ab72611c 100644 --- a/static/js/apps/homepage/components/partners.tsx +++ b/static/js/apps/homepage/components/partners.tsx @@ -14,11 +14,16 @@ * limitations under the License. */ +/** + * A component that renders the partners section of the home page. + */ + import React, { ReactElement } from "react"; import { Partner } from "../../../shared/types/homepage"; interface PartnersProps { + //the partners passed from the backend through to the JavaScript via the templates partners: Partner[]; } diff --git a/static/js/apps/homepage/components/search_animation.tsx b/static/js/apps/homepage/components/search_animation.tsx index 90893258c2..d05513b30e 100644 --- a/static/js/apps/homepage/components/search_animation.tsx +++ b/static/js/apps/homepage/components/search_animation.tsx @@ -14,6 +14,10 @@ * limitations under the License. */ +/** + * A component that renders the search animation section of the home page. + */ + import React, { ReactElement, useEffect, useRef } from "react"; const SearchAnimation = (): ReactElement => { diff --git a/static/js/apps/homepage/components/tools.tsx b/static/js/apps/homepage/components/tools.tsx index 3bc569c91e..1651183254 100644 --- a/static/js/apps/homepage/components/tools.tsx +++ b/static/js/apps/homepage/components/tools.tsx @@ -14,11 +14,16 @@ * limitations under the License. */ +/** + * A component that renders the tools section of the home page. + */ + import React, { ReactElement } from "react"; import { resolveHref } from "../../base/utilities/utilities"; interface ToolsProps { + //the routes dictionary - this is used to convert routes to resolved urls routes: Record; } diff --git a/static/js/apps/homepage/components/topics.tsx b/static/js/apps/homepage/components/topics.tsx index aa8b48e448..94d567fb58 100644 --- a/static/js/apps/homepage/components/topics.tsx +++ b/static/js/apps/homepage/components/topics.tsx @@ -14,11 +14,16 @@ * limitations under the License. */ +/** + * A component that renders the topics section of the home page. + */ + import React, { ReactElement } from "react"; import { Topic } from "../../../shared/types/homepage"; interface TopicsProps { + //the topics passed from the backend through to the JavaScript via the templates topics: Topic[]; } diff --git a/static/js/apps/homepage/main.ts b/static/js/apps/homepage/main.ts index c7987618b5..ad81c41366 100644 --- a/static/js/apps/homepage/main.ts +++ b/static/js/apps/homepage/main.ts @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + /** * Entrypoint file for homepage. */ + import React from "react"; import ReactDOM from "react-dom"; diff --git a/static/js/apps/homepage/main_v2.ts b/static/js/apps/homepage/main_v2.ts index f44cdff005..c705ad88f4 100644 --- a/static/js/apps/homepage/main_v2.ts +++ b/static/js/apps/homepage/main_v2.ts @@ -15,7 +15,7 @@ */ /** - * Entrypoint file for homepage. + * Entry point for Version 2 of the home page. */ import React from "react"; @@ -24,7 +24,7 @@ import ReactDOM from "react-dom"; import { loadLocaleData } from "../../i18n/i18n"; import { Topic } from "../../shared/types/homepage"; import { extractRoutes } from "../base/utilities/utilities"; -import { App } from "./app"; +import { App } from "./app_v2"; window.addEventListener("load", (): void => { loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( diff --git a/static/js/shared/types/base.ts b/static/js/shared/types/base.ts index 70bab4558f..b130a58c2c 100644 --- a/static/js/shared/types/base.ts +++ b/static/js/shared/types/base.ts @@ -18,28 +18,50 @@ * Typing for components and TypeScript associated with the base template. */ +// The top level of the header menu export interface HeaderMenu { + //the label that displays in the top level menu item of the header label: string; + //the aria-label attribute for that header ariaLabel: string; + //the children of the top-level menu (these populate the dropdowns that appear on click) subMenu: HeaderSubMenu[]; } +//The entries that populate the dropdown menus in the header menu export interface HeaderSubMenu { + //the link of the entry - these can be either a route, a url, or a route embedded into a string: {tools.visualization}#visType=timeline) href: string; + //the label (the text) of the link label: string; + //an option to hide the entry - this is to allow the json to contain items (for later forking) that will not display on the menu (such as BigQuery) hide?: boolean; } +//The sections of the footer menu export interface FooterMenu { + //the label of the section: this appears directly above the list of links. label: string; + //the list of links that appear in the section subMenu: FooterSubMenu[]; } +//The entries that populate a section of the footer. interface FooterSubMenu { + //the link of the entry - these can be either a route, a url, or a route embedded into a string: {tools.visualization}#visType=timeline) href: string; + //the label (the text) of the link label: string; + //an option to hide the entry - this is to allow the json to contain items (for later forking) that will not display on the menu (such as BigQuery) hide?: boolean; } +//A dictionary of routes. These map route names, such as "tools.visualization" to the resolved route. +//These are implemented as a proxy object that will return the text of the key if the key is not found. +//This is to allow the flexibility using both urls and routes in source data such as JSON that will be passed through the object. export type Routes = Record; + +//A dictionary of labels. These map words or phrases, such as "Data Commons" to a version that has been passed through a {% trans %} tag in the template. +//These are implemented as a proxy object that will return the text of the key if the key is not found. +//This is to allow the flexibility of putting in text in the source data such as JSON that may not have yet been provided. export type Labels = Record; diff --git a/static/js/shared/types/homepage.ts b/static/js/shared/types/homepage.ts index 403e8103eb..29ee349a41 100644 --- a/static/js/shared/types/homepage.ts +++ b/static/js/shared/types/homepage.ts @@ -18,20 +18,36 @@ * Typing for components and TypeScript associated with the homepage template. */ +//An interface for the partner objects that are passed through to the JavaScript from the template. +//These are then used to render the partner links on the homepage. export interface Partner { + //the id of the partner id: string; + //the name of the partner title: string; + //the url that the partner box links to url: string; + //the sprite index of the partner's logo "sprite-index": number; } +//An interface for the topic objects that are passed through to the JavaScript from the template. +//These are used to render the topics links on the homepage export interface Topic { + //the id of the topic id: string; + //the title of the topic title: string; + //a blurb describing the topic description: string; + //the icon is currently unused by the homepage template icon: string; + //the image is unused by the homepage template image: string; + //the url is unused by the homepage template url: string; + //the url that the topic box links to browseUrl: string; + //the sprite index of the partner's logo "sprite-index": number; } From ea913a3a4e756a98aa09d5c4a29eaee5cc2a8fab Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 21:30:48 -0700 Subject: [PATCH 09/92] This push brings back the application container for the original home page, and some minor TypeScript fixes. We also added in some missing comments. --- static/js/apps/base/footer_app.tsx | 3 ++ static/js/apps/base/header_app.tsx | 3 ++ static/js/apps/homepage/app.tsx | 50 ++++++++++++++++++++++++++++++ static/js/apps/homepage/main.ts | 4 +-- 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 static/js/apps/homepage/app.tsx diff --git a/static/js/apps/base/footer_app.tsx b/static/js/apps/base/footer_app.tsx index ba48b657ac..bc857cf80e 100644 --- a/static/js/apps/base/footer_app.tsx +++ b/static/js/apps/base/footer_app.tsx @@ -40,6 +40,9 @@ interface FooterAppProps { routes: Routes; } +/** + * Footer application container + */ export function FooterApp({ hideFullFooter, hideSubFooter, diff --git a/static/js/apps/base/header_app.tsx b/static/js/apps/base/header_app.tsx index acea198000..841f170d32 100644 --- a/static/js/apps/base/header_app.tsx +++ b/static/js/apps/base/header_app.tsx @@ -36,6 +36,9 @@ interface HeaderAppProps { routes: Routes; } +/** + * Header application container + */ export function HeaderApp({ name, logoPath, diff --git a/static/js/apps/homepage/app.tsx b/static/js/apps/homepage/app.tsx new file mode 100644 index 0000000000..bbfde15cd1 --- /dev/null +++ b/static/js/apps/homepage/app.tsx @@ -0,0 +1,50 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Main component for homepage. + */ +import React, { ReactElement } from "react"; + +import { NlSearchBar } from "../../components/nl_search_bar"; +import { + GA_EVENT_NL_SEARCH, + GA_PARAM_QUERY, + GA_PARAM_SOURCE, + GA_VALUE_SEARCH_SOURCE_HOMEPAGE, + triggerGAEvent, +} from "../../shared/ga_events"; + +/** + * Application container + */ +export function App(): ReactElement { + return ( + { + triggerGAEvent(GA_EVENT_NL_SEARCH, { + [GA_PARAM_QUERY]: q, + [GA_PARAM_SOURCE]: GA_VALUE_SEARCH_SOURCE_HOMEPAGE, + }); + window.location.href = `/explore#q=${encodeURIComponent(q)}`; + }} + placeholder={"Enter a question to explore"} + initialValue={""} + shouldAutoFocus={false} + /> + ); +} diff --git a/static/js/apps/homepage/main.ts b/static/js/apps/homepage/main.ts index ad81c41366..ffa6273af9 100644 --- a/static/js/apps/homepage/main.ts +++ b/static/js/apps/homepage/main.ts @@ -66,7 +66,7 @@ window.addEventListener("load", (): void => { ); const resultsElList = svgDiv.getElementsByClassName("result"); - searchSequenceContainer.onclick = () => { + searchSequenceContainer.onclick = (): void => { if (prompt) { window.location.href = `/explore#q=${encodeURIComponent( prompt.dataset.query @@ -74,7 +74,7 @@ window.addEventListener("load", (): void => { } }; - function startNextPrompt() { + function startNextPrompt(): void { let inputLength = 0; if (currentPromptIndex < resultsElList.length) { prompt = resultsElList.item(currentPromptIndex); From db7146f1e8d01f73cf972ce66bbb8259371a7697 Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 21:48:46 -0700 Subject: [PATCH 10/92] Used macros to help render the label and route data containers. We were somewhat limited in how efficiently we could clean up the labels with macros because of the translators tags. --- server/templates/metadata/labels.html | 104 +++++++++++++++++--------- server/templates/metadata/routes.html | 30 +++++--- 2 files changed, 87 insertions(+), 47 deletions(-) diff --git a/server/templates/metadata/labels.html b/server/templates/metadata/labels.html index 1ec75eb5c4..58d7af9422 100644 --- a/server/templates/metadata/labels.html +++ b/server/templates/metadata/labels.html @@ -14,68 +14,98 @@ limitations under the License. -#} +{% macro render_metadata_label(label, trans_block=None) %} + {% set translation_value = trans_block if trans_block else label %} +
+{% endmacro %} +
- {# TRANSLATORS: The label for a link to informational pages about the Data Commons project. #} -
-
-
-
-
+ {# TRANSLATORS: The label for a link back to the homepage with a project-specific name. #} + {{ render_metadata_label('Back to homepage', 'Back to {{ NAME }} homepage') }} + + {{ render_metadata_label('Show site navigation') }} + {{ render_metadata_label('Show exploration tools') }} + {{ render_metadata_label('Explore') }} + {# TRANSLATORS: The name of a tool to browse statistics about a place. #} -
+ {{ render_metadata_label('Place Explorer') }} + {# TRANSLATORS: The name of a tool to browse the Data Commons knowledge graph. #} -
+ {{ render_metadata_label('Knowledge Graph') }} + {# TRANSLATORS: The name of a tool to explore timeline charts of statistical variables for places. #} -
+ {{ render_metadata_label('Timelines Explorer') }} + {# TRANSLATORS: The name of a tool to explore scatter plots of statistical variables for places. #} -
+ {{ render_metadata_label('Scatter Plot Explorer') }} + {# TRANSLATORS: The name of a tool to explore maps of statistical variables for places. #} -
+ {{ render_metadata_label('Map Explorer') }} + {# TRANSLATORS: The name of a tool that provides observation and import information about statistical variables. #} -
+ {{ render_metadata_label('Statistical Variable Explorer') }} + {# TRANSLATORS: The name of a tool that allows users to download data. #} -
+ {{ render_metadata_label('Data Download Tool') }} + {# TRANSLATORS: The name of a dashboard that shows information about natural disasters. #} -
-
-
+ {{ render_metadata_label('Natural Disaster Dashboard') }} + + {{ render_metadata_label('Sustainability Explorer') }} + {{ render_metadata_label('Show documentation links') }} + {# TRANSLATORS: The label for a list of documentation links. #} -
+ {{ render_metadata_label('Documentation') }} + {# TRANSLATORS: The label for a link to our API documentation. #} -
+ {{ render_metadata_label('APIs') }} + {# TRANSLATORS: The label for a link to BigQuery integration starter docs. #} -
+ {{ render_metadata_label('BigQuery') }} + {# TRANSLATORS: The label for a link to our API tutorials. #} -
+ {{ render_metadata_label('Tutorials') }} + {# TRANSLATORS: The label for a link to instructions about contributing to the project. #} -
+ {{ render_metadata_label('Contribute') }} + {# TRANSLATORS: The label for a link to the project's GitHub repository (for open sourced code). #} -
-
-
+ {{ render_metadata_label('Github Repository') }} + + {{ render_metadata_label('Show about links') }} + {{ render_metadata_label('About') }} + {# TRANSLATORS: The label for a link to the project's about page. #} -
+ {{ render_metadata_label('About Data Commons') }} + {# TRANSLATORS: The label for a link to the project's blog. #} -
+ {{ render_metadata_label('Blog') }} + {# TRANSLATORS: The label for a link to data sources included in the Data Commons knowledge graph. #} -
+ {{ render_metadata_label('Data Sources') }} + {# TRANSLATORS: The label for a link to project FAQ page (abbreviated version). #} -
+ {{ render_metadata_label('FAQ') }} + {# TRANSLATORS: The label for a link to project FAQ page. #} -
+ {{ render_metadata_label('Frequently Asked Questions') }} + {# TRANSLATORS: The label for a link to instructions about sending feedback. #} -
+ {{ render_metadata_label('Feedback') }} + {# TRANSLATORS: The label for a collection of exploration tools. #} -
- {# TRANSLATORS: The label for a link to instructions about sending feedback. #} -
+ {{ render_metadata_label('Tools') }} + {# TRANSLATORS: The label for the Google branding byline. #} -
+ {{ render_metadata_label('An initiative from') }} + {# TRANSLATORS: The label for a link to site terms and conditions. #} -
+ {{ render_metadata_label('Terms and Conditions') }} + {# TRANSLATORS: The label for a link to site privacy policy. #} -
+ {{ render_metadata_label('Privacy Policy') }} + {# TRANSLATORS: The label for a link to site disclaimers. #} -
+ {{ render_metadata_label('Disclaimers') }}
diff --git a/server/templates/metadata/routes.html b/server/templates/metadata/routes.html index 15258a1162..6ad7a998ed 100644 --- a/server/templates/metadata/routes.html +++ b/server/templates/metadata/routes.html @@ -14,16 +14,26 @@ limitations under the License. -#} +{% macro render_metadata_routes(routes) %}
-
-
-
-
-
-
-
-
-
-
+ {% for route_name in routes %} +
+ {% endfor %}
+{% endmacro %} + +{% set routes = [ + 'static.homepage', + 'place.place', + 'browser.browser_main', + 'tools.visualization', + 'tools.stat_var', + 'tools.download', + 'static.about', + 'static.feedback', + 'static.faq', + 'static.disclaimers' +] %} + +{{ render_metadata_routes(routes) }} From 4bf9b1853c82b6c0a5b1640b8013854d2566707b Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 23:03:29 -0700 Subject: [PATCH 11/92] Updating the label and route proxy objects to take the type as a generic. --- static/js/apps/base/utilities/utilities.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/static/js/apps/base/utilities/utilities.ts b/static/js/apps/base/utilities/utilities.ts index bafc06daa9..e8251dd28c 100644 --- a/static/js/apps/base/utilities/utilities.ts +++ b/static/js/apps/base/utilities/utilities.ts @@ -62,10 +62,13 @@ export const slugify = (text: string): string => { */ export const extractRoutes = (elementId = "metadata-routes"): Routes => { const routeElements = document.getElementById(elementId)?.children; - const routes: Routes = new Proxy( + const routes: Routes = new Proxy( {}, { get: (target, prop): string => { + if (typeof prop === "symbol") { + throw new Error("Invalid property key."); + } return prop in target ? target[prop] : (prop as string); }, } @@ -90,10 +93,13 @@ export const extractRoutes = (elementId = "metadata-routes"): Routes => { */ export const extractLabels = (elementId = "metadata-labels"): Labels => { const labelElements = document.getElementById(elementId)?.children; - const labels: Labels = new Proxy( + const labels = new Proxy( {}, { get: (target, prop): string => { + if (typeof prop === "symbol") { + throw new Error("Invalid property key."); + } return prop in target ? target[prop] : (prop as string); }, } From 8ff02ae369603e4a701d5e9e48de7928a7e9b173 Mon Sep 17 00:00:00 2001 From: Nick B Date: Wed, 4 Sep 2024 23:29:43 -0700 Subject: [PATCH 12/92] The label proxy object now emits a log when the requested label does not exist. --- server/templates/metadata/labels.html | 3 +++ static/js/apps/base/utilities/utilities.ts | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/server/templates/metadata/labels.html b/server/templates/metadata/labels.html index 58d7af9422..4f6e45fe48 100644 --- a/server/templates/metadata/labels.html +++ b/server/templates/metadata/labels.html @@ -20,6 +20,9 @@ {% endmacro %}
+ {# TRANSLATORS: The label for a link to informational pages about the Data Commons project. #} + {{ render_metadata_label('Data Commons') }} + {# TRANSLATORS: The label for a link back to the homepage with a project-specific name. #} {{ render_metadata_label('Back to homepage', 'Back to {{ NAME }} homepage') }} diff --git a/static/js/apps/base/utilities/utilities.ts b/static/js/apps/base/utilities/utilities.ts index e8251dd28c..d7da473138 100644 --- a/static/js/apps/base/utilities/utilities.ts +++ b/static/js/apps/base/utilities/utilities.ts @@ -100,7 +100,15 @@ export const extractLabels = (elementId = "metadata-labels"): Labels => { if (typeof prop === "symbol") { throw new Error("Invalid property key."); } - return prop in target ? target[prop] : (prop as string); + + if (prop in target) { + return target[prop]; + } else { + console.log( + `Requested label "${prop}" does not exist in labels dictionary.` + ); + return prop as string; + } }, } ); From 9d354012aed8e9a5d4c907982485d035e8728196 Mon Sep 17 00:00:00 2001 From: Nick B Date: Thu, 5 Sep 2024 11:02:57 -0700 Subject: [PATCH 13/92] Template routes must now be wrapped in `{}` in order to be resolved, and non-existent routes now throw errors. --- server/config/base/footer.json | 14 +++++++------- server/config/base/header.json | 14 +++++++------- static/js/apps/base/utilities/utilities.ts | 14 +++++++++----- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/server/config/base/footer.json b/server/config/base/footer.json index 17eb98a2fb..8b755e896e 100644 --- a/server/config/base/footer.json +++ b/server/config/base/footer.json @@ -3,11 +3,11 @@ "label": "Tools", "subMenu": [ { - "href": "place.place", + "href": "{place.place}", "label": "Place Explorer" }, { - "href": "browser.browser_main", + "href": "{browser.browser_main}", "label": "Knowledge Graph" }, { @@ -23,11 +23,11 @@ "label": "Map Explorer" }, { - "href": "tools.stat_var", + "href": "{tools.stat_var}", "label": "Statistical Variable Explorer" }, { - "href": "tools.download", + "href": "{tools.download}", "label": "Data Download Tool" } ] @@ -66,7 +66,7 @@ "label": "Data Commons", "subMenu": [ { - "href": "static.about", + "href": "{static.about}", "label": "About Data Commons" }, { @@ -78,11 +78,11 @@ "label": "Data Sources" }, { - "href": "static.feedback", + "href": "{static.feedback}", "label": "Feedback" }, { - "href": "static.faq", + "href": "{static.faq}", "label": "Frequently Asked Questions" } ] diff --git a/server/config/base/header.json b/server/config/base/header.json index 0a4cd87384..0a04fccb2a 100644 --- a/server/config/base/header.json +++ b/server/config/base/header.json @@ -4,11 +4,11 @@ "ariaLabel": "Show exploration tools", "subMenu": [ { - "href": "place.place", + "href": "{place.place}", "label": "Place Explorer" }, { - "href": "browser.browser_main", + "href": "{browser.browser_main}", "label": "Knowledge Graph" }, { @@ -24,11 +24,11 @@ "label": "Map Explorer" }, { - "href": "tools.stat_var", + "href": "{tools.stat_var}", "label": "Statistical Variable Explorer" }, { - "href": "tools.download", + "href": "{tools.download}", "label": "Data Download Tool" } ] @@ -65,7 +65,7 @@ "ariaLabel": "Show about links", "subMenu": [ { - "href": "static.about", + "href": "{static.about}", "label": "About Data Commons" }, { @@ -77,11 +77,11 @@ "label": "Data Sources" }, { - "href": "static.faq", + "href": "{static.faq}", "label": "FAQ" }, { - "href": "static.feedback", + "href": "{static.feedback}", "label": "Feedback" } ] diff --git a/static/js/apps/base/utilities/utilities.ts b/static/js/apps/base/utilities/utilities.ts index d7da473138..6b089b0cb2 100644 --- a/static/js/apps/base/utilities/utilities.ts +++ b/static/js/apps/base/utilities/utilities.ts @@ -17,9 +17,10 @@ import { Labels, Routes } from "../../../shared/types/base"; /* - This function takes a string that is either a pure url, a route (such as static.homepage) - or a route wrapped in {} located inside a string (such as {tools.visualization}#visType=timeline), - returns the string converted into a url. + This function takes a string that may contain a route from the template wrapped in {}. + The string may be a pure URL with no route, a route such as "{static.homepage}", or + a route embedded into a string such as "{tools.visualization}#visType=timeline". + The function will return the string with the route converted. The purpose of the function is to flexibly resolve strings from sources such as JSON that may contain either routes or raw URLs and to return the final URL. @@ -33,7 +34,7 @@ export const resolveHref = (href: string, routes: Routes): string => { const resolvedRoute = routes[routeKey] || ""; return href.replace(regex, resolvedRoute); } else { - return routes[href] || href; + return href; } }; @@ -69,7 +70,10 @@ export const extractRoutes = (elementId = "metadata-routes"): Routes => { if (typeof prop === "symbol") { throw new Error("Invalid property key."); } - return prop in target ? target[prop] : (prop as string); + if (!(prop in target)) { + throw new Error(`Route not found: ${String(prop)}`); + } + return target[prop]; }, } ); From 6ea848fca350371bcdd9110017bc5308abc9a533 Mon Sep 17 00:00:00 2001 From: Nick B Date: Thu, 5 Sep 2024 17:48:27 -0700 Subject: [PATCH 14/92] WIP: A functional implementation of the rich menu. A note that this has not been styled to match the design. As part of this commit, we have defined the header structure for the data source that populates the rich menu (a somewhat more complicated structure than the previous header). We have separated the natural languages search bar component into a parent that calls one of two variant implementations in order to accommodate both the header version (which will be quite different in how it looks) and the standard version. --- server/__init__.py | 3 +- server/config/base/header_v2.json | 205 +++++++++++++++++ server/templates/base.html | 2 +- static/css/header.scss | 217 ++++++++++++++++++ static/css/homepage.scss | 1 + static/js/apps/base/components/header_bar.tsx | 102 ++------ .../base/components/header_bar_search.tsx | 49 ++++ .../js/apps/base/components/menu_desktop.tsx | 101 ++++++++ .../components/menu_desktop_rich_menu.tsx | 69 ++++++ .../components/menu_desktop_section_group.tsx | 88 +++++++ static/js/apps/base/header_app.tsx | 4 +- static/js/apps/base/main.ts | 4 +- static/js/apps/homepage/app_v2.tsx | 26 --- static/js/components/nl_search_bar.tsx | 92 ++++---- .../nl_search_bar_header_inline.tsx | 59 +++++ .../nl_search_bar/nl_search_bar_standard.tsx | 73 ++++++ static/js/shared/types/base.ts | 50 ++++ 17 files changed, 990 insertions(+), 155 deletions(-) create mode 100644 server/config/base/header_v2.json create mode 100644 static/css/header.scss create mode 100644 static/js/apps/base/components/header_bar_search.tsx create mode 100644 static/js/apps/base/components/menu_desktop.tsx create mode 100644 static/js/apps/base/components/menu_desktop_rich_menu.tsx create mode 100644 static/js/apps/base/components/menu_desktop_section_group.tsx create mode 100644 static/js/components/nl_search_bar/nl_search_bar_header_inline.tsx create mode 100644 static/js/components/nl_search_bar/nl_search_bar_standard.tsx diff --git a/server/__init__.py b/server/__init__.py index 7959daceb6..46d0a5eba2 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -444,7 +444,8 @@ def add_language_code(endpoint, values): def inject_common_parameters(): common_variables = { 'HEADER_MENU': json.dumps(libutil.get_json("config/base/header.json")), - 'FOOTER_MENU': json.dumps(libutil.get_json("config/base/footer.json")) + 'FOOTER_MENU': json.dumps(libutil.get_json("config/base/footer.json")), + 'HEADER_MENU_V2': json.dumps(libutil.get_json("config/base/header_v2.json")), } locale_variable = dict(locale=get_locale()) return {**common_variables, **locale_variable} diff --git a/server/config/base/header_v2.json b/server/config/base/header_v2.json new file mode 100644 index 0000000000..6f8b74518e --- /dev/null +++ b/server/config/base/header_v2.json @@ -0,0 +1,205 @@ +[ + { + "label": "Overview", + "ariaLabel": "Data commons overview", + "introduction": "Data Commons is an open-source platform aggregating global public data for easy exploration using natural language.", + "primarySectionGroups": [ + { + "title": "Key Features", + "items": [ + { + "title": "Large harmonized public dataset", + "description": "240 billion data points across 260K statistical variables, harmonized from governmental, inter-governmental, academic and non-profit organizations" + }, + { + "title": "Natural language interface", + "description": "Data Commons uses AI for natural language queries, making public data accessible and useful to all" + } + ] + }, + { + "title": "Build your Data Commons", + "items": [ + { + "title": "Tailor your own Data Commons", + "description": "Launch your own Data Commons and customize it with your own data to better engage your specific audience" + }, + { + "title": "Explore your data with natural language", + "description": "Ask questions in your own words and get answers directly from your data" + }, + { + "title": "Actionable Insights", + "description": "Gain actionable insights from your data in connection to global data", + "links": [ + { + "title": "Learn more & build yours today", + "url": "{place.place}" + } + ] + } + ] + } + ], + "secondarySectionGroups": [ + { + "title": "Other Data Commons", + "items": [ + { + "title": "Biomedical Data Commons", + "url": "https://docs.datacommons.org/datasets/Biomedical.html", + "type": "external", + "description": "Data Commons has invested into harmonized other domain specific topics like Biomedical data" + } + ] + }, + { + "items": [ + { + "title": "Partners", + "description": "Featured organizations who organizations have tailored their own Data Commons to meet their specific needs and goals", + "links": [ + { + "title": "United Nations", + "url": "https://www.un.org", + "linkType": "external" + }, + { + "title": "One.org", + "url": "https://one.org", + "linkType": "external" + } + ] + } + ] + } + ] + }, + { + "label": "Tools", + "ariaLabel": "Show exploration tools", + "introduction": "Explore a variety of tools to visualize, analyze, and interact with the Data Commons knowledge graph and its extensive datasets.", + "primarySectionGroups": [ + { + "items": [ + { + "title": "Knowledge Graph", + "url": "{browser.browser_main}", + "description": "Explore what data is available and understand the graph structure" + }, + { + "title": "Statistical Variable Explorer", + "url": "{tools.stat_var}", + "description": "Explore statistical variable details including metadata and observations" + }, + { + "title": "Data Download Tool", + "url": "{tools.download}", + "description": "Download data for selected statistical variables" + } + ] + }, + { + "items": [ + { + "title": "Map Explorer", + "url": "{tools.visualization}#visType=map", + "description": "Study how a selected statistical variable can vary across geographic regions" + }, + { + "title": "Scatter Plot Explorer", + "description": "Visualize the correlation between two statistical variables" + }, + { + "title": "Timelines Explorer", + "description": "See trends over time for selected statistical variables" + } + ] + } + ] + }, + { + "label": "Documentation", + "ariaLabel": "Show documentation links", + "introduction": "Access in-depth tutorials, guides, and API references to unlock the full potential of Data Commons and integrate it into your projects.", + "primarySectionGroups": [ + { + "items": [ + { + "title": "Docs", + "url": "https://docs.datacommons.org", + "description": "Learn how to leverage the Data Commons unified database with comprehensive documentation, tutorials, and guides." + }, + { + "title": "API", + "url": "https://docs.datacommons.org/api", + "description": "Access a unified knowledge graph with standardized data from diverse sources using Data Commons APIs." + } + ] + }, + { + "items": [ + { + "title": "Tutorials", + "url": "https://docs.datacommons.org/tutorials", + "description": "Get familiar with the Data Commons Knowledge Graph and APIs using analysis examples in Google Colab notebooks written in Python." + }, + { + "title": "Contributions", + "url": "https://docs.datacommons.org/contributing/", + "description": "Become part of Data Commons by contributing data, tools, educational materials, or sharing your analysis and insights. Collaborate and help expand the knowledge graph!" + } + ] + } + ] + }, + { + "label": "About", + "ariaLabel": "Show about links", + "introduction": "Data Commons is an initiative from Google. Discover how Data Commons is changing data analysis. Explore diverse data, learn to use its tools through Python examples, and stay updated on the latest news and research.", + "primarySectionGroups": [ + { + "items": [ + { + "title": "Why Data Commons", + "url": "{static.about}", + "description": "Discover why Data Commons is revolutionizing data access and analysis. Learn how its unified knowledge graph empowers you to explore diverse, standardized data." + }, + { + "title": "Data Sources", + "url": "https://docs.datacommons.org/datasets/", + "description": "Get familiar with the Data Commons Knowledge Graph and APIs using analysis examples in Google Colab notebooks written in Python.", + "links": [ + { + "title": "Data Updates", + "url": "https://www.datacommons.org/rss", + "linkType": "rss" + } + ] + } + ] + }, + { + "items": [ + { + "title": "FAQ", + "url": "{static.faq}", + "description": "Find quick answers to common questions about Data Commons, its usage, data sources, and available resources." + }, + { + "title": "Blog", + "url": "https://blog.datacommons.org/", + "description": "Stay up-to-date with the latest news, updates, and insights from the Data Commons team. Explore new features, research, and educational content related to the project.", + "links": [ + { + "title": "Blog posts", + "url": "https://blog.datacommons.org/rss", + "linkType": "rss" + } + ] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/server/templates/base.html b/server/templates/base.html index 63eba3f566..2955260631 100644 --- a/server/templates/base.html +++ b/server/templates/base.html @@ -95,7 +95,7 @@
{ - const visibleMenu = useMemo(() => { - return menu.map((menuItem) => ({ - ...menuItem, - subMenu: menuItem.subMenu.filter((subMenuItem) => !subMenuItem.hide), - })); - }, [menu]); + console.log(menu); return ( -
-
From 6eae1b4427ca1cfb59e9e28137d1d730c3ba67a6 Mon Sep 17 00:00:00 2001 From: Pablo Noel Date: Fri, 6 Sep 2024 17:51:09 -0400 Subject: [PATCH 30/92] Breakpoints and responsive spacing --- static/css/header.scss | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/static/css/header.scss b/static/css/header.scss index be96ebefcf..cb22f7f724 100644 --- a/static/css/header.scss +++ b/static/css/header.scss @@ -16,12 +16,16 @@ /* Styles for the header reflected in version 2 of the homepage */ -@import "node_modules/bootstrap/scss/bootstrap"; +@import "base"; + +// Component Variables $font-family-google-title: "Google Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; $font-family-google-text: "Google Sans Text", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + +// Styles #main-nav-v2 { display: flex; @@ -49,11 +53,16 @@ $font-family-google-text: "Google Sans Text", Arial, sans-serif, flex-shrink: 0; align-items: center; gap: 0.75rem; - padding: 0 0 0 1rem; white-space: nowrap; font-size: 1.1rem; line-height: 24px; } + @include media-breakpoint-up(xl) { + padding: 0 0 0 32px; + } + @include media-breakpoint-down(xl) { + padding: 0 0 0 16px; + } .panel { width: 100%; @@ -76,12 +85,13 @@ $font-family-google-text: "Google Sans Text", Arial, sans-serif, align-items: stretch; list-style: none; margin: 0; - padding: 0 32px; @include media-breakpoint-up(xl) { gap: 32px; + padding: 0 32px; } @include media-breakpoint-down(xl) { gap: 16px; + padding: 0 16px 0 0; } li { @@ -245,7 +255,12 @@ $font-family-google-text: "Google Sans Text", Arial, sans-serif, flex-grow: 1; height: fit-content; margin: 1rem 0; - padding-right: 64px; + @include media-breakpoint-up(xl) { + padding-right: 64px; + } + @include media-breakpoint-down(xl) { + padding-right: 16px; + } .btn { height: 2.5rem; From b449369a619dccd3981a3b4a7b8f010b01928971 Mon Sep 17 00:00:00 2001 From: Nick B Date: Fri, 6 Sep 2024 15:05:44 -0700 Subject: [PATCH 31/92] Removal of stray console log and some formatting. --- static/js/apps/base/components/header_bar.tsx | 2 -- .../js/components/nl_search_bar/nl_search_bar_header_inline.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/static/js/apps/base/components/header_bar.tsx b/static/js/apps/base/components/header_bar.tsx index 377aeeed4f..b5d7c10d5c 100644 --- a/static/js/apps/base/components/header_bar.tsx +++ b/static/js/apps/base/components/header_bar.tsx @@ -45,8 +45,6 @@ const HeaderBar = ({ labels, routes, }: HeaderBarProps): ReactElement => { - console.log(menu); - return (
From cff31149e1fd2ae3a9fbc62df8a9f28b2490d76b Mon Sep 17 00:00:00 2001 From: Pablo Noel Date: Sat, 7 Sep 2024 19:05:12 -0400 Subject: [PATCH 45/92] New homepage initial commit Bring changes and work from previous branch to this consolidated version. --- server/templates/static/homepage.html | 2 +- static/css/homepage_v2.scss | 350 ++++++++++++++++++ static/js/apps/homepage/app_v2.tsx | 27 +- static/js/apps/homepage/components/build.tsx | 48 +++ .../js/apps/homepage/components/data_size.tsx | 83 ----- static/js/apps/homepage/components/hero.tsx | 32 ++ .../apps/homepage/components/learn_more.tsx | 72 ---- .../js/apps/homepage/components/partners.tsx | 46 ++- .../homepage/components/sample_questions.tsx | 135 +++++++ .../homepage/components/search_animation.tsx | 301 --------------- static/js/apps/homepage/components/tools.tsx | 57 +-- static/js/apps/homepage/components/topics.tsx | 51 +-- static/webpack.config.js | 1 + 13 files changed, 645 insertions(+), 560 deletions(-) create mode 100644 static/css/homepage_v2.scss create mode 100644 static/js/apps/homepage/components/build.tsx delete mode 100644 static/js/apps/homepage/components/data_size.tsx create mode 100644 static/js/apps/homepage/components/hero.tsx delete mode 100644 static/js/apps/homepage/components/learn_more.tsx create mode 100644 static/js/apps/homepage/components/sample_questions.tsx delete mode 100644 static/js/apps/homepage/components/search_animation.tsx diff --git a/server/templates/static/homepage.html b/server/templates/static/homepage.html index f7c4556dc2..d31af8b12e 100644 --- a/server/templates/static/homepage.html +++ b/server/templates/static/homepage.html @@ -25,7 +25,7 @@ {%- endmacro %} {% block head %} - + {% endblock %} diff --git a/static/css/homepage_v2.scss b/static/css/homepage_v2.scss new file mode 100644 index 0000000000..f48dd046f8 --- /dev/null +++ b/static/css/homepage_v2.scss @@ -0,0 +1,350 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Styles for the homepage */ +@use "./nl_search_bar"; +@import "base"; + +$spacing: 8px; +$container-horizontal-padding: 3rem; + +$color-white-container: #F8FAFD; + +$color-blue:#0B57D0; +$color-blue_pill_text:#041E49; +$color-blue_pill_bckg:#E8F0FE; +$color-blue-bckg: #F6F9FF; + +$color-green:#146C2E; +$color-green_pill_text:#072711; +$color-green_pill_bckg:#C4EED0; + +$color-red:#B3261E; +$color-red_pill_text:#410E0B; +$color-red_pill_bckg:#F9DEDC; + +$color-yellow:#945700; +$color-yellow_pill_text:#410E0B; +$color-yellow_pill_bckg:#FFF0D1; + +$color-gray:#444746; +$color-gray_pill_text:#945700; +$color-gray_pill_bckg:#E2E2EC; + +@mixin shadow{ + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15); +} + +@mixin white-box{ + @include shadow; + background-color: $color-white-container; + text-decoration: none; + &:hover{ + background-color: rgba(11, 87, 208, 0.08); + } +} + +@mixin big-text{ + font-size: 1.4rem; + line-height: 1.73rem; +} + +#homepage { + // General Styles + h3{ + display: block; + font-size: 1.4rem; + line-height: 1.75rem; + font-weight: 400; + color: #072711; + margin-bottom: calc(#{$spacing} * 4); + } + .container{ + padding: $container-horizontal-padding 0; + } + .big-description{ + margin-bottom: calc(#{$spacing} * 4); + max-width: 650px; + h3{ + font-size: 2rem; + line-height: 2.5rem; + } + p{ + @include big-text; + } + } + // Hero section + .hero{ + background-color: $color-blue-bckg; + } + // Topics section + .topics-container{ + margin: 0; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: calc(#{$spacing} * 2); + max-width: 80%; + @include media-breakpoint-down(md) { + max-width: 100%; + } + } + .topic-item { + display: block; + list-style: none; + a{ + @include white-box; + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: calc(#{$spacing} * 10); + padding: 10px calc(#{$spacing} * 3) 10px calc(#{$spacing} * 2); + } + } + // Questions section + .questions-carousel{ + display: flex; + gap: calc(#{$spacing} * 3); + margin-bottom: calc(#{$spacing} * 3); + } + .questions-carousel-dots{ + display: flex; + justify-content: center; + width: 100%; + gap: $spacing; + margin: auto; + padding: 0; + .questions-carousel-dot{ + display: block; + margin: 0; + padding: 0; + a{ + display: block; + text-decoration: none; + display: inline-block; + overflow: hidden; + text-indent: -1000em; + width: 10px; + height: 10px; + background: white; + border: 1px solid #{$color-blue}; + border-radius: 100px; + } + &.active a{ + background: $color-blue; + } + } + } + .questions-column{ + display: flex; + flex-direction: column; + gap: calc(#{$spacing} * 3); + } + .question-item { + display: block; + list-style: none; + a{ + @include white-box; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; + padding: calc(#{$spacing} * 3); + border-radius: calc(#{$spacing} * 4); + p{ + font-size: 1.6rem; + line-height: 2rem; + font-weight: 400; + } + small{ + display: inline-block; + padding: 2px 8px; + font-size: 12px; + line-height: 16px; + border-radius: 24px; + } + } + &.green a{ + p { + color: $color-green; + } + small{ + color: $color-green_pill_text; + background-color: $color-green_pill_bckg; + } + } + &.blue a{ + p { + color: $color-blue; + } + small{ + color: $color-blue_pill_text; + background-color: $color-blue_pill_bckg; + } + } + &.red a{ + p { + color: $color-red; + } + small{ + color: $color-red_pill_text; + background-color: $color-red_pill_bckg; + } + } + &.yellow a{ + p { + color: $color-yellow; + } + small{ + color: $color-yellow_pill_text; + background-color: $color-yellow_pill_bckg; + } + } + &.gray a{ + p { + color: $color-gray; + } + small{ + color: $color-gray_pill_text; + background-color: $color-gray_pill_bckg; + } + } + } + // Tools section + .tools{ + padding-top: calc(#{$spacing} * 10); + padding-bottom: calc(#{$spacing} * 10); + background: $color-blue-bckg; + } + .tools-buttons{ + display: flex; + align-items: stretch; + flex-wrap: wrap; + gap: calc(#{$spacing} * 3); + margin: 0; + padding: 0; + li{ + display: block; + padding: 0; + margin: 0; + flex-basis: 200px; + a{ + @include white-box; + display: flex; + width: 100%; + height: 100%; + flex-direction: column; + align-items: flex-start; + gap: calc(#{$spacing} * 2); + padding: calc(#{$spacing} * 3); + border-radius: calc(#{$spacing} * 4); + font-size: 1.75rem; + line-height: 2.25rem; + } + } + } + .tool-icon{ + display: block; + width: 32px; + height: 32px; + background-repeat: no-repeat; + background-position: center center; + background-size: 100%; + &.timeline{ + background-image: url('/images/icons/icon-timeline.svg') + } + &.api{ + background-image: url('/images/icons/icon-api.svg') + } + &.download{ + background-image: url('/images/icons/icon-download.svg') + } + &.map{ + background-image: url('/images/icons/icon-map.svg') + } + &.scaterplot{ + background-image: url('/images/icons/icon-scaterplot.svg') + } + } + // Build Your Own section + .video-container{ + display: grid; + grid-template-columns: 1fr 1fr; + gap: calc(#{$spacing} * 6); + } + .video-description{ + p{ + @include big-text; + } + } + .video-player{ + box-sizing: border-box; + position: relative; + width: 100%; + display: block; + padding-bottom: 56.25%; + border-radius: 20px; + overflow: hidden; + & > iframe{ + margin: 0; + padding: 0; + border: 0; + box-sizing: border-box; + display: block; + width: 100%; + height: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + } + // Partners section + .partners-items{ + display: flex; + justify-content: space-around; + flex-wrap: wrap; + gap: calc(#{$spacing} * 3); + margin: 0; + margin-top: calc(#{$spacing} * 3); + padding: 0; + li{ + display: block; + margin: 0; + padding: 0; + a{ + display: block; + border-radius: 300px; + transition: transform 0.3s ease-in-out, border 0.3s ease-in-out; + border: 2px solid transparent; + img{ + display: block; + width: 100%; + height: auto; + max-width: 90px; + } + &:hover{ + @include shadow; + transform: translateY(-5px); + border: 2px solid white; + } + } + } + } + +} \ No newline at end of file diff --git a/static/js/apps/homepage/app_v2.tsx b/static/js/apps/homepage/app_v2.tsx index ca6aff8f0d..52cdcc7195 100644 --- a/static/js/apps/homepage/app_v2.tsx +++ b/static/js/apps/homepage/app_v2.tsx @@ -24,12 +24,12 @@ import React, { ReactElement } from "react"; import { Routes } from "../../shared/types/base"; import { Partner, Topic } from "../../shared/types/homepage"; -import DataSize from "./components/data_size"; -import LearnMore from "./components/learn_more"; import Partners from "./components/partners"; -import SearchAnimation from "./components/search_animation"; import Tools from "./components/tools"; import Topics from "./components/topics"; +import Build from "./components/build"; +import Hero from "./components/hero"; +import SampleQuestions from "./components/sample_questions"; interface AppProps { //the topics passed from the backend through to the JavaScript via the templates @@ -46,21 +46,12 @@ interface AppProps { export function App({ topics, partners, routes }: AppProps): ReactElement { return ( <> - - - - -
- -
- -
- -
- - - -
+ + + + + + ); } diff --git a/static/js/apps/homepage/components/build.tsx b/static/js/apps/homepage/components/build.tsx new file mode 100644 index 0000000000..33afa5b529 --- /dev/null +++ b/static/js/apps/homepage/components/build.tsx @@ -0,0 +1,48 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +const Build = (): ReactElement => { + return ( +
+
+
+

Build your own Data Commons

+

Deploying your own Data Commons lets you create a tailored platform showcasing relevant data and tools to engage your audience. Any entity or organization can build their own instance with specific data, a dedicated website, and specialized tools.

+
+

United Nations Data Commons for the SDGs

+
+
\ + +
+
+

United Nations deployed a Data Commons to amplify the impact of their Sustainable Development Goals data. With their deployed Data Commons, the UN created a centralized repository, allowing for dynamic storytelling and targeted analysis related to global progress. Learn more

+
+
+
+
+ ); +}; + +export default Build; \ No newline at end of file diff --git a/static/js/apps/homepage/components/data_size.tsx b/static/js/apps/homepage/components/data_size.tsx deleted file mode 100644 index ec37b16302..0000000000 --- a/static/js/apps/homepage/components/data_size.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * A component that renders the data size section of the home page. - */ - -import React, { ReactElement } from "react"; - -const DataSize = (): ReactElement => { - return ( -
-
-
-
-

Data from

-
    -
  • 193 countries
  • -
  • 110,000 cities
  • -
  • 5,000 states and provinces
  • -
-
- -
-
-
-
-
-
- ); -}; - -export default DataSize; diff --git a/static/js/apps/homepage/components/hero.tsx b/static/js/apps/homepage/components/hero.tsx new file mode 100644 index 0000000000..fd90426c49 --- /dev/null +++ b/static/js/apps/homepage/components/hero.tsx @@ -0,0 +1,32 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +const Hero = (): ReactElement => { + return ( +
+
+
+

Data Commons aggregates and harmonizes global, open data, giving everyone the power to uncover insights with natural language questions

+

Data Commons' open source foundation allows organizations to create tailored, private instances, deciding on the openness of their data contributions. Build yours today

+
+
+
+ ); +}; + +export default Hero; \ No newline at end of file diff --git a/static/js/apps/homepage/components/learn_more.tsx b/static/js/apps/homepage/components/learn_more.tsx deleted file mode 100644 index 2c83cf17cb..0000000000 --- a/static/js/apps/homepage/components/learn_more.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * A component that renders the learn more section of the home page. - */ - -import React, { ReactElement } from "react"; - -import { Routes } from "../../../shared/types/base"; -import { resolveHref } from "../../base/utilities/utilities"; - -interface LearnMoreProps { - //the routes dictionary - this is used to convert routes to resolved urls - routes: Routes; -} - -const LearnMore = ({ routes }: LearnMoreProps): ReactElement => { - return ( -
-
-
-
-
-
-
-

Learn More

-

- Data forms the foundation of science, policy, and journalism, but - its full potential is often limited. Data Commons addresses this - by offering cloud-based APIs to access and integrate cleaned - datasets, boosting their usability across different domains. -

- -
-
-
-
- ); -}; - -export default LearnMore; diff --git a/static/js/apps/homepage/components/partners.tsx b/static/js/apps/homepage/components/partners.tsx index b4ab72611c..0a7468ef6f 100644 --- a/static/js/apps/homepage/components/partners.tsx +++ b/static/js/apps/homepage/components/partners.tsx @@ -29,31 +29,27 @@ interface PartnersProps { const Partners = ({ partners }: PartnersProps): ReactElement => { return ( -
-
-
-

Our Partners

-
- {partners.map((partner) => ( - -
-
- ))} -
-
-
-
+
+
+

Other organizations with a Data Commons

+
    + {partners.map((partner) => ( +
  • + + {partner.title} + +
  • + ))} +
+
+
); }; diff --git a/static/js/apps/homepage/components/sample_questions.tsx b/static/js/apps/homepage/components/sample_questions.tsx new file mode 100644 index 0000000000..11e629e39d --- /dev/null +++ b/static/js/apps/homepage/components/sample_questions.tsx @@ -0,0 +1,135 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +const SampleQuestions = (): ReactElement => { + return ( +
+
+

Sample Questions

+
+ + + + + +
+ +
+
+ ); +}; + +export default SampleQuestions; \ No newline at end of file diff --git a/static/js/apps/homepage/components/search_animation.tsx b/static/js/apps/homepage/components/search_animation.tsx deleted file mode 100644 index d05513b30e..0000000000 --- a/static/js/apps/homepage/components/search_animation.tsx +++ /dev/null @@ -1,301 +0,0 @@ -/** - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * A component that renders the search animation section of the home page. - */ - -import React, { ReactElement, useEffect, useRef } from "react"; - -const SearchAnimation = (): ReactElement => { - const CHARACTER_INPUT_INTERVAL_MS = 45; - const NEXT_PROMPT_DELAY_MS = 5000; - const INITIAL_MISSION_ON_SCREEN_DELAY_MS = 2000; - const INITIAL_MISSION_FADE_IN_DELAY_MS = 1000; - const ANSWER_DELAY_MS = 2000; - const FADE_OUT_MS = 800; - const FADE_OUT_CLASS = "fade-out"; - const HIDDEN_CLASS = "hidden"; - const SLIDE_DOWN_CLASS = "slide-down"; - const INVISIBLE_CLASS = "invisible"; - const FADE_IN_CLASS = "fade-in"; - const ANIMATION_TOGGLE_COOKIE_NAME = "keepAnimationClosed"; - const ANIMATION_TOGGLE_MARGIN = "6px"; - const MAX_COOKIE_AGE = 60 * 60 * 24; - - const currentPromptIndex = useRef(0); - const currentPrompt = useRef(null); - const inputIntervalTimer = useRef>(); - const nextInputTimer = useRef>(); - const inputEl = useRef(null); - const searchAnimationContainer = useRef(null); - const searchSequenceContainer = useRef(null); - const defaultTextContainer = useRef(null); - const svgDiv = useRef(null); - const promptDiv = useRef(null); - const missionDiv = useRef(null); - const resultsElList = useRef>(); - - useEffect(() => { - resultsElList.current = document.querySelectorAll("#result-svg .result"); - - if ( - !resultsElList.current || - !promptDiv.current || - !svgDiv.current || - !missionDiv.current || - !defaultTextContainer.current || - !searchSequenceContainer.current - ) { - return; - } - - const startNextPrompt = (): void => { - let inputLength = 0; - const prompt = resultsElList.current.item( - currentPromptIndex.current - ) as HTMLElement; - currentPrompt.current = prompt; - - if (currentPromptIndex.current >= resultsElList.current.length) { - setTimeout(() => { - defaultTextContainer.current.classList.remove(FADE_OUT_CLASS); - }, FADE_OUT_MS); - searchSequenceContainer.current.classList.add(FADE_OUT_CLASS); - clearInterval(nextInputTimer.current); - return; - } - - if (currentPromptIndex.current === 0) { - defaultTextContainer.current.classList.add(FADE_OUT_CLASS); - searchSequenceContainer.current.classList.remove(HIDDEN_CLASS); - } else { - resultsElList.current - .item(currentPromptIndex.current - 1) - .classList.add(FADE_OUT_CLASS); - } - - setTimeout(() => { - if (currentPromptIndex.current === 0) { - defaultTextContainer.current.classList.add(FADE_OUT_CLASS); - svgDiv.current.classList.remove(HIDDEN_CLASS); - promptDiv.current.classList.add(HIDDEN_CLASS); - missionDiv.current.classList.remove(HIDDEN_CLASS); - } - prompt.classList.remove(HIDDEN_CLASS); - prompt.classList.add(SLIDE_DOWN_CLASS); - - if (currentPromptIndex.current > 0) { - resultsElList.current - .item(currentPromptIndex.current - 1) - .classList.add(HIDDEN_CLASS); - } - currentPromptIndex.current++; - }, ANSWER_DELAY_MS); - - inputIntervalTimer.current = setInterval(() => { - if (inputLength <= prompt.dataset.query.length) { - if (inputEl.current) { - inputEl.current.value = - prompt.dataset.query?.substring(0, inputLength) || ""; - inputEl.current.scrollLeft = inputEl.current.scrollWidth; - } - inputLength++; - } else { - clearInterval(inputIntervalTimer.current); - } - }, CHARACTER_INPUT_INTERVAL_MS); - }; - - setTimeout(() => { - promptDiv.current.classList.remove(INVISIBLE_CLASS); - promptDiv.current.classList.add(FADE_IN_CLASS); - setTimeout(() => { - startNextPrompt(); - nextInputTimer.current = setInterval(() => { - startNextPrompt(); - }, NEXT_PROMPT_DELAY_MS); - }, INITIAL_MISSION_ON_SCREEN_DELAY_MS); - }, INITIAL_MISSION_FADE_IN_DELAY_MS); - - const hideAnimation = (): void => { - searchAnimationContainer.current.setAttribute("style", "display: none;"); - const searchAnimationToggle = document.getElementById( - "search-animation-toggle" - ); - searchAnimationToggle.classList.add(HIDDEN_CLASS); - searchAnimationToggle.innerHTML = - "keyboard_double_arrow_down"; - searchAnimationToggle.setAttribute( - "style", - `margin-top: ${ANIMATION_TOGGLE_MARGIN};` - ); - document.cookie = `${ANIMATION_TOGGLE_COOKIE_NAME}=true;max-age=${MAX_COOKIE_AGE};`; - }; - - const showAnimation = (): void => { - searchAnimationContainer.current.setAttribute( - "style", - "display: visible;" - ); - const searchAnimationToggle = document.getElementById( - "search-animation-toggle" - ); - searchAnimationToggle.classList.remove(HIDDEN_CLASS); - searchAnimationToggle.innerHTML = - "keyboard_double_arrow_up"; - searchAnimationToggle.setAttribute( - "style", - `margin-bottom: ${ANIMATION_TOGGLE_MARGIN};` - ); - document.cookie = `${ANIMATION_TOGGLE_COOKIE_NAME}=;max-age=0;`; - }; - - const searchAnimationToggle = document.getElementById( - "search-animation-toggle" - ); - searchAnimationToggle.addEventListener("click", () => { - if (searchAnimationToggle.classList.contains(HIDDEN_CLASS)) { - showAnimation(); - } else { - hideAnimation(); - } - }); - - if ( - document.cookie - .split(";") - .some((item) => item.includes(`${ANIMATION_TOGGLE_COOKIE_NAME}=true`)) - ) { - hideAnimation(); - } - - return () => { - clearInterval(nextInputTimer.current); - clearInterval(inputIntervalTimer.current); - }; - }, [MAX_COOKIE_AGE]); - - const handleSearchSequenceClick = (): void => { - if (currentPrompt.current) { - const query = currentPrompt.current.dataset.query; - if (query) { - window.location.href = `/explore#q=${encodeURIComponent(query)}`; - } - } - }; - - return ( - <> -
-
-
-
-

Data tells interesting stories

-

- Ask a question like... -

-

- Data Commons, an initiative from Google, -
- organizes the world’s publicly available data -
- and makes it more accessible and useful -

-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- - keyboard_double_arrow_up - -
- - ); -}; - -export default SearchAnimation; diff --git a/static/js/apps/homepage/components/tools.tsx b/static/js/apps/homepage/components/tools.tsx index ada13a6e95..2049a7370a 100644 --- a/static/js/apps/homepage/components/tools.tsx +++ b/static/js/apps/homepage/components/tools.tsx @@ -31,62 +31,63 @@ const Tools = ({ routes }: ToolsProps): ReactElement => { return (
-
-

Data Commons Tools

-

- Explore the public database through these tools -

+
+

Data Commons Tools

+

Data forms the foundation of science, policy, and journalism, but its full potential is often limited. Data Commons addresses this by offering data exploration tools and cloud-based APIs to access and integrate cleaned datasets, boosting their usability across different domains.

- + +
); diff --git a/static/js/apps/homepage/components/topics.tsx b/static/js/apps/homepage/components/topics.tsx index 94d567fb58..5503aa9f27 100644 --- a/static/js/apps/homepage/components/topics.tsx +++ b/static/js/apps/homepage/components/topics.tsx @@ -29,39 +29,26 @@ interface TopicsProps { const Topics = ({ topics }: TopicsProps): ReactElement => { return ( -
-

Explore the Data

-
- {topics.map((topic) => ( -
{ - window.location.href = topic.browseUrl; - }} - > -
-
-
-
-
{topic.title}
-
{topic.description}
- -
-
- ))} +
+
+

Topics to explore

+
-
+ ); }; diff --git a/static/webpack.config.js b/static/webpack.config.js index df6f47d89e..f64fb5cec0 100644 --- a/static/webpack.config.js +++ b/static/webpack.config.js @@ -146,6 +146,7 @@ const config = { homepage: [ __dirname + "/js/apps/homepage/main.ts", __dirname + "/css/homepage.scss", + __dirname + "/css/homepage_v2.scss", ], homepage_v2: [ __dirname + "/js/apps/homepage/main_v2.ts", From c9b6dae0324225131930873b2a50a4c929a7468a Mon Sep 17 00:00:00 2001 From: Nick B Date: Sat, 7 Sep 2024 16:54:57 -0700 Subject: [PATCH 46/92] Scaffolding for the two new pages to be built: a revamp of the about page, and the "Build your own Data Commmons" page. --- server/routes/static.py | 5 + server/templates/static/about.html | 102 ++---------------- server/templates/static/build.html | 32 ++++++ static/css/about.scss | 18 ++++ static/css/build.scss | 18 ++++ static/js/apps/about/app.tsx | 40 +++++++ .../js/apps/about/components/splash_quote.tsx | 40 +++++++ static/js/apps/about/main.ts | 45 ++++++++ static/js/apps/build/app.tsx | 40 +++++++ static/js/apps/build/components/hero.tsx | 35 ++++++ static/js/apps/build/main.ts | 45 ++++++++ static/webpack.config.js | 10 +- 12 files changed, 333 insertions(+), 97 deletions(-) create mode 100644 server/templates/static/build.html create mode 100644 static/css/about.scss create mode 100644 static/css/build.scss create mode 100644 static/js/apps/about/app.tsx create mode 100644 static/js/apps/about/components/splash_quote.tsx create mode 100644 static/js/apps/about/main.ts create mode 100644 static/js/apps/build/app.tsx create mode 100644 static/js/apps/build/components/hero.tsx create mode 100644 static/js/apps/build/main.ts diff --git a/server/routes/static.py b/server/routes/static.py index 01a4bd98b4..cb51161713 100644 --- a/server/routes/static.py +++ b/server/routes/static.py @@ -49,6 +49,11 @@ def about(): return lib_render.render_page("static/about.html", "about.html") +@bp.route('/build') +def build(): + return lib_render.render_page("static/build.html", "build.html") + + @bp.route('/faq') def faq(): current_date = date.today().strftime('%-d %b %Y') diff --git a/server/templates/static/about.html b/server/templates/static/about.html index f624018a25..91762e30ce 100644 --- a/server/templates/static/about.html +++ b/server/templates/static/about.html @@ -1,5 +1,5 @@ {# - Copyright 2020 Google LLC + Copyright 2024 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,103 +20,13 @@ {% set title = 'About Us' %} {% block head %} - + + {% endblock %} {% block content %} -
-

About Data Commons

-
- -
-
-

Why Data Commons

-

- Many of the big challenges we face — climate change, increasing - inequities, epidemics of diabetes and other health conditions — will - need deep insights to solve. These insights will need to be firmly - grounded in data. Fortunately, a lot of this data is already publicly - available. Unfortunately, there is a difference between data being public - and data being easily usable by those who need access to it. It is this - gap that we are trying to bridge. -

-

- Google has organized and made easily accessible many kinds of information - — web pages, images, maps, videos and so on. Now we are doing this for - publicly available data. We have organized the core of this data from a - wide range of sources, ranging from governmental statistical organizations - like census bureaus to organizations like the World Bank and the United - Nations. -

-

- And recent advances in AI have enabled us to go much farther than we had - thought possible in making this data easily accessible - now you can use - natural language to access the data. Still early days, but we are very - bullish on it. -

-

- Data Commons synthesizes a single graph from - these different data sources. It links references to the same entities - (such - as cities, counties, organizations, etc.) across different datasets to - nodes - on the graph, so that users can access data about a particular entity - aggregated from different sources without data cleaning or joining. We - hope the data - contained within Data Commons will be useful to students, researchers, and - enthusiasts across different disciplines. -

-
-
-

Who can use it?

-

- Data Commons can be accessed by anyone at Datacommons.org! There - are tools available for students, researchers, journalists, non profits, - policymakers, and private enterprises that allow you to manipulate and - make decisions based on data without the need to know how to code. - Software developers can use the REST, Python and - Google Sheets APIs, all of which are free for educational, academic - and journalistic research purposes. -

-
-
-

Collaborations

-

- Data Commons has benefited greatly from many collaborations. In addition - to help from US Department of Commerce (notably the Census Bureau), we - have received help from our many academic collaborations, including, - University of California San Francisco, Stanford University, University of - California Berkeley, Harvard University and Indian Institute of Technology - Madras. We have also collaborated with nonprofits such as Techsoup, - Feeding America, and Resources for the Future. -

-

- We are looking for more collaborators, both for adding new data to Data - Commons and for building new and interesting applications of Data Commons. - Please fill out this form if you are interested in working - with us. -

-
-
-

Stay in touch

-

- Sign up for our announcement mailing list to stay up to date with our - latest developments. Join the list by clicking here, then clicking the “Join group” button. -

-
-
-

See also

- -
+
+
+ {% endblock %} diff --git a/server/templates/static/build.html b/server/templates/static/build.html new file mode 100644 index 0000000000..fd348c8055 --- /dev/null +++ b/server/templates/static/build.html @@ -0,0 +1,32 @@ +{# + Copyright 2024 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +#} +{%- extends BASE_HTML -%} + +{% set main_id = 'build' %} +{% set page_id = 'page-build' %} +{% set title = 'Build your own Data Commons' %} + +{% block head %} + + +{% endblock %} + +{% block content %} + +
+
+ + {% endblock %} diff --git a/static/css/about.scss b/static/css/about.scss new file mode 100644 index 0000000000..3c35b03bc1 --- /dev/null +++ b/static/css/about.scss @@ -0,0 +1,18 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Styles for the about page */ +@import "base"; diff --git a/static/css/build.scss b/static/css/build.scss new file mode 100644 index 0000000000..3c35b03bc1 --- /dev/null +++ b/static/css/build.scss @@ -0,0 +1,18 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Styles for the about page */ +@import "base"; diff --git a/static/js/apps/about/app.tsx b/static/js/apps/about/app.tsx new file mode 100644 index 0000000000..c3a4bd5545 --- /dev/null +++ b/static/js/apps/about/app.tsx @@ -0,0 +1,40 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Main component for the about page + */ + +import React, { ReactElement } from "react"; + +import { Routes } from "../../shared/types/base"; +import SplashQuote from "./components/splash_quote"; + +interface AppProps { + //the routes dictionary - this is used to convert routes to resolved urls + routes: Routes; +} + +/** + * Application container + */ +export function App({ routes }: AppProps): ReactElement { + return ( + <> + + + ); +} diff --git a/static/js/apps/about/components/splash_quote.tsx b/static/js/apps/about/components/splash_quote.tsx new file mode 100644 index 0000000000..ca372a7640 --- /dev/null +++ b/static/js/apps/about/components/splash_quote.tsx @@ -0,0 +1,40 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +/** + * A component to render the splash quote of the about page. + */ + +const SplashQuote = (): ReactElement => { + return ( +
+

+ “Every moment around the world people and organizations are generating + data that can be extraordinary useful and I think we have to find the + way to harness that to solve problems. The challenge is that a lot of + this data is very fragmented.” +

+

+ James Manyika, Senior Vice President, Research, Technology & Society at + Google +

+
+ ); +}; + +export default SplashQuote; diff --git a/static/js/apps/about/main.ts b/static/js/apps/about/main.ts new file mode 100644 index 0000000000..50d41c937e --- /dev/null +++ b/static/js/apps/about/main.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Entry point for the about page. + */ + +import React from "react"; +import ReactDOM from "react-dom"; + +import { loadLocaleData } from "../../i18n/i18n"; +import { extractRoutes } from "../base/utilities/utilities"; +import { App } from "./app"; + +window.addEventListener("load", (): void => { + loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( + () => { + renderPage(); + } + ); +}); + +function renderPage(): void { + const routes = extractRoutes(); + + ReactDOM.render( + React.createElement(App, { + routes, + }), + document.getElementById("app-container") + ); +} diff --git a/static/js/apps/build/app.tsx b/static/js/apps/build/app.tsx new file mode 100644 index 0000000000..163c24fd97 --- /dev/null +++ b/static/js/apps/build/app.tsx @@ -0,0 +1,40 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Main component for the build your own Data Commons page + */ + +import React, { ReactElement } from "react"; + +import { Routes } from "../../shared/types/base"; +import Hero from "./components/hero"; + +interface AppProps { + //the routes dictionary - this is used to convert routes to resolved urls + routes: Routes; +} + +/** + * Application container + */ +export function App({ routes }: AppProps): ReactElement { + return ( + <> + + + ); +} diff --git a/static/js/apps/build/components/hero.tsx b/static/js/apps/build/components/hero.tsx new file mode 100644 index 0000000000..c52b8a6ec5 --- /dev/null +++ b/static/js/apps/build/components/hero.tsx @@ -0,0 +1,35 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactElement } from "react"; + +/** + * A component to render the hero section of the build your own Data Commons page. + */ + +const Hero = (): ReactElement => { + return ( +
+

+ Build your Data Commons, overlay your data with global data, and let + everyone in your organization uncover insights with natural language + questions. Learn how +

+
+ ); +}; + +export default Hero; diff --git a/static/js/apps/build/main.ts b/static/js/apps/build/main.ts new file mode 100644 index 0000000000..191e799f75 --- /dev/null +++ b/static/js/apps/build/main.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Entry point for the build your own Data Commons page. + */ + +import React from "react"; +import ReactDOM from "react-dom"; + +import { loadLocaleData } from "../../i18n/i18n"; +import { extractRoutes } from "../base/utilities/utilities"; +import { App } from "./app"; + +window.addEventListener("load", (): void => { + loadLocaleData("en", [import("../../i18n/compiled-lang/en/units.json")]).then( + () => { + renderPage(); + } + ); +}); + +function renderPage(): void { + const routes = extractRoutes(); + + ReactDOM.render( + React.createElement(App, { + routes, + }), + document.getElementById("app-container") + ); +} diff --git a/static/webpack.config.js b/static/webpack.config.js index f64fb5cec0..b993bc9375 100644 --- a/static/webpack.config.js +++ b/static/webpack.config.js @@ -129,7 +129,15 @@ const config = { __dirname + "/js/import_wizard2/import_wizard.ts", __dirname + "/css/import_wizard2.scss", ], + about: [ + __dirname + "/js/apps/about/main.ts", + __dirname + "/css/about.scss", + ], admin: [__dirname + "/js/admin/main.ts", __dirname + "/css/admin.scss"], + build: [ + __dirname + "/js/apps/build/main.ts", + __dirname + "/css/build.scss", + ], disaster_dashboard: [ __dirname + "/js/apps/disaster_dashboard/main.ts", __dirname + "/css/disaster_dashboard.scss", @@ -146,10 +154,10 @@ const config = { homepage: [ __dirname + "/js/apps/homepage/main.ts", __dirname + "/css/homepage.scss", - __dirname + "/css/homepage_v2.scss", ], homepage_v2: [ __dirname + "/js/apps/homepage/main_v2.ts", + __dirname + "/css/homepage_v2.scss", ], homepage_custom_dc: [ __dirname + "/js/apps/homepage/main_custom_dc.ts", From f8bf97ef38d687ae73ad1f281c541dab208d7376 Mon Sep 17 00:00:00 2001 From: Pablo Noel Date: Sat, 7 Sep 2024 20:10:46 -0400 Subject: [PATCH 47/92] New Images --- static/images/icons/icon-api.svg | 3 +++ static/images/icons/icon-download.svg | 3 +++ static/images/icons/icon-map.svg | 3 +++ static/images/icons/icon-scaterplot.svg | 3 +++ static/images/icons/icon-timeline.svg | 3 +++ static/images/partners/logo_feedingAmerica.png | Bin 0 -> 5543 bytes static/images/partners/logo_iit.png | Bin 0 -> 27696 bytes static/images/partners/logo_rff.png | Bin 0 -> 8906 bytes static/images/partners/logo_stanford.png | Bin 0 -> 36572 bytes static/images/partners/logo_techsoup.png | Bin 0 -> 8011 bytes static/images/partners/logo_unSdg.png | Bin 0 -> 11114 bytes 11 files changed, 15 insertions(+) create mode 100644 static/images/icons/icon-api.svg create mode 100644 static/images/icons/icon-download.svg create mode 100644 static/images/icons/icon-map.svg create mode 100644 static/images/icons/icon-scaterplot.svg create mode 100644 static/images/icons/icon-timeline.svg create mode 100644 static/images/partners/logo_feedingAmerica.png create mode 100644 static/images/partners/logo_iit.png create mode 100644 static/images/partners/logo_rff.png create mode 100644 static/images/partners/logo_stanford.png create mode 100644 static/images/partners/logo_techsoup.png create mode 100644 static/images/partners/logo_unSdg.png diff --git a/static/images/icons/icon-api.svg b/static/images/icons/icon-api.svg new file mode 100644 index 0000000000..2589a63de3 --- /dev/null +++ b/static/images/icons/icon-api.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/icons/icon-download.svg b/static/images/icons/icon-download.svg new file mode 100644 index 0000000000..eb6caea105 --- /dev/null +++ b/static/images/icons/icon-download.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/icons/icon-map.svg b/static/images/icons/icon-map.svg new file mode 100644 index 0000000000..6bb6b119b2 --- /dev/null +++ b/static/images/icons/icon-map.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/icons/icon-scaterplot.svg b/static/images/icons/icon-scaterplot.svg new file mode 100644 index 0000000000..a033dff5c6 --- /dev/null +++ b/static/images/icons/icon-scaterplot.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/icons/icon-timeline.svg b/static/images/icons/icon-timeline.svg new file mode 100644 index 0000000000..4eefd77cc9 --- /dev/null +++ b/static/images/icons/icon-timeline.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/images/partners/logo_feedingAmerica.png b/static/images/partners/logo_feedingAmerica.png new file mode 100644 index 0000000000000000000000000000000000000000..a5b3064f7f08168c2865b5b205705fc55e6d8a45 GIT binary patch literal 5543 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 literal 0 HcmV?d00001 diff --git a/static/images/partners/logo_rff.png b/static/images/partners/logo_rff.png new file mode 100644 index 0000000000000000000000000000000000000000..e67eee958fa79dc3d521b580747aae7d41e6295f GIT binary patch literal 8906 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 literal 0 HcmV?d00001 From 629a8b8e8f3e4e5f010a3a1c254af584c5ef25d3 Mon Sep 17 00:00:00 2001 From: Pablo Noel Date: Sat, 7 Sep 2024 20:26:49 -0400 Subject: [PATCH 48/92] Refactor reusable variables --- static/css/homepage_v2.scss | 138 ++++++++++++---------------------- static/css/new_variables.scss | 43 +++++++++++ 2 files changed, 90 insertions(+), 91 deletions(-) create mode 100644 static/css/new_variables.scss diff --git a/static/css/homepage_v2.scss b/static/css/homepage_v2.scss index f48dd046f8..0f512b8643 100644 --- a/static/css/homepage_v2.scss +++ b/static/css/homepage_v2.scss @@ -16,52 +16,9 @@ /* Styles for the homepage */ @use "./nl_search_bar"; +@use "./new_variables" as var; @import "base"; -$spacing: 8px; -$container-horizontal-padding: 3rem; - -$color-white-container: #F8FAFD; - -$color-blue:#0B57D0; -$color-blue_pill_text:#041E49; -$color-blue_pill_bckg:#E8F0FE; -$color-blue-bckg: #F6F9FF; - -$color-green:#146C2E; -$color-green_pill_text:#072711; -$color-green_pill_bckg:#C4EED0; - -$color-red:#B3261E; -$color-red_pill_text:#410E0B; -$color-red_pill_bckg:#F9DEDC; - -$color-yellow:#945700; -$color-yellow_pill_text:#410E0B; -$color-yellow_pill_bckg:#FFF0D1; - -$color-gray:#444746; -$color-gray_pill_text:#945700; -$color-gray_pill_bckg:#E2E2EC; - -@mixin shadow{ - box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15); -} - -@mixin white-box{ - @include shadow; - background-color: $color-white-container; - text-decoration: none; - &:hover{ - background-color: rgba(11, 87, 208, 0.08); - } -} - -@mixin big-text{ - font-size: 1.4rem; - line-height: 1.73rem; -} - #homepage { // General Styles h3{ @@ -70,25 +27,25 @@ $color-gray_pill_bckg:#E2E2EC; line-height: 1.75rem; font-weight: 400; color: #072711; - margin-bottom: calc(#{$spacing} * 4); + margin-bottom: calc(#{var.$spacing} * 4); } .container{ - padding: $container-horizontal-padding 0; + padding: var.$container-horizontal-padding 0; } .big-description{ - margin-bottom: calc(#{$spacing} * 4); + margin-bottom: calc(#{var.$spacing} * 4); max-width: 650px; h3{ font-size: 2rem; line-height: 2.5rem; } p{ - @include big-text; + @include var.big-text; } } // Hero section .hero{ - background-color: $color-blue-bckg; + background-color: var.$color-blue-bckg; } // Topics section .topics-container{ @@ -96,7 +53,7 @@ $color-gray_pill_bckg:#E2E2EC; padding: 0; display: flex; flex-wrap: wrap; - gap: calc(#{$spacing} * 2); + gap: calc(#{var.$spacing} * 2); max-width: 80%; @include media-breakpoint-down(md) { max-width: 100%; @@ -106,26 +63,26 @@ $color-gray_pill_bckg:#E2E2EC; display: block; list-style: none; a{ - @include white-box; + @include var.white-box; display: flex; justify-content: center; align-items: center; gap: 10px; - border-radius: calc(#{$spacing} * 10); - padding: 10px calc(#{$spacing} * 3) 10px calc(#{$spacing} * 2); + border-radius: calc(#{var.$spacing} * 10); + padding: 10px calc(#{var.$spacing} * 3) 10px calc(#{var.$spacing} * 2); } } // Questions section .questions-carousel{ display: flex; - gap: calc(#{$spacing} * 3); - margin-bottom: calc(#{$spacing} * 3); + gap: calc(#{var.$spacing} * 3); + margin-bottom: calc(#{var.$spacing} * 3); } .questions-carousel-dots{ display: flex; justify-content: center; width: 100%; - gap: $spacing; + gap: var.$spacing; margin: auto; padding: 0; .questions-carousel-dot{ @@ -141,30 +98,30 @@ $color-gray_pill_bckg:#E2E2EC; width: 10px; height: 10px; background: white; - border: 1px solid #{$color-blue}; + border: 1px solid #{var.$color-blue}; border-radius: 100px; } &.active a{ - background: $color-blue; + background: var.$color-blue; } } } .questions-column{ display: flex; flex-direction: column; - gap: calc(#{$spacing} * 3); + gap: calc(#{var.$spacing} * 3); } .question-item { display: block; list-style: none; a{ - @include white-box; + @include var.white-box; display: flex; flex-direction: column; align-items: flex-start; gap: 10px; - padding: calc(#{$spacing} * 3); - border-radius: calc(#{$spacing} * 4); + padding: calc(#{var.$spacing} * 3); + border-radius: calc(#{var.$spacing} * 4); p{ font-size: 1.6rem; line-height: 2rem; @@ -180,61 +137,61 @@ $color-gray_pill_bckg:#E2E2EC; } &.green a{ p { - color: $color-green; + color: var.$color-green; } small{ - color: $color-green_pill_text; - background-color: $color-green_pill_bckg; + color: var.$color-green_pill_text; + background-color: var.$color-green_pill_bckg; } } &.blue a{ p { - color: $color-blue; + color: var.$color-blue; } small{ - color: $color-blue_pill_text; - background-color: $color-blue_pill_bckg; + color: var.$color-blue_pill_text; + background-color: var.$color-blue_pill_bckg; } } &.red a{ p { - color: $color-red; + color: var.$color-red; } small{ - color: $color-red_pill_text; - background-color: $color-red_pill_bckg; + color: var.$color-red_pill_text; + background-color: var.$color-red_pill_bckg; } } &.yellow a{ p { - color: $color-yellow; + color: var.$color-yellow; } small{ - color: $color-yellow_pill_text; - background-color: $color-yellow_pill_bckg; + color: var.$color-yellow_pill_text; + background-color: var.$color-yellow_pill_bckg; } } &.gray a{ p { - color: $color-gray; + color: var.$color-gray; } small{ - color: $color-gray_pill_text; - background-color: $color-gray_pill_bckg; + color: var.$color-gray_pill_text; + background-color: var.$color-gray_pill_bckg; } } } // Tools section .tools{ - padding-top: calc(#{$spacing} * 10); - padding-bottom: calc(#{$spacing} * 10); - background: $color-blue-bckg; + padding-top: calc(#{var.$spacing} * 10); + padding-bottom: calc(#{var.$spacing} * 10); + background: var.$color-blue-bckg; } .tools-buttons{ display: flex; align-items: stretch; flex-wrap: wrap; - gap: calc(#{$spacing} * 3); + gap: calc(#{var.$spacing} * 3); margin: 0; padding: 0; li{ @@ -243,15 +200,15 @@ $color-gray_pill_bckg:#E2E2EC; margin: 0; flex-basis: 200px; a{ - @include white-box; + @include var.white-box; display: flex; width: 100%; height: 100%; flex-direction: column; align-items: flex-start; - gap: calc(#{$spacing} * 2); - padding: calc(#{$spacing} * 3); - border-radius: calc(#{$spacing} * 4); + gap: calc(#{var.$spacing} * 2); + padding: calc(#{var.$spacing} * 3); + border-radius: calc(#{var.$spacing} * 4); font-size: 1.75rem; line-height: 2.25rem; } @@ -284,11 +241,11 @@ $color-gray_pill_bckg:#E2E2EC; .video-container{ display: grid; grid-template-columns: 1fr 1fr; - gap: calc(#{$spacing} * 6); + gap: calc(#{var.$spacing} * 6); } .video-description{ p{ - @include big-text; + @include var.big-text; } } .video-player{ @@ -319,9 +276,9 @@ $color-gray_pill_bckg:#E2E2EC; display: flex; justify-content: space-around; flex-wrap: wrap; - gap: calc(#{$spacing} * 3); + gap: calc(#{var.$spacing} * 3); margin: 0; - margin-top: calc(#{$spacing} * 3); + margin-top: calc(#{var.$spacing} * 3); padding: 0; li{ display: block; @@ -339,12 +296,11 @@ $color-gray_pill_bckg:#E2E2EC; max-width: 90px; } &:hover{ - @include shadow; + @include var.shadow; transform: translateY(-5px); border: 2px solid white; } } } } - } \ No newline at end of file diff --git a/static/css/new_variables.scss b/static/css/new_variables.scss new file mode 100644 index 0000000000..d9b58501fe --- /dev/null +++ b/static/css/new_variables.scss @@ -0,0 +1,43 @@ +$spacing: 8px; +$container-horizontal-padding: 3rem; + +$color-white-container: #F8FAFD; + +$color-blue:#0B57D0; +$color-blue_pill_text:#041E49; +$color-blue_pill_bckg:#E8F0FE; +$color-blue-bckg: #F6F9FF; + +$color-green:#146C2E; +$color-green_pill_text:#072711; +$color-green_pill_bckg:#C4EED0; + +$color-red:#B3261E; +$color-red_pill_text:#410E0B; +$color-red_pill_bckg:#F9DEDC; + +$color-yellow:#945700; +$color-yellow_pill_text:#410E0B; +$color-yellow_pill_bckg:#FFF0D1; + +$color-gray:#444746; +$color-gray_pill_text:#945700; +$color-gray_pill_bckg:#E2E2EC; + +@mixin shadow{ + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15); +} + +@mixin white-box{ + @include shadow; + background-color: $color-white-container; + text-decoration: none; + &:hover{ + background-color: rgba(11, 87, 208, 0.08); + } +} + +@mixin big-text{ + font-size: 1.4rem; + line-height: 1.73rem; +} \ No newline at end of file From 458bc23ba06a536a23b6edd26cccb4b6b4455502 Mon Sep 17 00:00:00 2001 From: Nick B Date: Sat, 7 Sep 2024 17:47:46 -0700 Subject: [PATCH 49/92] The sample questions are now coming through to the React app via the template from a JSON source, and these are now rendering in the SampleQuestions component. --- server/__init__.py | 2 + server/config/home_page/sample_questions.json | 50 +++++++ server/routes/static.py | 3 +- server/templates/static/homepage.html | 1 + static/js/apps/homepage/app_v2.tsx | 8 +- .../js/apps/homepage/components/partners.tsx | 2 +- .../homepage/components/sample_questions.tsx | 128 ++++-------------- static/js/apps/homepage/main_v2.ts | 6 +- static/js/shared/types/homepage.ts | 9 ++ 9 files changed, 101 insertions(+), 108 deletions(-) create mode 100644 server/config/home_page/sample_questions.json diff --git a/server/__init__.py b/server/__init__.py index 46d0a5eba2..983515e870 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -310,6 +310,8 @@ def create_app(nl_root=DEFAULT_NL_ROOT): "config/home_page/topics.json") app.config['HOMEPAGE_PARTNERS'] = libutil.get_json( "config/home_page/partners.json") + app.config['HOMEPAGE_SAMPLE_QUESTIONS'] = libutil.get_json( + "config/home_page/sample_questions.json") if cfg.TEST or cfg.LITE: app.config['MAPS_API_KEY'] = '' diff --git a/server/config/home_page/sample_questions.json b/server/config/home_page/sample_questions.json new file mode 100644 index 0000000000..e4f25f0c56 --- /dev/null +++ b/server/config/home_page/sample_questions.json @@ -0,0 +1,50 @@ +[ + { + "category": "Sustainability", + "questions": [ + "Which counties in the US have the most smoke pollution?", + "Which countries have the most greenhouse gas emissions?", + "What is the solar energy consumption around the world?" + ] + }, + { + "category": "Economics", + "questions": [ + "Tell me about the economy in Brazil.", + "Show me the breakdown of businesses by industry type in the US.", + "What is the GDP in Iran?" + ] + }, + { + "category": "Equity", + "questions": [ + "Which countries have the lowest Gini index?", + "Which counties in the US have the highest rates of uninsured?", + "What about health equity in Florida?" + ] + }, + { + "category": "Education, housing and commute", + "questions": [ + "When were the houses in NYC built?", + "Which countries have the highest college-educated population in the world?", + "How much time do people in San Francisco spend commuting?" + ] + }, + { + "category": "Demographics", + "questions": [ + "Demographics around the world", + "Which country has the most old people?", + "Which state in the US has the most Tamil speakers?" + ] + }, + { + "category": "Health", + "questions": [ + "How does life expectancy vary across countries in Africa?", + "What is the prevalence of asthma across California counties?", + "What are the rates of child immunization in Pakistan?" + ] + } +] \ No newline at end of file diff --git a/server/routes/static.py b/server/routes/static.py index cb51161713..79d0bc7d77 100644 --- a/server/routes/static.py +++ b/server/routes/static.py @@ -41,7 +41,8 @@ def homepage(): "homepage.html", topics=json.dumps(current_app.config.get('HOMEPAGE_TOPICS', [])), partners_list=current_app.config.get('HOMEPAGE_PARTNERS', []), - partners=json.dumps(current_app.config.get('HOMEPAGE_PARTNERS', []))) + partners=json.dumps(current_app.config.get('HOMEPAGE_PARTNERS', [])), + sample_questions=json.dumps(current_app.config.get('HOMEPAGE_SAMPLE_QUESTIONS', []))) @bp.route('/about') diff --git a/server/templates/static/homepage.html b/server/templates/static/homepage.html index d31af8b12e..53725463f4 100644 --- a/server/templates/static/homepage.html +++ b/server/templates/static/homepage.html @@ -34,6 +34,7 @@
diff --git a/static/js/apps/homepage/app_v2.tsx b/static/js/apps/homepage/app_v2.tsx index 52cdcc7195..2e22009508 100644 --- a/static/js/apps/homepage/app_v2.tsx +++ b/static/js/apps/homepage/app_v2.tsx @@ -23,7 +23,7 @@ import React, { ReactElement } from "react"; import { Routes } from "../../shared/types/base"; -import { Partner, Topic } from "../../shared/types/homepage"; +import {Partner, SampleQuestionCategory, Topic} from "../../shared/types/homepage"; import Partners from "./components/partners"; import Tools from "./components/tools"; import Topics from "./components/topics"; @@ -36,6 +36,8 @@ interface AppProps { topics: Topic[]; //the partners passed from the backend through to the JavaScript via the templates partners: Partner[]; + //the sample question categories and questions passed from the backend through to the JavaScript via the templates + sampleQuestions: SampleQuestionCategory[]; //the routes dictionary - this is used to convert routes to resolved urls routes: Routes; } @@ -43,12 +45,12 @@ interface AppProps { /** * Application container */ -export function App({ topics, partners, routes }: AppProps): ReactElement { +export function App({ topics, partners, sampleQuestions, routes }: AppProps): ReactElement { return ( <> - + diff --git a/static/js/apps/homepage/components/partners.tsx b/static/js/apps/homepage/components/partners.tsx index 0a7468ef6f..89cd05fa65 100644 --- a/static/js/apps/homepage/components/partners.tsx +++ b/static/js/apps/homepage/components/partners.tsx @@ -34,7 +34,7 @@ const Partners = ({ partners }: PartnersProps): ReactElement => {

Other organizations with a Data Commons