Merge pull request #14 from Nona-Creative/feat/data-restructure

Data Restructure & Search Functionality
This commit is contained in:
Wisani 2018-07-17 09:19:43 +02:00 committed by GitHub
commit 7fabd4ef5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 2017 additions and 1386 deletions

View File

@ -7,7 +7,7 @@ import TopicsList from '../topicsList';
import ProjectsList from '../projectsList';
import SearchBar from '../searchBar';
import SearchResults from '../searchResults';
import InterviewsData from '../../data/archives/interviews';
import { InterviewsData, Questions } from '../../data/archives/interviews';
import './style.scss';
class BrowseArchives extends React.Component {
@ -16,11 +16,13 @@ class BrowseArchives extends React.Component {
this.state = {
term: '',
searchResults: [],
debounceTerm: '',
searchResults: [null],
isSingleInterviewModalOpen: false,
isInterviewsListModalOpen: false,
activeSingleInterviewId: 1,
isSearchActive: false,
interviewData: this.transformInterviews(InterviewsData),
};
this.onSearchInputChange = this.onSearchInputChange.bind(this);
@ -28,15 +30,16 @@ class BrowseArchives extends React.Component {
this.toggleInterviewsListModal = this.toggleInterviewsListModal.bind(this);
this.setSearchTerm = this.setSearchTerm.bind(this);
this.clearSearchInput = this.clearSearchInput.bind(this);
this.transformInterviews = this.transformInterviews.bind(this);
this.termIsInInterview = this.termIsInInterview.bind(this);
this.getSearchResultsDebounce = this.getSearchResultsDebounce.bind(this);
}
onSearchInputChange = (event) => {
const { term } = this.state;
const getSearchResults = _.debounce(() => { this.getSearchResults(term); }, 500);
this.setState({
term: event.target.value,
isSearchActive: true,
searchResults: [null],
});
if (event.target.value.length === 0) {
@ -44,37 +47,50 @@ class BrowseArchives extends React.Component {
}
// Throttle search result frequency with debounce while typing
getSearchResults(term);
this.getSearchResultsDebounce();
}
getSearchResults = (term) => {
// Edit function to do actual search based on term and update searchResults with dynamic data
// eslint-disable-next-line
console.log(`Get search results array based on searching for: ${term}`);
const { interviewData } = this.state;
const searchResults = interviewData.reduce((filtered, interview) => {
const findTerm = this.termIsInInterview(term, interview);
const matchedIndex = findTerm.foundIndex;
if (findTerm.found) {
filtered.push({ ...interview, matchedIndex });
}
return filtered;
}, []);
// Using static full interviews data array for now
this.setState({
searchResults: InterviewsData,
searchResults,
debounceTerm: term,
});
}
getSelectedInterview = () => {
const { activeSingleInterviewId } = this.state;
const selectedInterview = InterviewsData.find(item => item.id === activeSingleInterviewId);
const { activeSingleInterviewId, interviewData } = this.state;
const selectedInterview = interviewData
.find(item => item.id === activeSingleInterviewId);
return selectedInterview;
}
setSearchTerm = (event) => {
const { term } = this.state;
this.setState({
term: event.target.innerText,
isSearchActive: true,
});
this.getSearchResults(term);
this.getSearchResults(event.target.innerText);
}
getSearchResultsDebounce = _.debounce(() => {
const { term } = this.state;
this.getSearchResults(term);
}, 700);
clearSearchInput = () => {
this.setState({
isSearchActive: false,
@ -82,6 +98,37 @@ class BrowseArchives extends React.Component {
});
}
transformInterviews = (interviews) => {
// eslint-disable-next-line
const length = Object.keys(interviews).length;
const betterInterviews = [];
for (let i = 0; i < length; i++) {
const interview = interviews[i];
const qKeys = Object.keys(interview);
const interviewFormatted = [];
qKeys.forEach((key, index) => {
if (key !== 'Name' && interview[key] !== null) {
interviewFormatted.push({
question: index,
answer: interview[key],
});
}
});
betterInterviews.push({
id: i + 1,
tags: 'tag1, tag2, tag3',
name: interview.Name,
matchedIndex: -1,
interview: interviewFormatted,
});
}
return betterInterviews;
}
toggleInterviewsListModal = () => {
const { isInterviewsListModalOpen } = this.state;
@ -107,6 +154,57 @@ class BrowseArchives extends React.Component {
});
}
termIsInInterview = (term, interview) => {
const lcTerm = term.toLowerCase();
const matchesName = interview.name.toLowerCase().includes(lcTerm);
const { interviewData } = this.state;
let foundIndex = 0;
if (matchesName) {
return {
found: true,
foundIndex: 0,
};
}
const matchesTag = interview.tags.toLowerCase().indexOf(lcTerm) !== -1;
if (matchesTag) {
return {
found: true,
foundIndex: 0,
};
}
const matchingQuestions = interview.interview
.filter((question, questionIndex) => {
if (question.answer === null) {
return false;
}
const index = question.answer.toLowerCase().indexOf(lcTerm);
if (index !== -1 && interview.activeIndex !== -1) {
foundIndex = questionIndex;
}
return index !== -1;
});
this.setState({
interviewData,
});
if (matchingQuestions.length > 0) {
return {
found: true,
foundIndex,
};
}
return false;
}
render() {
const {
isSingleInterviewModalOpen,
@ -115,6 +213,8 @@ class BrowseArchives extends React.Component {
activeSingleInterviewId,
term,
searchResults,
interviewData,
debounceTerm,
} = this.state;
return (
@ -129,7 +229,7 @@ class BrowseArchives extends React.Component {
<div className="browse-content-left">
{isSearchActive &&
(<RelatedInterviewsList
data={InterviewsData}
data={searchResults}
toggleSingleInterview={this.toggleSingleInterview}
/>)
}
@ -138,12 +238,14 @@ class BrowseArchives extends React.Component {
{isSearchActive ? (
<SearchResults
data={searchResults}
questions={Questions}
term={debounceTerm}
toggleSingleInterview={this.toggleSingleInterview}
/>) :
(
<React.Fragment>
<InterviewsList
data={InterviewsData}
data={interviewData}
isInterviewsListModalOpen={isInterviewsListModalOpen}
toggleSingleInterview={this.toggleSingleInterview}
toggleInterviewsListModal={this.toggleInterviewsListModal}
@ -159,6 +261,7 @@ class BrowseArchives extends React.Component {
activeSingleInterviewId={activeSingleInterviewId}
selectedInterview={this.getSelectedInterview()}
toggleSingleInterview={this.toggleSingleInterview}
questions={Questions}
/>)
}
</div>

View File

@ -2,22 +2,30 @@ import React from 'react';
import { PropTypes } from 'prop-types';
import './style.scss';
const RelatedInterviewsList = props => (
<div className="related-interviews-list">
<h4>Related <br />Interviews</h4>
<ul>
{ props.data.map(interview => (
<li
id={interview.id}
key={interview.id}
>
<button onClick={props.toggleSingleInterview}>{interview.name}</button>
</li>
))
}
</ul>
</div>
);
const RelatedInterviewsList = (props) => {
if (!props.data || props.data[0] === null) {
return <div>Loading...</div>;
} else if (props.data.length < 1) {
return <div>No results found</div>;
}
return (
<div className="related-interviews-list">
<h4>Related <br />Interviews</h4>
<ul>
{ props.data.map(interview => (
<li
id={interview.id}
key={interview.id}
>
<button onClick={props.toggleSingleInterview}>{ interview.name }</button>
</li>
))
}
</ul>
</div>
);
};
RelatedInterviewsList.propTypes = {
data: PropTypes.arrayOf(PropTypes.shape({})).isRequired,

View File

@ -1,6 +1,5 @@
import React from 'react';
import { PropTypes } from 'prop-types';
import Parser from 'html-react-parser';
import Modal from '../../modal';
import './style.scss';
@ -11,7 +10,19 @@ const SingleInterview = props => (
>
<div className="single-interview">
<span className="number">{ props.activeSingleInterviewId }</span>
<div>{ Parser(props.selectedInterview.content) }</div>
<span className="name"> { props.selectedInterview.name } </span>
{ props.selectedInterview.interview.filter(interview => interview.answer !== null)
.map((interview, index) => {
const question = props.questions.find(q => q.id === interview.question);
return (
<div key={`question-${question.id}`}>
<p className="question">{index + 1}) { question.text }</p>
<p className="answer">{ interview.answer }</p>
</div>
);
})
}
</div>
</Modal>
);
@ -19,9 +30,14 @@ const SingleInterview = props => (
SingleInterview.propTypes = {
activeSingleInterviewId: PropTypes.number.isRequired,
selectedInterview: PropTypes.shape({
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
interview: PropTypes.array.isRequired,
}).isRequired,
toggleSingleInterview: PropTypes.func.isRequired,
questions: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired,
})).isRequired,
};
export default SingleInterview;

View File

@ -6,6 +6,21 @@
font-size: calculateRem(50);
}
.name {
color: white;
font-size: calculateRem(36);
}
.question {
font-size: calculateRem(28);
color: white;
margin: calculateRem(16) 0;
}
.answer {
color: #ddd;
}
/* This is the css from `Fabio Berger + Remco Bloemen` interview
* Note: it does not correctly format the other 2 interviews
*/

View File

@ -1,15 +1,53 @@
import React from 'react';
import Parser from 'html-react-parser';
import { PropTypes } from 'prop-types';
import './style.scss';
const SearchResults = (props) => {
if (!props.data) {
if (!props.data || props.data[0] === null) {
return <div>Loading...</div>;
} else if (props.data.length < 1) {
return <div>No results found</div>;
}
// sort array alphabetically
const sortedInterviews = props.data.sort((a, b) => a.name.localeCompare(b.name));
const trimText = (text, length) => {
if (text === null) {
return '';
}
return text.length <= length ? text : `${text.substr(0, length)}...`;
};
const highlightTerm = (text) => {
const cleanTerm = props.term.replace(/[^a-zA-Z 0-9]+/g, '').toLowerCase();
const regex = new RegExp(cleanTerm, 'ig');
return text.replace(regex, `<span>${cleanTerm}</span>`);
};
const processText = (text, length = 1500) => highlightTerm(trimText(text, length));
const findFirstQuestion = (interview) => {
let { answer } = interview.interview[interview.matchedIndex];
let id = interview.interview[interview.matchedIndex].question;
if (answer === null) {
const firstNonNullAnswer = interview.interview.find(question => question.answer !== null);
id = firstNonNullAnswer.question;
// eslint-disable-next-line
answer = firstNonNullAnswer.answer;
}
const { text } = props.questions.find(question => question.id === id);
return {
question: text,
answer: processText(answer),
};
};
return (
<div className="search-results">
<ul>
@ -21,11 +59,10 @@ const SearchResults = (props) => {
onClick={props.toggleSingleInterview}
>
<h3>{ interview.name }</h3>
<h5>1) Question goes here</h5>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce hendrerit dolor quis
ante mollis fringilla. <span>Lorem</span> ipsum dolor sit amet, consectetur adipiscing
elit.
<h5>{interview.matchedIndex + 1})&nbsp;
{ findFirstQuestion(interview).question }
</h5>
<p>{ Parser(findFirstQuestion(interview).answer) }
</p>
</li>
))
@ -38,6 +75,8 @@ const SearchResults = (props) => {
SearchResults.propTypes = {
data: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
toggleSingleInterview: PropTypes.func.isRequired,
questions: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
term: PropTypes.string.isRequired,
};
export default SearchResults;

View File

@ -28,9 +28,9 @@
content: '';
position: absolute;
top: -10%;
left: -15%;
left: -2%;
height: 120%;
width: 130%;
width: 104%;
background-color: $red;
color: $navy;
z-index: -1;

File diff suppressed because one or more lines are too long