From a0fcfcb8476ceb9f732d73499e98374938ec42b8 Mon Sep 17 00:00:00 2001 From: Ravi P Date: Mon, 4 Apr 2016 12:50:50 -0700 Subject: [PATCH] shopping list app completed --- app/actions/LaneActions.js | 6 ++ app/actions/NoteActions.js | 3 + app/components/App.jsx | 32 ++++++++-- app/components/Editable.jsx | 56 +++++++++++++++++ app/components/Lane.jsx | 107 ++++++++++++++++++++++++++++++++ app/components/Lanes.jsx | 10 +++ app/components/Note.jsx | 78 ++++++++++++++++++++++- app/components/Notes.jsx | 18 ++++++ app/index.jsx | 7 ++- app/libs/alt.js | 7 +++ app/libs/persist.js | 18 ++++++ app/libs/storage.js | 13 ++++ app/main.css | 120 +++++++++++++++++++++++++++++++++++- app/stores/LaneStore.js | 67 ++++++++++++++++++++ app/stores/NoteStore.js | 48 +++++++++++++++ build/index.html | 3 +- package.json | 4 ++ 17 files changed, 589 insertions(+), 8 deletions(-) create mode 100644 app/actions/LaneActions.js create mode 100644 app/actions/NoteActions.js create mode 100644 app/components/Editable.jsx create mode 100644 app/components/Lane.jsx create mode 100644 app/components/Lanes.jsx create mode 100644 app/components/Notes.jsx create mode 100644 app/libs/alt.js create mode 100644 app/libs/persist.js create mode 100644 app/libs/storage.js create mode 100644 app/stores/LaneStore.js create mode 100644 app/stores/NoteStore.js diff --git a/app/actions/LaneActions.js b/app/actions/LaneActions.js new file mode 100644 index 0000000..65d348f --- /dev/null +++ b/app/actions/LaneActions.js @@ -0,0 +1,6 @@ +import alt from '../libs/alt'; + +export default alt.generateActions( + 'create', 'update', 'delete', + 'attachToLane', 'detachFromLane' +); \ No newline at end of file diff --git a/app/actions/NoteActions.js b/app/actions/NoteActions.js new file mode 100644 index 0000000..387e111 --- /dev/null +++ b/app/actions/NoteActions.js @@ -0,0 +1,3 @@ +import alt from '../libs/alt'; + +export default alt.generateActions('create', 'update', 'delete'); \ No newline at end of file diff --git a/app/components/App.jsx b/app/components/App.jsx index 56912f8..82605e3 100644 --- a/app/components/App.jsx +++ b/app/components/App.jsx @@ -1,8 +1,32 @@ +import AltContainer from 'alt-container'; import React from 'react'; -import Note from './Note.jsx'; +import Lanes from './Lanes.jsx'; +import LaneActions from '../actions/LaneActions'; +import LaneStore from '../stores/LaneStore'; + +export default class APP extends React.Component { -export default class App extends React.Component { render() { - return ; + + return ( +
+ + + LaneStore.getState().lanes || [] + }} + > + + +
+ ); + } + + addLane() { + LaneActions.create({name: 'New List'}); } -} +} \ No newline at end of file diff --git a/app/components/Editable.jsx b/app/components/Editable.jsx new file mode 100644 index 0000000..020be6a --- /dev/null +++ b/app/components/Editable.jsx @@ -0,0 +1,56 @@ +import React from 'react'; + +export default class Editable extends React.Component { + + render() { + const {value, onEdit, onValueClick, editing, ...props} = this.props; + + return ( +
+ {editing ? this.renderEdit() : this.renderValue()} +
+ ); + } + + renderEdit = () => { + return e ? e.selectionStart = this.props.value.length : null + } + autoFocus={true} + defaultValue={this.props.value} + onBlur={this.finishEdit} + onKeyPress={this.checkEnter} />; + }; + + renderValue = () => { + const onDelete = this.props.onDelete; + + return ( +
+ {this.props.value} + {onDelete ? this.renderDelete() : null } +
+ ); + }; + + renderDelete = () => { + return ; + }; + + checkEnter = (e) => { + if(e.key === 'Enter') { + this.finishEdit(e); + } + }; + + finishEdit = (e) => { + const value = e.target.value; + + if(this.props.onEdit) { + this.props.onEdit(value); + } + }; +} \ No newline at end of file diff --git a/app/components/Lane.jsx b/app/components/Lane.jsx new file mode 100644 index 0000000..a7976b5 --- /dev/null +++ b/app/components/Lane.jsx @@ -0,0 +1,107 @@ +import AltContainer from 'alt-container'; +import React from 'react'; +import Notes from './Notes.jsx'; +import NoteActions from '../actions/NoteActions'; +import NoteStore from '../stores/NoteStore'; +import LaneActions from '../actions/LaneActions'; +import Editable from './Editable.jsx'; + +export default class Lane extends React.Component { + render() { + const {lane, ...props} = this.props; + + return ( +
+
+
+ +
+ +
+ +
+
+ NoteStore.getNotesByIds(lane.notes) + }} + > + + +
+ ) + } + + editNote(id, item, sku, price) { + // Don't modify if trying set an empty value + if(!item.trim()) { + NoteActions.update({id, editing: false}); + return; + } + // if(!sku.trim()) { + // NoteActions.update({id, editing: false}); + // return; + // } + // if(!price.trim()) { + // NoteActions.update({id, editing: false}); + // return; + // } + + NoteActions.update({id, item, sku, price, editing: false}); + } + + addNote = (e) => { + e.stopPropagation(); + + const laneId = this.props.lane.id; + const note = NoteActions.create({item: 'New item', sku: 'SKU#233', price: '$1.99'}); + + LaneActions.attachToLane({ + noteId: note.id, + laneId + }); + }; + + deleteNote = (noteId, e) => { + e.stopPropagation(); + + const laneId = this.props.lane.id; + + LaneActions.detachFromLane({laneId, noteId}); + NoteActions.delete(noteId); + }; + + editName = (name) => { + const laneId = this.props.lane.id; + + // Don't modify if trying set an empty value + if(!name.trim()) { + LaneActions.update({id: laneId, editing: false}); + + return; + } + + LaneActions.update({id: laneId, name, editing: false}); + }; + + deleteLane = () => { + const laneId = this.props.lane.id; + + LaneActions.delete(laneId); + }; + + activateLaneEdit = () => { + const laneId = this.props.lane.id; + + LaneActions.update({id: laneId, editing: true}); + }; + + activateNoteEdit(id) { + NoteActions.update({id, editing: true}); + } +} \ No newline at end of file diff --git a/app/components/Lanes.jsx b/app/components/Lanes.jsx new file mode 100644 index 0000000..6828002 --- /dev/null +++ b/app/components/Lanes.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import Lane from './Lane.jsx'; + +export default ({lanes}) => { + return ( +
{lanes.map(lane => + + )}
+ ); +} \ No newline at end of file diff --git a/app/components/Note.jsx b/app/components/Note.jsx index 5d49b21..0063613 100644 --- a/app/components/Note.jsx +++ b/app/components/Note.jsx @@ -1,3 +1,79 @@ import React from 'react'; -export default () =>
Learn React and Webpack!
; +export default class Note extends React.Component { + constructor(props) { + super(props); + + // Keeps track of "editing" state. + this.state = { + editing: false + }; + } + + render() { + // Rendering the component differently based on state. + if(this.state.editing) { + return this.renderEdit(); + } + return this.renderNote(); + } + + renderEdit = () => { + return ( + e ? e.selectionStart = this.props.item.length : null + } + autoFocus={true} + defaultValue={this.props.item} + onBlur={this.finishEdit} + onKeyPress={this.checkEnter} />; + }; + + renderNote = () => { + // When user clicks a normal item, it triggers editing logic. + const onDelete = this.props.onDelete; + + return ( +
+ {this.props.item} + SKU# {this.props.sku} + Price {this.props.price} + {onDelete ? this.renderDelete() : null } +
+ ); + }; + + renderDelete = () => { + return ; + }; + + // Enter edit mode. + edit = () => { + this.setState({ + editing: true + }); + }; + + checkEnter = (e) => { + // When user hits "enter", it finishes up. + if(e.key === 'Enter') { + this.finishEdit(e); + } + }; + + finishEdit = (e) => { + const value = e.target.value; + + if(this.props.onEdit) { + this.props.onEdit(value); + + // Exit edit mode. + this.setState({ + editing: false + }); + } + }; +} \ No newline at end of file diff --git a/app/components/Notes.jsx b/app/components/Notes.jsx new file mode 100644 index 0000000..582c7d7 --- /dev/null +++ b/app/components/Notes.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import Editable from './Editable.jsx'; + +export default ({notes, onValueClick, onEdit, onDelete}) => { + return ( + + ); +} diff --git a/app/index.jsx b/app/index.jsx index 5d1364e..2f47991 100644 --- a/app/index.jsx +++ b/app/index.jsx @@ -1,7 +1,12 @@ import './main.css'; - import React from 'react'; import ReactDOM from 'react-dom'; import App from './components/App.jsx'; +import alt from './libs/alt'; +import storage from './libs/storage'; +import persist from './libs/persist'; + +persist(alt, storage, 'app'); ReactDOM.render(, document.getElementById('app')); + diff --git a/app/libs/alt.js b/app/libs/alt.js new file mode 100644 index 0000000..180afe1 --- /dev/null +++ b/app/libs/alt.js @@ -0,0 +1,7 @@ +import Alt from 'alt'; +//import chromeDebug from 'alt-utils/lib/chromeDebug'; + +const alt = new Alt(); +//chromeDebug(alt); + +export default alt; \ No newline at end of file diff --git a/app/libs/persist.js b/app/libs/persist.js new file mode 100644 index 0000000..0634e9e --- /dev/null +++ b/app/libs/persist.js @@ -0,0 +1,18 @@ +import makeFinalStore from 'alt-utils/lib/makeFinalStore'; + +export default function(alt, storage, storeName) { + const finalStore = makeFinalStore(alt); + + try { + alt.bootstrap(storage.get(storeName)); + } + catch(e) { + console.error('Failed to bootstrap data', e); + } + + finalStore.listen(() => { + if(!storage.get('debug')) { + storage.set(storeName, alt.takeSnapshot()); + } + }); +} \ No newline at end of file diff --git a/app/libs/storage.js b/app/libs/storage.js new file mode 100644 index 0000000..60eb9e6 --- /dev/null +++ b/app/libs/storage.js @@ -0,0 +1,13 @@ +export default { + get(k) { + try { + return JSON.parse(localStorage.getItem(k)); + } + catch(e) { + return null; + } + }, + set(k, v) { + localStorage.setItem(k, JSON.stringify(v)); + } +}; \ No newline at end of file diff --git a/app/main.css b/app/main.css index 62a79b7..4ec06c9 100644 --- a/app/main.css +++ b/app/main.css @@ -1,3 +1,121 @@ body { - background: cornsilk; + background: #99ceff; + font-family: "Comic Sans MS", cursive, sans-serif; +} + +h1 { + text-align: center; +} + +.lane { + display: inline-block; + + margin: 1em; + + background-color: #efefef; + border: 1px solid #ccc; + border-radius: 0.5em; + + min-width: 10em; + vertical-align: top; +} + +.lane-header { + overflow: auto; + + padding: 1em; + + color: #efefef; + background-color: #333; + + border-top-left-radius: 0.5em; + border-top-right-radius: 0.5em; +} + +.lane-name { + float: left; +} + +.lane-add-note { + float: left; + + margin-right: 0.5em; +} + +.lane-delete { + float: right; + + margin-left: 0.5em; + + visibility: hidden; +} +.lane-header:hover .lane-delete { + visibility: visible; +} + +.add-lane, .lane-add-note button { + cursor: pointer; + + background-color: #fdfdfd; + border: 1px solid #ccc; +} + +.add-lane { + border: 10px solid #333; + border-radius: 0.5em; +} + +.lane-delete button { + padding: 0; + + cursor: pointer; + + color: white; + background-color: rgba(0, 0, 0, 0); + border: 0; +} + +.add-note { + background-color: #fdfdfd; + border: 1px solid #ccc; +} + +.notes { + margin: 0.5em; + padding-left: 0; + max-width: 10em; + list-style: none; +} + +.note { + margin-bottom: 0.5em; + padding: 0.5em; + background-color: #fdfdfd; + box-shadow: 0 0 0.3em 0.03em rgba(0, 0, 0, 0.3); +} + +.note:hover { + box-shadow: 0 0 0.3em 0.03em rgba(0, 0, 0, 0.7); + transition: 0.6s; +} + +.note .value { + display: inline-block; +} + +span { + display: block; +} + +.note .delete { + float: right; + padding: 0; + background-color: #fdfdfd; + border: none; + cursor: pointer; + visibility: hidden; +} + +.note:hover .delete { + visibility: visible; } \ No newline at end of file diff --git a/app/stores/LaneStore.js b/app/stores/LaneStore.js new file mode 100644 index 0000000..038bf5f --- /dev/null +++ b/app/stores/LaneStore.js @@ -0,0 +1,67 @@ +import uuid from 'node-uuid'; +import alt from '../libs/alt'; +import LaneActions from '../actions/LaneActions'; + +class LaneStore { + constructor() { + this.bindActions(LaneActions); + + this.lanes = []; + } + create(lane) { + const lanes = this.lanes; + + lane.id = uuid.v4(); + lane.notes = lane.notes || []; + + this.setState({ + lanes: lanes.concat(lane) + }); + } + update(updatedLane) { + const lanes = this.lanes.map(lane => { + if(lane.id === updatedLane.id) { + return Object.assign({}, lane, updatedLane); + } + + return lane; + }); + + this.setState({lanes}); + } + delete(id) { + this.setState({ + lanes: this.lanes.filter(lane => lane.id !== id) + }); + } + attachToLane({laneId, noteId}) { + const lanes = this.lanes.map(lane => { + if(lane.id === laneId) { + if(lane.notes.includes(noteId)) { + console.warn('Already attached note to lane', lanes); + } + else { + lane.notes.push(noteId); + } + } + + return lane; + }); + + this.setState({lanes}); + } + + detachFromLane({laneId, noteId}) { + const lanes = this.lanes.map(lane => { + if(lane.id === laneId) { + lane.notes = lane.notes.filter(note => note !== noteId); + } + + return lane; + }); + + this.setState({lanes}); + } +} + +export default alt.createStore(LaneStore, 'LaneStore'); \ No newline at end of file diff --git a/app/stores/NoteStore.js b/app/stores/NoteStore.js new file mode 100644 index 0000000..3db7daf --- /dev/null +++ b/app/stores/NoteStore.js @@ -0,0 +1,48 @@ +import uuid from 'node-uuid'; +import alt from '../libs/alt'; +import NoteActions from '../actions/NoteActions'; + +class NoteStore { + constructor() { + this.bindActions(NoteActions); + + this.notes = []; + + this.exportPublicMethods({ + getNotesByIds: this.getNotesByIds.bind(this) + }); + } + create(note) { + + const notes = this.notes; + + note.id = uuid.v4(); + + this.setState({ + notes: notes.concat(note) + }); + return note; + } + update(updatedNote) { + const notes = this.notes.map(note => { + if(note.id === updatedNote.id) { + return Object.assign({}, note, updatedNote); + } + return note; + }); + this.setState({notes}); + } + delete(id) { + this.setState({ + notes: this.notes.filter(note => note.id !== id) + }); + } + + getNotesByIds(ids) { + return (ids || []).map( + id => this.notes.filter(note => note.id === id) + ).filter(a => a.length).map(a => a[0]); + } +} + +export default alt.createStore(NoteStore, 'NoteStore'); \ No newline at end of file diff --git a/build/index.html b/build/index.html index b052827..956150e 100644 --- a/build/index.html +++ b/build/index.html @@ -2,9 +2,10 @@ - Kanban app + Shopping List App +

Ravi's Shopping Lists

diff --git a/package.json b/package.json index f364602..826855f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,10 @@ "webpack-merge": "^0.7.3" }, "dependencies": { + "alt": "^0.18.4", + "alt-container": "^1.0.2", + "alt-utils": "^1.0.0", + "node-uuid": "^1.4.7", "react": "^0.14.7", "react-dom": "^0.14.7" }