feat: sync redux to localstorage

This commit is contained in:
Joshua Seigler 2020-02-19 02:00:21 -05:00
parent 9242ce66ec
commit ff4a941dd8
10 changed files with 230 additions and 149 deletions

12
.gitignore vendored
View file

@ -1,17 +1,17 @@
# dependencies # dependencies
/node_modules node_modules
/.pnp .pnp
.pnp.js .pnp.js
# testing # testing
/coverage coverage/
# next.js # next.js
/.next/ .next/
/out/ out/
# production # production
/build build/
# misc # misc
.DS_Store .DS_Store

View file

@ -1,19 +1,38 @@
import React from 'react'; import React from 'react';
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { withRedux } from '../lib/redux'; import { withRedux } from 'lib/redux';
const addCard = ({ columnIndex }) => { const addCard = ({ column }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
return <a onClick={ return <>
() => { <a className='addCard' onClick={
const userText = window.prompt('New card text:', '(no text)'); () => {
dispatch({ type: 'ADD_CARD', payload: { const userText = window.prompt('New card text:', '');
column: columnIndex, if (userText !== null) {
text: userText dispatch({ type: 'ADD_CARD', payload: {
} }); column,
} text: userText
}>+ Add a card</a>; }});
}
}
}><span>+</span> Add a card</a>
<style jsx>{`
.addCard {
margin-top: 12px;
cursor: pointer;
opacity: 0.4;
display: block;
padding: 0 8px;
}
.addCard:hover {
opacity: 1;
}
span {
font-size: 1.5em;
}
`}</style>
</>;
} }
export default withRedux(addCard); export default withRedux(addCard);

View file

@ -1,7 +1,26 @@
import React from 'react' import React from 'react';
import CardMover from 'components/cardMover';
export default ({ children }) => { export default ({ text = 'No text', moveLeft, moveRight }) => {
return <div className='card'> return <>
I am a card <div className='card'>
</div> { moveLeft && <CardMover direction='left' action={moveLeft} /> }
<div className='text'>
{ text }
</div>
{ moveRight && <CardMover direction='right' action={moveRight} /> }
</div>
<style jsx>{`
.card {
display: flex;
flex-direction: row;
margin-bottom: 6px;
background-color: white;
}
.text {
flex-grow: 1;
padding: 14px 12px;
}
`}</style>
</>;
} }

20
components/cardMover.js Normal file
View file

@ -0,0 +1,20 @@
import React from 'react';
export default ({ direction, action }) => {
return <>
<a className='cardMover' onClick={action}>
{ direction === 'left' ? '〈' : '〉' }
</a>
<style jsx>{`
.cardMover {
display: flex;
cursor: pointer;
padding-${direction}: 8px;
text-align: center;
flex-direction: column;
justify-content: center;
font-size: 1.5em;
}
`}</style>
</>;
}

View file

@ -1,41 +0,0 @@
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
const useClock = () => {
return useSelector(
state => ({
lastUpdate: state.lastUpdate,
light: state.light,
}),
shallowEqual
)
}
const formatTime = time => {
// cut off except hh:mm:ss
return new Date(time).toJSON().slice(11, 19)
}
const Clock = () => {
const { lastUpdate, light } = useClock()
return (
<div className={light ? 'light' : ''}>
{formatTime(lastUpdate)}
<style jsx>{`
div {
padding: 15px;
display: inline-block;
color: #82fa58;
font: 50px menlo, monaco, monospace;
background-color: #000;
}
.light {
background-color: #999;
}
`}</style>
</div>
)
}
export default Clock

View file

@ -1,23 +1,64 @@
import React from 'react'; import React from 'react';
import { useSelector, shallowEqual } from 'react-redux'; import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { withRedux } from '../lib/redux'; import { withRedux } from 'lib/redux';
import Card from './card'; import Card from 'components/card';
import AddCard from './addCard'; import AddCard from 'components/addCard';
const column = ({ name, index, headerColor }) => { const column = ({ name, id, headerColor }) => {
const cards = useSelector(state => state.columns[index].cards); const dispatch = useDispatch();
const cards = useSelector(state => state.cards.filter(
c => (c.column === id)
), shallowEqual);
const colIds = useSelector(state => state.columns.map(c => c.id));
const myIndex = colIds.findIndex(x => x === id);
const neighbors = [null, ...colIds, null];
const prevColId = neighbors[myIndex+1-1];
const nextColId = neighbors[myIndex+1+1];
return <div className='column'> const moverFactory = (id, column) => {
<h1 className='card-title' style={{ color: 'white', backgroundColor: headerColor }}>{name}</h1> if (column) {
{ cards.map((c, cardIndex) => <Card key={`card-${index}-${cardIndex}`} />) } return () => {
<AddCard index={index} /> dispatch({
type: 'MOVE_CARD',
payload: {
id,
column
}
});
}
} else {
return null;
}
}
return <section className='column'>
<h2
className='card-title'
style={{ color: '#FFFD', backgroundColor: headerColor }}
>{name}</h2>
{ cards.map(({ text, id }) => <Card
key={id}
text={text}
moveLeft={moverFactory(id, prevColId)}
moveRight={moverFactory(id, nextColId)}
/>) }
<AddCard column={id} />
<style jsx>{` <style jsx>{`
.column { .column {
flex-basis: 0;
flex-grow: 1;
margin: 12.5px; margin: 12.5px;
} }
h2 {
padding: 8px;
line-height: 14px;
margin-bottom: 6px;
font-size: inherit;
text-align: center;
}
`}</style> `}</style>
</div> </section>
} }
export default withRedux(column); export default withRedux(column);

View file

@ -1,36 +0,0 @@
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
const useCounter = () => {
const count = useSelector(state => state.count)
const dispatch = useDispatch()
const increment = () =>
dispatch({
type: 'INCREMENT',
})
const decrement = () =>
dispatch({
type: 'DECREMENT',
})
const reset = () =>
dispatch({
type: 'RESET',
})
return { count, increment, decrement, reset }
}
const Counter = () => {
const { count, increment, decrement, reset } = useCounter()
return (
<div>
<h1>
Count: <span>{count}</span>
</h1>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
)
}
export default Counter

9
next.config.js Normal file
View file

@ -0,0 +1,9 @@
const path = require('path');
module.exports = {
webpack(config, options) {
config.resolve.alias['components'] = path.join(__dirname, 'components')
config.resolve.alias['lib'] = path.join(__dirname, 'lib')
return config
},
};

View file

@ -1,44 +1,59 @@
import React from 'react' import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux' import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { useSelector, shallowEqual } from 'react-redux' import { withRedux } from 'lib/redux';
import { withRedux } from '../lib/redux'
import useInterval from '../lib/useInterval'
import Column from '../components/column'; import Column from 'components/column';
import Card from '../components/card';
const rehydrateStore = () => {
if (localStorage.getItem('triplebyte-react-spa') === null) {
return null;
} else {
return JSON.parse(localStorage.getItem('triplebyte-react-spa'));
}
};
const IndexPage = () => { const IndexPage = () => {
const [loaded, setLoaded] = useState(false);
const dispatch = useDispatch();
const columns = useSelector(state => state.columns, shallowEqual); const columns = useSelector(state => state.columns, shallowEqual);
useEffect(() => {
dispatch({ type: 'RESET', payload: rehydrateStore() });
setLoaded(true);
}, []);
if (!loaded) return null;
return ( return (
<main className='App'> <main className='App'>
{columns.map((c, index) => <Column index={index} key={`column${index}`} name={c.name} headerColor={c.headerColor} />)} {columns.map((c, index, array) => (
<Column
id={c.id}
key={`column-${c.id}`}
name={c.name}
headerColor={c.headerColor}
/>
))}
<style jsx>{` <style jsx>{`
.App { .App {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: 12.5px; padding: 12.5px;
} }
`}</style> `}</style>
<style global jsx>{` <style global jsx>{`
*, ::before, ::after {
box-sizing: inherit;
}
body { body {
box-sizing: border-box;
background: #ECEEEE; background: #ECEEEE;
margin: 0;
padding: 0;
font-family: sans-serif;
color: #000C;
} }
`}</style> `}</style>
</main> </main>
) )
} }
IndexPage.getInitialProps = ({ reduxStore }) => { export default withRedux(IndexPage);
// // Tick the time once, so we'll have a
// // valid time before first render
// const { dispatch } = reduxStore
// dispatch({
// type: 'TICK',
// light: typeof window === 'object',
// lastUpdate: Date.now(),
// })
return {}
}
export default withRedux(IndexPage)

View file

@ -1,31 +1,66 @@
import { createStore, applyMiddleware } from 'redux' import { compose, createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension' import { composeWithDevTools } from 'redux-devtools-extension';
const initialState = { const initialState = {
columns: [ columns: [
{ name: 'Backlog', headerColor: '#8E6E95', cards: [{}, {}] }, { name: 'Backlog', headerColor: '#8E6E95', id: 'backlog' },
{ name: 'In Progress', headerColor: '#8E6E95', headerColor: '#39A59C', cards: [{}, {}] }, { name: 'In Progress', headerColor: '#8E6E95', headerColor: '#39A59C', id: 'in-progress' },
{ name: 'Ready for Review', headerColor: '#344759', cards: [{}, {}] }, { name: 'Ready for Review', headerColor: '#344759', id: 'ready-for-review' },
{ name: 'Completed', headerColor: '#E8741E', cards: [{}, {}] }, { name: 'Completed', headerColor: '#E8741E', id: 'completed' },
] ],
} cards: [
],
};
const reducer = (state = initialState, {type, payload}) => { const reducer = (state = initialState, {type, payload}) => {
switch (type) { switch (type) {
case 'ADD_CARD': case 'ADD_CARD':
newCards = [...state.columns[action.payload.column].cards, { text: payload.text }];
return { return {
...state, ...state,
cards: state.cards.concat( {
id: `${Date.now()}-${Math.random()}`,
text: payload.text,
column: payload.column,
}),
}; };
case 'MOVE_CARD':
const cardIndex = state.cards.findIndex(c => c.id === payload.id);
const newCards = state.cards.slice(0);
newCards.splice(cardIndex, 1, {
...state.cards[cardIndex],
column: payload.column,
});
return {
...state,
cards: newCards,
};
case 'RESET':
if (payload) {
return payload;
} else {
return initialState;
}
default: default:
return state return state
} }
} };
const localStorageMiddleware = ({getState}) => {
return (next) => (action) => {
const result = next(action);
localStorage.setItem('triplebyte-react-spa', JSON.stringify(
getState()
));
return result;
};
};
export const initializeStore = (preloadedState = initialState) => { export const initializeStore = (preloadedState = initialState) => {
return createStore( return createStore(
reducer, reducer,
preloadedState, preloadedState,
composeWithDevTools(applyMiddleware()) composeWithDevTools(applyMiddleware(
localStorageMiddleware
))
) )
} };