Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useAirtableCMS hook updates #124

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
84 changes: 84 additions & 0 deletions src/js/components/CMSComponent.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
);
};

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 (
<ToastContainer containerPosition="bottom-end" position="bottom-end" style={{ position: "fixed", zIndex: 100, margin: "1em" }}>
<Toast onClose={() => setShow(false)} show={show} delay={10000} bg={bg} autohide>
<Toast.Header>
<strong className="me-auto">{title}</strong>
</Toast.Header>
<Toast.Body style={{ color: textColor }}>
Unable to fetch data{tableName != null && ` for ${tableName}`}
{!empty && "; displaying cached content"}
</Toast.Body>
</Toast>
</ToastContainer>
);
};

const EmptyMessage = () => {
return <p>No records to display.</p>;
};

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;
41 changes: 22 additions & 19 deletions src/js/components/LegislativePriorities.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div key={name} className="container">
<h2>{name}</h2>
<Markdown remarkPlugins={[remarkGfm]}>{content}</Markdown>
</div>
);
};

const LegislativePriorities = () => {
return (
<div className="component LegislativePriorities">
{legislativePriorities.map((priority) => {
return (
<div key={priority.name} className="container">
<h2>{priority.name}</h2>
<Markdown remarkPlugins={[remarkGfm]}>{priority.content}</Markdown>
</div>
);
})}
<CMSComponent
tableName="Legislative Priorities"
keyField="name"
fieldMapping={{
name: "Name",
order: "Order",
content: "Content",
}}
sortBy={(a, b) => a.order - b.order}
recordComponent={LegislativePriority}
/>
</div>
);
};
Expand Down
41 changes: 22 additions & 19 deletions src/js/components/LocalImplementationProjects.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container">
<h2>{name}</h2>
<Markdown remarkPlugins={[remarkGfm]}>{description}</Markdown>
</div>
);
};

const LocalImplementationProjects = () => {
return (
<div className="component LocalImplementationProjects">
{projects.map((project) => {
return (
<div key={project.name} className="container">
<h2>{project.name}</h2>
<Markdown remarkPlugins={[remarkGfm]}>{project.description}</Markdown>
</div>
);
})}
<CMSComponent
tableName="Local Implementation Projects"
keyField="name"
fieldMapping={{
name: "Name",
order: "Order",
description: "Description",
}}
sortBy={(a, b) => a.order - b.order}
recordComponent={LocalImplementationProject}
/>
</div>
);
};
Expand Down
6 changes: 3 additions & 3 deletions src/js/components/MemberName.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { getMuniProfileURL } from "../utils";

const MemberName = ({ municipalityName, name, title }) => {
const MemberName = ({ municipality, name, title }) => {
return (
<div className="component Member">
<div className="name">{name}</div>
<div className="title">{title}</div>
<div className="name">
<a style={{ color: "#002D40", textTransform: "uppercase", cursor: "pointer" }} href={getMuniProfileURL(municipalityName)}>
{municipalityName}
<a style={{ color: "#002D40", textTransform: "uppercase", cursor: "pointer" }} href={getMuniProfileURL(municipality)}>
{municipality}
</a>
</div>
</div>
Expand Down
39 changes: 17 additions & 22 deletions src/js/components/TaskForceNames.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="component TaskForceSection container">
<h3 className="with-bar">The 2018 Task Force</h3>
<div className="task-force-members">
{taskForce
// Filter any members that were not part of the original 2018 compact
.filter((member) => member.originalCompact)
.map((member) => (
<MemberName key={member.name} title={member.title} name={member.name} municipalityName={member.municipality} color={member.color} />
))}
<CMSComponent
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)}
filterBy={(member) => member.originalCompact}
recordComponent={MemberName}
/>
</div>
</section>
);
Expand Down
78 changes: 48 additions & 30 deletions src/js/components/TrackingProgress.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div key={section} className="container">
<h2>{section}</h2>
<Markdown
components={{
img: (props) => {
if (props.src.indexOf("gallery.shinyapps.io") >= 0) {
return (
<iframe
src={props.src}
width="1366"
height="768"
title={props.title}
scrolling="no"
frameBorder="0"
webkitallowfullscreen
mozallowfullscreen
allowfullscreen
></iframe>
);
} else {
return <img {...props} />;
}
},
}}
remarkPlugins={[remarkGfm]}
>
{content}
</Markdown>
</div>
);
};

const TrackingProgress = () => {
return (
<div className="component TrackingProgress">
{trackingProgress.map((record) => {
return (
<div className="container">
<h2>{record.section}</h2>
<Markdown
components={{
img: (props) => {
if(props.src.indexOf("gallery.shinyapps.io") >= 0) {
return <iframe src={props.src} width="1366" height="768" title={props.title} scrolling="no" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
} else {
return <img {...props} />
}
}
}}
remarkPlugins={[remarkGfm]}>{record.content}</Markdown>
</div>
);
})}
<CMSComponent
tableName="Tracking Progress"
keyField="section"
fieldMapping={{
section: "Section",
content: "Content",
order: "Order",
}}
sortBy={(a, b) => a.order - b.order}
recordComponent={PageSection}
/>
<div className="presentation-slides">
<iframe
src="https://slides.com/mapc/deck-7b8e5c/embed"
width="576"
height="420"
scrolling="no"
frameborder="0"
frameBorder="0"
webkitallowfullscreen
mozallowfullscreen
allowfullscreen
Expand Down
Loading