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

STAN-288: Create Associates microservice for StoryBlocks #709

Merged
merged 12 commits into from
Sep 28, 2023
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"cnbuilder": "^3.1.0",
"cookie-parser": "^1.4.6",
"decanter": "^7.0.0-rc.2",
"fast-sort": "^3.4.0",
moisesnarvaez marked this conversation as resolved.
Show resolved Hide resolved
"gatsby": "^4.14.0",
"gatsby-link": "^4.14.0",
"gatsby-plugin-fontawesome-css": "^1.2.0",
Expand Down
23 changes: 23 additions & 0 deletions src/api/contentful/api.js
moisesnarvaez marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import axios from 'axios';
import keys from './keys';

class ContentFulAPI {
constructor(space = keys.space, accessToken = keys.accessToken) {
this.space = space;
this.accessToken = accessToken;
}

async fethEntries(limit, skip) {
moisesnarvaez marked this conversation as resolved.
Show resolved Hide resolved
const apiUrl = `https://cdn.contentful.com/spaces/${this.space}/entries?access_token=${this.accessToken}&limit=${limit}&skip=${skip}&order=sys.id`;

const response = await axios.get(apiUrl);
const { items, total } = response.data;

return {
items: items.map((item) => item.fields),
total,
};
}
}

export default ContentFulAPI;
52 changes: 52 additions & 0 deletions src/api/contentful/associates/index.js
moisesnarvaez marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { sort } from 'fast-sort';

import ContentFulAPI from '../api';
import keys from '../keys';

const fetchNames = async (
space = keys.space,
accessToken = keys.accessToken
) => {
const client = new ContentFulAPI(space, accessToken);
const { items, total } = await client.fethEntries(1000, 0);
let associates = [...items];

const loops = Math.ceil(total / 1000);
let skip = 0;
const requests = [];

for (let i = 0; i < loops; i += 1) {
skip += 1000;
requests.push(client.fethEntries(1000, skip));
}

const responses = await Promise.all(requests);
const newAssociates = responses.reduce(
(acc, response) => acc.concat(response.items),
[]
);

associates = associates.concat(newAssociates);
const sortedNames = sort(associates).asc([
(person) => person.name.last,
(person) => person.name.first,
(person) => person.name.middle,
]);

const grouped = sortedNames.reduce((acc, person) => {
const firstLetter = person.name.last[0].toUpperCase();
if (!acc[firstLetter]) {
acc[firstLetter] = [];
}
acc[firstLetter].push(person);
return acc;
}, {});

return {
list: sortedNames,
grouped,
total: sortedNames.length,
};
};

export default fetchNames;
7 changes: 7 additions & 0 deletions src/api/contentful/keys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// It's not necessary to set these as environment variables, they are public and being used on the client side.
moisesnarvaez marked this conversation as resolved.
Show resolved Hide resolved
const keys = {
space: '0f39zonxf59w',
accessToken: '10OGNlSRGeKn81WAaTUxMjVL0nhXFEUszwRJIY7vPeI',
};

export default keys;
4 changes: 4 additions & 0 deletions src/components/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,16 @@ import VerticalNavWrapper from './navigation/verticalNavWrapper';
import VerticalNavItem from './navigation/verticalNavItem';
import Wysiwyg from './simple/wysiwyg';
import MembershipPaymentOptions from './page-types/membershipFormPage/membershipPaymentOptions';
import AssociatesDirectoryPage from './page-types/associatesDirectoryPage/associatesDirectoryPage';
import AssociatesDirectory from './page-types/associatesDirectoryPage/associatesDirectory';

const ComponentList = {
accordion: Accordion,
accordionItem: AccordionItem,
alert: SBAlert,
alertCtaLink: SBAlertCtaLink,
associatesDirectoryPage: AssociatesDirectoryPage,
associatesDirectory: AssociatesDirectory,
basicCard: BasicCard,
basicCardHorizontal: BasicCardHorizontal,
basicPage: BasicPage,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';

const AssociateProps = {
isEnabled: PropTypes.bool,
person: PropTypes.shape({
name: PropTypes.shape({
first: PropTypes.string,
last: PropTypes.string,
}),
yearAdded: PropTypes.number,
}),
};

const Associate = ({ isEnabled = true, person }) => {
if (!isEnabled) return null;

return (
<div className="su-flex even:su-bg-black-10">
sherakama marked this conversation as resolved.
Show resolved Hide resolved
<div className="su-flex-1 su-w-[50%] su-py-10 su-pl-30">
{person.name.first} {person.name.last}
</div>
<div> </div>
<div className="su-flex-1 su-w-[50%] su-py-10">
{Object.values(person.years || []).join(', ')}
</div>
</div>
moisesnarvaez marked this conversation as resolved.
Show resolved Hide resolved
);
};

Associate.propTypes = AssociateProps;

export default Associate;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';

import Associate from './Associate';

const AssociateListProps = {
isEnabled: PropTypes.bool,
letter: PropTypes.string,
associates: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.shape({
first: PropTypes.string,
last: PropTypes.string,
}),
yearAdded: PropTypes.number,
})
),
onlyNewMembers: PropTypes.bool,
recentYear: PropTypes.number,
};

const AssociateList = ({
isEnabled,
letter,
associates,
onlyNewMembers,
recentYear,
}) => {
if (!isEnabled) return null;

return (
<div>
<h4 className="su-p-10 su-text-cardinal-red-xdark su-font-serif su-border-b su-border-dashed su-border-1 su-border-black-30-opacity-40">
{letter}
</h4>
<div>
{associates?.map((person, index) => (
<Associate
// eslint-disable-next-line react/no-array-index-key
moisesnarvaez marked this conversation as resolved.
Show resolved Hide resolved
key={`person-${person.entryTitle}-${index}`}
person={person}
isEnabled={!onlyNewMembers || person.yearAdded === recentYear}
/>
))}
</div>
</div>
);
};

AssociateList.propTypes = AssociateListProps;

export default AssociateList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useState, useEffect } from 'react';
import fetchNames from '../../../../api/contentful/associates';
import Tabs from './Tabs';
import Results from './Results';

const Directory = () => {
const [associatesData, setAssociatesData] = useState({});
const [onlyNewMembers, setOnlyNewMembers] = useState(false);
const [recentYear, setRecentYear] = useState(null);
const [search, setSearch] = useState('');
const [filteredList, setFilteredList] = useState([]);

const filterResult = () => {
const result = associatesData.list?.filter((person) => {
const fullName = `${person.name.first} ${person.name.last}`;
const isVisible =
fullName.toLowerCase().includes(search.toLowerCase()) &&
(!onlyNewMembers || person.yearAdded === recentYear);
return isVisible;
});
setFilteredList(result);
};

const handleNewMembersToggle = (event) => {
setOnlyNewMembers(event.target.checked);
};

const handleSearch = (event) => {
setSearch(event.target.value);
};

useEffect(() => {
const fetchData = async () => {
const data = await fetchNames();
setAssociatesData(data);

const mostRecent = Math.max(
...data.list.map((person) => person.yearAdded || 0)
);
setRecentYear(mostRecent);
};

fetchData();
}, []);

useEffect(() => {
filterResult();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onlyNewMembers, search]);

return (
<div className="su-mt-30">
<div className="su-my-20">{associatesData.total} Associates Total</div>
<div className="su-my-20">
<input
type="text"
className="su-py-10 su-px-20 su-text-19 su-border su-border-solid su-border-black-40"
placeholder="Search for a Name"
value={search}
onChange={handleSearch}
/>
</div>
<div className="su-mb-50">
<label>
<input
type="checkbox"
checked={onlyNewMembers}
value={onlyNewMembers}
onChange={handleNewMembersToggle}
className="su-peer su-form-checkbox su-text-digital-red-light su-mr-10 su-w-[1.5rem] su-h-[1.5rem] su-cursor-pointer su-rounded su-border-black-40 hocus:su-border-none hocus:su-ring hocus:su-ring-digital-red-light hocus:su-ring-offset-0"
/>{' '}
View Only New Members
</label>
</div>

{search.length ? <Results filteredList={filteredList} /> : ''}

{!search.length && (
<Tabs
groupedNames={associatesData.grouped || {}}
onlyNewMembers={onlyNewMembers}
recentYear={recentYear}
search={search}
/>
)}
</div>
);
};

export default Directory;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import Associate from './Associate';

const ResultsProps = {
onlyNewMembers: PropTypes.bool,
recentYear: PropTypes.number,
};

const Results = ({ filteredList }) => {
const total = filteredList?.length;
if (!total) {
return <div className="su-my-50">No associates found.</div>;
}
return (
<div>
<div className="su-my-20">{total} associates found:</div>
<ul>
moisesnarvaez marked this conversation as resolved.
Show resolved Hide resolved
{filteredList?.map((person, index) => (
<Associate
// eslint-disable-next-line react/no-array-index-key
sherakama marked this conversation as resolved.
Show resolved Hide resolved
key={`person-${person.entryTitle}-${index}`}
person={person}
/>
))}
</ul>
</div>
);
};

Results.propTypes = ResultsProps;

export default Results;
Loading
Loading