Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2382 from studentinsights/feature/home-feed-stude…
…nt-voice-cards Student voice: Highlight new student voice surveys in home feed
- Loading branch information
Showing
18 changed files
with
611 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import PropTypes from 'prop-types'; | ||
import React from 'react'; | ||
import _ from 'lodash'; | ||
import hash from 'object-hash'; | ||
import Card from '../components/Card'; | ||
|
||
|
||
// Render a card in the feed showing there are student voice surveys in, | ||
// and allow clicking to see list of students and then jump to their profiles. | ||
export default class StudentVoiceCard extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
isExpanded: false, | ||
shuffleSeed: props.shuffleSeed || _.random(0, 32) | ||
}; | ||
|
||
this.onExpandClicked = this.onExpandClicked.bind(this); | ||
} | ||
|
||
onExpandClicked(e) { | ||
e.preventDefault(); | ||
this.setState({isExpanded: true}); | ||
} | ||
|
||
render() { | ||
const {style = {}} = this.props; | ||
return ( | ||
<Card className="StudentVoiceCard" style={style}> | ||
{this.renderStudentsAndCount()} | ||
</Card> | ||
); | ||
} | ||
|
||
renderStudentsAndCount() { | ||
const {studentVoiceCardJson} = this.props; | ||
const {students} = studentVoiceCardJson; | ||
const {isExpanded, shuffleSeed} = this.state; | ||
const emoji = <span style={{position: 'relative', top: 1}}>💬</span>; | ||
|
||
// If the list is <=3 students, show them all inline with links. | ||
// If it's more, pick two random to show and then +n more, which expands. | ||
if (students.length === 0) { | ||
return null; // shouldn't happen, but guard | ||
} else if (students.length === 1) { | ||
return <div>{this.renderStudentLink(students[0])} shared their perspective in a student voice survey. {emoji}</div>; | ||
} else if (students.length === 2) { | ||
return <div>{this.renderStudentLink(students[0])} and {this.renderStudentLink(students[1])} shared their perspective in student voice surveys. {emoji}</div>; | ||
} else if (students.length === 3) { | ||
return <div>{this.renderStudentLink(students[0])}, {this.renderStudentLink(students[1])} and {this.renderStudentLink(students[2])} shared their perspective in student voice surveys. {emoji}</div>; | ||
} | ||
|
||
const shuffledStudents = _.sortBy(students, student => hash({...student, shuffleSeed})); | ||
if (!isExpanded) { | ||
return <div>{this.renderStudentLink(shuffledStudents[0])}, {this.renderStudentLink(shuffledStudents[1])} and {this.renderExpandableListLink(shuffledStudents.slice(2))} shared their perspective in student voice surveys. {emoji}</div>; | ||
} else { | ||
return <div>{this.renderStudentLink(shuffledStudents[0])}, {this.renderStudentLink(shuffledStudents[1])} and {this.renderMoreStudentsText(shuffledStudents.slice(2))} shared their perspective in student voice surveys. {emoji} {this.renderExpandedList(shuffledStudents.slice(2))}</div>; | ||
} | ||
} | ||
|
||
renderMoreStudentsText(restOfStudents) { | ||
return `${restOfStudents.length} more students`; | ||
} | ||
|
||
renderExpandableListLink(restOfStudents) { | ||
return <a href="#" onClick={this.onExpandClicked}>{this.renderMoreStudentsText(restOfStudents)}</a>; | ||
} | ||
|
||
renderExpandedList(restOfStudents) { | ||
const sortedStudents = _.sortBy(restOfStudents, student => `${student.last_name} ${student.first_name}`); | ||
return ( | ||
<div style={{paddingTop: 10, paddingLeft: 10, maxHeight: 190, overflowY: 'scroll'}}> | ||
{sortedStudents.map(student => ( | ||
<div key={student.id} style={{padding: 5, paddingTop: 0}}>{this.renderStudentLink(student)}</div> | ||
))} | ||
</div> | ||
); | ||
} | ||
|
||
renderStudentLink(student) { | ||
return <a style={{fontWeight: 'bold'}} href={`/students/${student.id}`}>{student.first_name} {student.last_name}</a>; | ||
} | ||
} | ||
StudentVoiceCard.propTypes = { | ||
studentVoiceCardJson: PropTypes.shape({ | ||
latest_form_timestamp: PropTypes.string.isRequired, | ||
imported_forms_for_date_count: PropTypes.number.isRequired, | ||
students: PropTypes.arrayOf(PropTypes.shape({ | ||
id: PropTypes.number.isRequired, | ||
first_name: PropTypes.string.isRequired, | ||
last_name: PropTypes.string.isRequired | ||
})).isRequired | ||
}).isRequired, | ||
style: PropTypes.object, | ||
shuffleSeed: PropTypes.number | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import React from 'react'; | ||
import {storiesOf} from '@storybook/react'; | ||
import {testEl, propsWithNStudents} from './StudentVoiceCard.test'; | ||
|
||
storiesOf('feed/StudentVoiceCard', module) // eslint-disable-line no-undef | ||
.add('combinations', () => ( | ||
<div> | ||
{testEl(propsWithNStudents(1))} | ||
{testEl(propsWithNStudents(2))} | ||
{testEl(propsWithNStudents(3))} | ||
{testEl(propsWithNStudents(4))} | ||
{testEl({...propsWithNStudents(4), shuffleSeed: 100})} | ||
{testEl({...propsWithNStudents(4), shuffleSeed: 200})} | ||
</div> | ||
)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import renderer from 'react-test-renderer'; | ||
import StudentVoiceCard from './StudentVoiceCard'; | ||
import {withDefaultNowContext} from '../testing/NowContainer'; | ||
|
||
function testStudents() { | ||
return [{ | ||
id: 4, | ||
first_name: 'Luke', | ||
last_name: 'Skywalker' | ||
}, { | ||
id: 5, | ||
first_name: 'Mari', | ||
last_name: 'Kenobi' | ||
}, { | ||
id: 6, | ||
first_name: 'Kylo', | ||
last_name: 'Ren' | ||
}, { | ||
id: 7, | ||
first_name: 'Darth', | ||
last_name: 'Tater' | ||
}]; | ||
} | ||
|
||
export function testProps(props = {}) { | ||
return { | ||
shuffleSeed: 42, | ||
studentVoiceCardJson: { | ||
latest_form_timestamp: '2018-03-11T11:03:00.000Z', | ||
imported_forms_for_date_count: 2, | ||
students: testStudents() | ||
}, | ||
...props | ||
}; | ||
} | ||
|
||
export function propsWithNStudents(n) { | ||
const props = testProps(); | ||
return { | ||
...props, | ||
studentVoiceCardJson: { | ||
...props.studentVoiceCardJson, | ||
students: testStudents().slice(0, n) | ||
} | ||
}; | ||
} | ||
|
||
|
||
export function testEl(props) { | ||
return withDefaultNowContext(<StudentVoiceCard {...props} />); | ||
} | ||
|
||
it('renders without crashing', () => { | ||
const el = document.createElement('div'); | ||
ReactDOM.render(testEl(testProps()), el); | ||
expect($(el).text()).toContain('Darth Tater, Kylo Ren and 2 more students shared their perspective in student voice surveys. 💬'); | ||
expect($(el).find('a').toArray().map(el => $(el).attr('href'))).toEqual([ | ||
'/students/7', | ||
'/students/6', | ||
'#' | ||
]); | ||
}); | ||
|
||
describe('snapshots', () => { | ||
it('works with one student', () => { | ||
const tree = renderer.create(testEl(propsWithNStudents(1))).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
|
||
it('works with two students', () => { | ||
const tree = renderer.create(testEl(propsWithNStudents(2))).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
|
||
it('works with three students', () => { | ||
const tree = renderer.create(testEl(propsWithNStudents(3))).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
|
||
it('works with four students', () => { | ||
const tree = renderer.create(testEl(propsWithNStudents(4))).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
}); |
Oops, something went wrong.