diff --git a/package.json b/package.json
index 8c25d09..ed7c59b 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
},
"type": "module",
"devDependencies": {
- "@mapc/eslint-config": "^1.0.2",
+ "@mapc/eslint-config": "^1.0.4",
"babel-core": "^6.26.3",
"babel-loader": "^9.2.1",
"babel-plugin-root-import": "^6.6.0",
@@ -27,7 +27,7 @@
"immutability-helper": "^3.1.1",
"mini-css-extract-plugin": "^2.9.1",
"prettier": "^3.3.3",
- "sass": "^1.79.4",
+ "sass": "1.77.6",
"sass-loader": "^16.0.2",
"style-loader": "^4.0.0",
"url-loader": "^4.1.1",
@@ -41,12 +41,15 @@
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/preset-env": "^7.25.7",
"@babel/preset-react": "^7.25.7",
+ "@mapc/airtable-cms": "^1.1.1",
"airtable": "^0.12.2",
"babel-preset-env": "^1.7.0",
+ "bootstrap": "^5.3.3",
"core-js": "^3.38.1",
"eslint-plugin-react-hooks": "4.6.2",
"prop-types": "^15.8.1",
"react": "18.3.1",
+ "react-bootstrap": "^2.10.5",
"react-dom": "18.3.1",
"react-ga": "^3.3.1",
"react-markdown": "^9.0.1",
diff --git a/src/js/components/CMSComponent.jsx b/src/js/components/CMSComponent.jsx
new file mode 100644
index 0000000..11372c8
--- /dev/null
+++ b/src/js/components/CMSComponent.jsx
@@ -0,0 +1,84 @@
+import React, { useState } from "react";
+import Spinner from "react-bootstrap/Spinner";
+import ToastContainer from "react-bootstrap/ToastContainer";
+import Toast from "react-bootstrap/Toast";
+import { useAirtableCMS } from "@mapc/airtable-cms";
+
+const LoadingIndicator = () => {
+ return (
+
+ Loading...
+
+ );
+};
+
+const ErrorMessage = ({ empty = false, tableName }) => {
+ const [show, setShow] = useState(true);
+ const title = empty ? "Error" : "Warning";
+ const bg = empty ? "danger" : "warning";
+ const textColor = empty ? "white" : "#333";
+ return (
+
+ setShow(false)} show={show} delay={10000} bg={bg} autohide>
+
+ {title}
+
+
+ Unable to fetch data{tableName != null && ` for ${tableName}`}
+ {!empty && "; displaying cached content"}
+
+
+
+ );
+};
+
+const EmptyMessage = () => {
+ return
No records to display.
;
+};
+
+const CMSComponent = ({
+ tableName,
+ keyField,
+ fieldMapping,
+ sortBy,
+ filterBy,
+ recordComponent,
+ loadingComponent = LoadingIndicator,
+ errorComponent = ErrorMessage,
+ emptyComponent = EmptyMessage,
+}) => {
+ const {
+ data,
+ metadata: { done, error },
+ } = useAirtableCMS({
+ tableName,
+ keyField,
+ fieldMapping,
+ sortBy,
+ filterBy,
+ });
+
+ const renderedComponents = [];
+
+ if (!done && data.length === 0) {
+ // If we're still fetching updates and have nothing in the cache to display, show loadingComponent
+ renderedComponents.push(React.createElement(loadingComponent));
+ } else if (done) {
+ if (data.length === 0) {
+ if (error) {
+ renderedComponents.push(React.createElement(errorComponent, { empty: true, key: "errorComponent", tableName }));
+ }
+ // If we have nothing in the cache to display, show emptyComponent
+ renderedComponents.push(React.createElement(emptyComponent, { key: "emptyComponent" }));
+ } else if (data.length > 0) {
+ if (error) {
+ renderedComponents.push(React.createElement(errorComponent, { empty: false, key: "errorComponent", tableName }));
+ }
+ // If we have something in the cache to display, show those records
+ renderedComponents.push(...data.map((record) => React.createElement(recordComponent, { key: record[keyField], ...record })));
+ }
+ }
+ return renderedComponents;
+};
+
+export default CMSComponent;
diff --git a/src/js/components/LegislativePriorities.jsx b/src/js/components/LegislativePriorities.jsx
index 9ad5c7d..6cb05d7 100644
--- a/src/js/components/LegislativePriorities.jsx
+++ b/src/js/components/LegislativePriorities.jsx
@@ -1,28 +1,31 @@
-import useAirtableCMS from "../hooks/useAirtableCMS";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
-const LegislativePriorities = () => {
- const legislativePriorities = useAirtableCMS({
- baseID: "app1YqNgXXkVH04nO",
- tableName: "Legislative Priorities",
- keyField: "name",
- fieldMapping: {
- name: "Name",
- content: "Content",
- },
- });
+import CMSComponent from "./CMSComponent";
+const LegislativePriority = ({ name, content }) => {
+ return (
+
+
{name}
+ {content}
+
+ );
+};
+
+const LegislativePriorities = () => {
return (
- {legislativePriorities.map((priority) => {
- return (
-
-
{priority.name}
- {priority.content}
-
- );
- })}
+
a.order - b.order}
+ recordComponent={LegislativePriority}
+ />
);
};
diff --git a/src/js/components/LocalImplementationProjects.jsx b/src/js/components/LocalImplementationProjects.jsx
index ee687eb..6fd31fd 100644
--- a/src/js/components/LocalImplementationProjects.jsx
+++ b/src/js/components/LocalImplementationProjects.jsx
@@ -1,28 +1,31 @@
-import useAirtableCMS from "../hooks/useAirtableCMS";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
-const LocalImplementationProjects = () => {
- const projects = useAirtableCMS({
- baseID: "app1YqNgXXkVH04nO",
- tableName: "Local Implementation Projects",
- keyField: "name",
- fieldMapping: {
- name: "Name",
- description: "Description",
- },
- });
+import CMSComponent from "./CMSComponent";
+const LocalImplementationProject = ({ name, description }) => {
+ return (
+
+
{name}
+ {description}
+
+ );
+};
+
+const LocalImplementationProjects = () => {
return (
- {projects.map((project) => {
- return (
-
-
{project.name}
- {project.description}
-
- );
- })}
+
a.order - b.order}
+ recordComponent={LocalImplementationProject}
+ />
);
};
diff --git a/src/js/components/MemberName.jsx b/src/js/components/MemberName.jsx
index 6081071..d33b0d0 100644
--- a/src/js/components/MemberName.jsx
+++ b/src/js/components/MemberName.jsx
@@ -1,13 +1,13 @@
import { getMuniProfileURL } from "../utils";
-const MemberName = ({ municipalityName, name, title }) => {
+const MemberName = ({ municipality, name, title }) => {
return (
diff --git a/src/js/components/TaskForceNames.jsx b/src/js/components/TaskForceNames.jsx
index ba6bf3d..44e2dfa 100644
--- a/src/js/components/TaskForceNames.jsx
+++ b/src/js/components/TaskForceNames.jsx
@@ -1,32 +1,27 @@
+import CMSComponent from "./CMSComponent";
+
import MemberName from "./MemberName";
-import { useAirtableCMS } from "../hooks/useAirtableCMS";
const TaskForceNames = () => {
- const taskForce = useAirtableCMS({
- baseID: "app1YqNgXXkVH04nO",
- tableName: "Task Force Members",
- keyField: "municipality",
- fieldMapping: {
- municipality: "Municipality",
- title: "Title",
- name: "Name",
- color: "Color",
- originalCompact: "Original Compact",
- currentMember: "Current Member",
- },
- sortBy: (a, b) => a.municipality.localeCompare(b.municipality),
- });
-
return (
The 2018 Task Force
- {taskForce
- // Filter any members that were not part of the original 2018 compact
- .filter((member) => member.originalCompact)
- .map((member) => (
-
- ))}
+ a.municipality.localeCompare(b.municipality)}
+ filterBy={(member) => member.originalCompact}
+ recordComponent={MemberName}
+ />
);
diff --git a/src/js/components/TrackingProgress.jsx b/src/js/components/TrackingProgress.jsx
index 262f658..fd31fc0 100644
--- a/src/js/components/TrackingProgress.jsx
+++ b/src/js/components/TrackingProgress.jsx
@@ -1,45 +1,63 @@
-import useAirtableCMS from "../hooks/useAirtableCMS";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
-const TrackingProgress = () => {
- const trackingProgress = useAirtableCMS({
- baseID: "app1YqNgXXkVH04nO",
- tableName: "Tracking Progress",
- keyField: "section",
- fieldMapping: {
- section: "Section",
- content: "Content",
- },
- });
+import CMSComponent from "./CMSComponent";
+const PageSection = ({ section, content }) => {
+ return (
+
+
{section}
+
{
+ if (props.src.indexOf("gallery.shinyapps.io") >= 0) {
+ return (
+
+ );
+ } else {
+ return ;
+ }
+ },
+ }}
+ remarkPlugins={[remarkGfm]}
+ >
+ {content}
+
+
+ );
+};
+
+const TrackingProgress = () => {
return (
- {trackingProgress.map((record) => {
- return (
-
-
{record.section}
-
{
- if(props.src.indexOf("gallery.shinyapps.io") >= 0) {
- return
- } else {
- return
- }
- }
- }}
- remarkPlugins={[remarkGfm]}>{record.content}
-
- );
- })}
+
a.order - b.order}
+ recordComponent={PageSection}
+ />