mirror of
https://github.com/seigler/triplebyte-react-spa
synced 2025-07-26 07:16:10 +00:00
feat: sync redux to localstorage
This commit is contained in:
parent
9242ce66ec
commit
ff4a941dd8
10 changed files with 230 additions and 149 deletions
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
20
components/cardMover.js
Normal 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>
|
||||||
|
</>;
|
||||||
|
}
|
|
@ -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
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
9
next.config.js
Normal 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
|
||||||
|
},
|
||||||
|
};
|
|
@ -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)
|
|
||||||
|
|
59
store.js
59
store.js
|
@ -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
|
||||||
|
))
|
||||||
)
|
)
|
||||||
}
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue