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 (
{name}
{title}
- - {municipalityName} + + {municipality}
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} + />