Skip to content

Commit

Permalink
Adding new Directory component
Browse files Browse the repository at this point in the history
  • Loading branch information
moisesnarvaez committed Sep 12, 2023
1 parent 5e50ecf commit f2b4dde
Show file tree
Hide file tree
Showing 10 changed files with 368 additions and 4 deletions.
23 changes: 23 additions & 0 deletions src/api/contentful/api.js
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) {
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
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.
const keys = {
space: '0f39zonxf59w',
accessToken: '10OGNlSRGeKn81WAaTUxMjVL0nhXFEUszwRJIY7vPeI',
};

export default keys;
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">
<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>
);
};

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
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>
{filteredList?.map((person, index) => (
<Associate
// eslint-disable-next-line react/no-array-index-key
key={`person-${person.entryTitle}-${index}`}
person={person}
/>
))}
</ul>
</div>
);
};

Results.propTypes = ResultsProps;

export default Results;
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';

import AssociateList from './AssociateList';

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

const tabsGroups = {
'A-B': ['A', 'B'],
'C-D': ['C', 'D'],
'E-F': ['E', 'F'],
'G-H': ['G', 'H'],
'I-J': ['I', 'J'],
'K-L': ['K', 'L'],
'M-N': ['M', 'N'],
'O-P': ['O', 'P'],
'Q-R': ['Q', 'R'],
'S-T': ['S', 'T'],
'U-V': ['U', 'V'],
'W-X': ['W', 'X'],
'Y-Z': ['Y', 'Z'],
};
const Tabs = ({ groupedNames, onlyNewMembers, recentYear }) => {
const [activeTab, setActiveTab] = useState('A-B');

const handleTabClick = (event) => {
setActiveTab(event.target.dataset.group);
};

return (
<div>
<nav>
{Object.keys(tabsGroups).map((group) => (
<a
key={`tab-${group}`}
className={`${
activeTab === group
? 'su-bg-cardinal-red-xdark'
: 'su-bg-saa-black su-bg-opacity-[80%]'
} su-py-5 su-px-10 su-text-white hover:su-text-white hover:su-no-underline focus:su-text-white focus:su-no-underline su-border su-border-solid su-border-1 su-border-black-30-opacity-40`}
href={`#${group}`}
onClick={handleTabClick}
data-group={group}
>
{group}
</a>
))}
</nav>
<div className="su-my-20">
{Object.keys(tabsGroups).map((group) => (
<div key={`content-${group}`}>
{tabsGroups[group].map((letter) => (
<AssociateList
key={`content-${letter}`}
isEnabled={activeTab === group}
tabsGroups={tabsGroups}
letter={letter}
associates={groupedNames[letter]}
onlyNewMembers={onlyNewMembers}
recentYear={recentYear}
/>
))}
</div>
))}
</div>
</div>
);
};

Tabs.propTypes = TabsProps;

export default Tabs;
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react';
import SbEditable from 'storyblok-react';
import { Container } from '../../layout/Container';
import LegacyDirectory from './legacyDirectory/LegacyDirectory';
import Directory from './Directory/Directory';

const AssociatesDirectory = (props) => {
const { blok } = props;

return (
<SbEditable content={blok}>
<Container id="directory">
<LegacyDirectory />
<Directory />
</Container>
</SbEditable>
);
Expand Down
Loading

0 comments on commit f2b4dde

Please sign in to comment.