In part 1 of my article, I discussed why I decided to rebuild my personal portfolio with a Rails API and React & Redux front-end, and touched on the set up of my application.
In part 2, we will take a look at the features that I built and how they work based on the Redux Flow.
Feature Highlights
Filtering Projects By Stacks
Some research show that “6 seconds is the average time recruiters spent reading a resume”. With that in mind, I tried to design a portfolio website with a simple UI and features that will keep users engaged, and focused on the most important visual elements.
For a full-stack software engineer role, one of the most important things recruiters ask is “does the candidate have any experience using ‘xyz’ language or frameworks?” What that in mind, I designed the portfolio website with a simple filter bar so any visitor can see exactly which projects correspond to which sets of selected technologies.
When the user presses a filter button, it will trigger an onClick event, calling the addFilter
or removeFilter
callback prop (line 34 and line 39), based on the current state of the button (the button state is handled in my local React state.)
1 import React, { Component } from 'react'
2
3 class FilterButton extends Component {
4 state = {
5 selected: undefined
6 }
7
8 componentDidMount() {
9 const { selectedStackIds, stack } = this.props
10 const myStackId = stack.id
11
12 this.setState({
13 selected: selectedStackIds.includes(myStackId.toString())
14 })
15 }
16
17 getButtonClassnames = () => {
18 const { selected } = this.state
19
20 let renderClasses = "btn btn-outline-info btn-sm"
21 if (selected) {
22 renderClasses = "btn btn-outline-info btn-sm active"
23 }
24
25 return renderClasses
26 }
27
28 handleOnClick = event => {
29 let pressed = this.state.selected
30 console.log('button was active: '+ this.state.selected)
31 const stackClicked = event.target.id
32
33 if (!pressed) {
34 this.props.addFilter(stackClicked)
35 this.setState({
36 selected: true
37 })
38 } else {
39 this.props.removeFilter(stackClicked)
40 this.setState({
41 selected: false
42 })
43 }
44 }
45
46 render() {
47 const { stack } = this.props
48 const renderClasses = this.getButtonClassnames()
49
50 return (
51 <button
52 id={stack.id}
53 type="button"
54 className={renderClasses}
55 aria-pressed={this.state.selected}
56 value={stack}
57 onClick={this.handleOnClick}>
58 {stack.name}
59 </button >
60 )
61 }
62 }
63
64 export default FilterButton
When the addFilter
or removeFilter
function in the ProjectsContainer
is invoked, it will execute the action creator below, which will return an action object:
// portfolio-frontend/src/actions/filterProjects.js
export const addFilter = stackId => {
return {
type: 'ADD_FILTER',
stackId
}
}
export const removeFilter = stackId => {
return {
type: 'REMOVE_FILTER',
stackId
}
}
The returned action object will then be dispatched to projectsReducer
, which will modify copies of the selectedStackIds
and filteredProjects
state in the Redux store. The reducer will then return the new version of our global state based on the sent action.
// portfolio-frontend/src/reducers/projectsReducer.js
const projectsReducer = (state = {
allProjects: [],
stacks: [],
selectedStackIds: [],
filteredProjects: [],
loading: false,
}, action) => {
let stackIds
let filteredProjects = []
...
case 'ADD_FILTER':
filteredProjects = state.filteredProjects.filter(proj => {
return proj.stacks.some(stack => stack.id.toString() === action.stackId)
})
stackIds = state.selectedStackIds.concat(action.stackId)
// Set store unique stackIds
stackIds = [...new Set(stackIds)]
return {
...state,
selectedStackIds: stackIds,
filteredProjects: filteredProjects,
}
case 'REMOVE_FILTER':
stackIds = state.selectedStackIds
stackIds.splice(stackIds.indexOf(action.stackId), 1)
filteredProjects = state.allProjects
// only include projects that have all the selected stacks
if (stackIds.length > 0) {
filteredProjects = state.allProjects.filter(proj => {
const projectStacks = proj.stacks.map(proj => proj['id'].toString())
const includesSelectedStacks = stackIds.every(selectedStack =>
projectStacks.includes(selectedStack)
)
return includesSelectedStacks
})
}
return {
...state,
filteredProjects: filteredProjects,
selectedStackIds: stackIds,
}
...
The project components subscribed to Redux store will re-render when state changes, displaying not only the toggled button update but also the filtered project results. This all happens on the client-side without ever needing to communicate with the Rails server.
Adding Comments to a Project
The addComment
action works similarly to the addFilter
action. However, instead of just updating the local state, store, and re-rendering the component, it also sends an asynchronous POST request to the Rails API using Javascript’s Fetch API. This is necessary for persisting the new comment record into our Postgres database.
Upon submission of the form, the addComment()
function will dispatch the following action to the store:
// portfolio-frontend/src/actions/addComment.js
export const addComment = comment => {
return (dispatch) => {
fetch(`http://localhost:3000/api/v1/projects/${comment.project_id}/comments`, {
headers: {
// data content sent to backend will be json
'Content-Type': 'application/json',
// what content types will be accepted on the return of data
'Accept': 'application/json'
},
method: 'POST',
// tell server to expect data as a JSON string
body: JSON.stringify(comment)
})
//immediately render the new data
.then(resp => resp.json())
.then(newComment => dispatch({ type: 'ADD_COMMENT', comment: newComment }))
}
}
Here, I am using a middleware Redux Thunk. It allows the action creator to take the dispatch function as an argument, giving us access to dispatch function. Next, we send the action returned by addComment
action creator to the projectsReducer
immediately after the asynchronous fetch request is resolved.
Lastly, projectsReducer
will update our store with the remote data that has just been persisted.
//portfolio-frontend/src/reducers/projectsReducer.js
...
case 'ADD_COMMENT':
let index = state.filteredProjects.findIndex(project => project.id === action.comment.project_id)
let project = state.filteredProjects[index]
return {
...state,
filteredProjects: [
...state.filteredProjects.slice(0, index),
{ ...project, comments: project.comments.concat(action.comment) },
...state.filteredProjects.slice(index + 1)
]
}
The new comment
component will be rendered in the browser:
Conclusion
With this portfolio website, I hope it adds additional color beyond the paper resume. It tells a story of a full stack web developer who can hit the ground running and contribute not only robust code, but also keen design principles.
In addition to what exists now, I also plan on adding a contact page (with a contact form and social media links), a "featured project" button on the homepage to bring the user directly to my latest project showcase, and possibly a dark mode toggle.
I would love to hear your suggestions for any other features that you think might be a great addition to my portfolio. Thank you for reading and stay tuned for the deployed website.
Top comments (1)
I love your elegant style of coding