├── src ├── styles │ ├── Task.css │ ├── index.css │ ├── Job.css │ └── App.css ├── fonts │ ├── bolts_sf.ttf │ └── proxima-nova.otf ├── App.test.js ├── index.js ├── reducer.js ├── events.js ├── App.js ├── components │ ├── Job.jsx │ ├── Stop.jsx │ ├── ChecklistManager.jsx │ └── Task.jsx ├── core.js └── JobFixtures.js ├── public ├── favicon.png └── index.html ├── .gitignore ├── README.md └── package.json /src/styles/Task.css: -------------------------------------------------------------------------------- 1 | .completed { 2 | text-decoration: line-through; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptainStack/truck-stop/master/public/favicon.png -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/fonts/bolts_sf.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptainStack/truck-stop/master/src/fonts/bolts_sf.ttf -------------------------------------------------------------------------------- /src/fonts/proxima-nova.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaptainStack/truck-stop/master/src/fonts/proxima-nova.otf -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /src/styles/Job.css: -------------------------------------------------------------------------------- 1 | table { 2 | border-collapse: collapse; 3 | width: 100%; 4 | transition: 0.5s; 5 | } 6 | table td { 7 | border: 1px solid black; 8 | padding: 5px; 9 | } 10 | table.active-job { 11 | background-color: OldLace; 12 | } 13 | table.complete-job { 14 | background-color: LightCyan; 15 | } 16 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Truck Stop 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Truck Stop 2 | Truck Stop is a web-based app from Convoy that helps truck drivers keep track of their jobs, each of which has a set of stops, each of which has a set of tasks. 3 | Drivers may mark tasks completed. A Stop is complete when all its tasks have been marked complete. A job is complete when all of its stops are complete. These changes are persistant across app loads. 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "truck-stop", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^15.5.3", 7 | "react-dom": "^15.5.3", 8 | "redux": "^3.6.0" 9 | }, 10 | "devDependencies": { 11 | "react-scripts": "0.9.5" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './styles/index.css'; 5 | import {createStore} from 'redux'; 6 | import reducer from './reducer'; 7 | 8 | export const store = createStore(reducer); 9 | 10 | const render = () => ReactDOM.render(, document.getElementById('root')); 11 | 12 | render(); 13 | store.subscribe(render); 14 | store.subscribe(() => localStorage.setItem('reduxState', JSON.stringify(store.getState()))); 15 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import {update_clicked_task, update_task_filter, add_image_to_task, INITIAL_STATE} from './core'; 2 | 3 | export default function reducer(state = INITIAL_STATE, action) { 4 | switch (action.type) { 5 | case 'UPDATE_SELECTED_TASK': 6 | return update_clicked_task(state, action.task, action.GPS); 7 | case 'UPDATE_FILTER': 8 | return update_task_filter(state, action.filter); 9 | case 'ADD_IMAGE_TO_TASK': 10 | return add_image_to_task(state, action.task, action.image); 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | import {store} from './index' 2 | 3 | export const taskClick = clicked_task => e => { 4 | navigator.geolocation.getCurrentPosition(position => { 5 | store.dispatch({ type: 'UPDATE_SELECTED_TASK', task: clicked_task, GPS: { latitude: position.coords.latitude, longitude: position.coords.longitude} }); 6 | }); 7 | } 8 | 9 | export const filterClick = clicked_filter => e => { 10 | store.dispatch({ type: 'UPDATE_FILTER', filter: clicked_filter }); 11 | } 12 | 13 | export const saveImage = (task, image) => { 14 | store.dispatch({ type: 'ADD_IMAGE_TO_TASK', task: task, image: image }); 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChecklistManager from './components/ChecklistManager'; 3 | import './styles/App.css'; 4 | 5 | const App = ({state}) => { 6 | return ( 7 |
8 |
9 |

TRUCK STOP

10 |
11 |
12 | 13 | 17 |
18 |
19 | ); 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/components/Job.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Stop from './Stop'; 3 | import '../styles/Job.css'; 4 | 5 | const Job = ({job, job_complete}) => { 6 | let job_status = job_complete ? 'Completed' : 'Active'; 7 | let stops = job.stops.map(stop => { 8 | return 9 | }); 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 | {stops} 17 | 18 |

{job.title} (Reference ID: {job.referenceId})

{job_status}
TypeAddressCargo DescriptionArrival DeadlineTasksStatus
19 |
20 |
21 | ); 22 | }; 23 | 24 | export default Job; -------------------------------------------------------------------------------- /src/components/Stop.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Task from './Task'; 3 | 4 | const Stop = ({stop}) => { 5 | let stop_status; 6 | let incomplete_tasks_count = 0; 7 | let task_cells = stop.tasks.map(task => { 8 | if (!task.completed) { 9 | incomplete_tasks_count++; 10 | } 11 | return 12 | }); 13 | 14 | switch(incomplete_tasks_count) { 15 | case 0: 16 | stop_status = 'Completed'; 17 | break; 18 | case stop.tasks.length: 19 | stop_status = 'Not Started'; 20 | break; 21 | default: 22 | stop_status = 'In progress'; 23 | } 24 | let arrival_date = new Date(stop.arrivalTime); 25 | 26 | return( 27 | 28 | {stop.type} 29 | {stop.address} 30 | {stop.cargoDescription} 31 | 32 | {`${arrival_date.toLocaleDateString()}`}
33 | {`${arrival_date.toLocaleTimeString()}`} 34 | 35 | {task_cells} 36 | {stop_status} 37 | ); 38 | }; 39 | 40 | export default Stop; 41 | -------------------------------------------------------------------------------- /src/styles/App.css: -------------------------------------------------------------------------------- 1 | header { 2 | background-color: #d84314; 3 | color: white; 4 | display: flex; 5 | justify-content: left; 6 | padding-left: 1em; 7 | } 8 | header a { 9 | text-decoration: none; 10 | color: white; 11 | font-family: "Bolts SF"; 12 | letter-spacing: 0.1em; 13 | font-size: 24pt; 14 | font-weight: 100; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | #main-container { 21 | padding: 2em; 22 | } 23 | #app-container { 24 | font-family: "Proxima Nova"; 25 | } 26 | 27 | footer { 28 | display: flex; 29 | justify-content: space-between; 30 | font-size: 0.75em; 31 | border-top: 1px solid rgba(51, 51, 51, 0.5); 32 | margin-top: 1em; 33 | padding-top: 0.5em; 34 | } 35 | footer a { 36 | text-decoration: none; 37 | color: rgba(51, 51, 51, 1); 38 | } 39 | footer a:hover { 40 | color: rgba(51, 51, 51, 0.6); 41 | } 42 | 43 | @font-face { 44 | font-family: "Bolts SF"; 45 | src: url('../fonts/bolts_sf.ttf'); 46 | } 47 | @font-face { 48 | font-family: "Proxima Nova"; 49 | src: url('../fonts/proxima-nova.otf'); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/ChecklistManager.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Job from './Job'; 3 | import { filterClick } from '../events'; 4 | 5 | const ChecklistManager = ({state}) => { 6 | let is_job_complete = job => { 7 | for (let stop of job.stops) { 8 | for (let task of stop.tasks) { 9 | if (!task.completed) { 10 | return false; 11 | } 12 | } 13 | } 14 | return true; 15 | } 16 | let job_elements = state.jobs.map(job => { 17 | let job_complete = is_job_complete(job); 18 | if ((state.tasks_shown === 'completed' && job_complete) || 19 | (state.tasks_shown === 'active' && !job_complete) || 20 | (state.tasks_shown === 'all')) { 21 | return 22 | } else { 23 | return null; 24 | } 25 | }); 26 | 27 | return ( 28 |
29 |

Showing {state.tasks_shown} jobs

30 |

31 | 32 | 33 | 34 |

35 | {job_elements} 36 |
37 | ); 38 | }; 39 | 40 | export default ChecklistManager; -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | import {jobs} from './JobFixtures'; 2 | 3 | let persistedState; 4 | 5 | // Rebuild the redux state by parsing the JSON string in localStorage 6 | if (localStorage.getItem('reduxState')) { 7 | persistedState = JSON.parse(localStorage.getItem('reduxState')); 8 | persistedState.selected_job = 9 | persistedState.jobs.find(job => job.referenceId === persistedState.selected_job.referenceId) || 10 | { title: 'NO JOB SELECTED', stops: [ {address: 'SELECT A JOB TO VIEW STOPS', tasks: [{completed: false, GPS: null}]} ] }; 11 | persistedState.selected_stop = persistedState.selected_job.stops.find(stop => stop.type === persistedState.selected_stop.type); 12 | } 13 | 14 | export const INITIAL_STATE = localStorage.getItem('reduxState') ? persistedState : 15 | { 16 | jobs: jobs, 17 | selected_job: { title: 'NO JOB SELECTED', stops: [ {address: 'SELECT A JOB TO VIEW STOPS', tasks: [{completed: false, GPS: null}]} ] }, 18 | selected_stop: {}, 19 | tasks_shown: 'all' 20 | }; 21 | 22 | export const update_clicked_task = (state, task, GPS) => { 23 | if (task.completed) { 24 | task.completed = false; 25 | task.GPS = null; 26 | task.photo = null; 27 | } else { 28 | task.completed = Date.now(); 29 | task.GPS = GPS; 30 | } 31 | return state; 32 | } 33 | 34 | export const update_task_filter = (state, filter) => { 35 | state.tasks_shown = filter; 36 | return state; 37 | } 38 | 39 | export const add_image_to_task = (state, task, image) => { 40 | task.photo = image; 41 | return state; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/Task.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { taskClick, saveImage } from '../events'; 3 | import '../styles/Task.css'; 4 | 5 | const Task = ({task}) => { 6 | let button; 7 | let photo_button; 8 | let photo_viewer; 9 | let datetime_string = '[Date & time of completion]'; 10 | let class_name; 11 | let button_symbol = '\u2713'; 12 | 13 | function readURL() { 14 | let file = event.srcElement.ownerDocument.activeElement.files[0]; 15 | let reader = new FileReader(); 16 | reader.onloadend = () => { 17 | saveImage(task, reader.result); 18 | } 19 | if (file) { 20 | reader.readAsDataURL(file); 21 | } 22 | } 23 | 24 | if (task.completed) { 25 | class_name = 'completed'; 26 | let date = new Date(task.completed); 27 | datetime_string = `${date.toLocaleTimeString()} on ${date.toLocaleDateString()}`; 28 | button_symbol = 'X'; 29 | } 30 | 31 | if (task.description === 'Receive Bill of Lading') { 32 | button = ; 33 | } else if (task.description === 'Bill of Lading Signed') { 34 | button = ; 35 | } else { 36 | button = ; 37 | photo_button = ; 38 | photo_viewer = [View photograph]; 39 | } 40 | 41 | let location_string = task.GPS ? `Coords(${Math.floor(task.GPS.latitude)}, ${Math.floor(task.GPS.longitude)})` : '[Location on completion]'; 42 | 43 | return ( 44 | 45 |
{task.description} {button}
46 |
{datetime_string}
47 |
{location_string}
48 |
{photo_button}
49 |
{photo_viewer}
50 | 51 | ); 52 | }; 53 | 54 | export default Task; 55 | -------------------------------------------------------------------------------- /src/JobFixtures.js: -------------------------------------------------------------------------------- 1 | export const jobs = [ 2 | { 3 | title: 'Coals to Newcastle', 4 | referenceId: '18615', 5 | stops: [ 6 | { 7 | type: 'PICKUP', 8 | address: '4506 East Avenue, Renton, Wa 98058', 9 | cargoDescription: '6 boxes 10x10x23', 10 | arrivalTime: '2016-01-19T19:14:33.000Z', 11 | tasks: [{description: 'Receive Bill of Lading', completed: false , GPS: null}, {description: 'Photograph Inventory', completed: false , GPS: null}] 12 | }, 13 | { 14 | type: 'DROPOFF', 15 | address: '6352 Sherwood Drive, Seattle, Wa 98121', 16 | arrivalTime: '2016-01-19T22:15:52.000Z', 17 | tasks: [{description: 'Bill of Lading Signed', completed: false , GPS: null}, {description: 'Photograph Inventory', completed: false , GPS: null}] 18 | }, 19 | ], 20 | }, 21 | { 22 | title: 'Twinbrook Creamery To Starbucks', 23 | referenceId: '548482', 24 | stops: [ 25 | { 26 | type: 'PICKUP', 27 | address: '4103 Fulton Street, Renton, Wa 98058', 28 | cargoDescription: '5 pallets', 29 | arrivalTime: '2016-01-20T19:00:00.000Z', 30 | tasks: [{description: 'Receive Bill of Lading', completed: false , GPS: null}, {description: 'Photograph Inventory', completed: false , GPS: null}] 31 | }, 32 | { 33 | type: 'DROPOFF', 34 | address: '7745 Cherry Street, Seattle, Wa 98121', 35 | arrivalTime: '2016-01-20T23:10:00.000Z', 36 | tasks: [{description: 'Bill of Lading Signed', completed: false , GPS: null}, {description: 'Photograph Inventory', completed: false , GPS: null}] 37 | }, 38 | ], 39 | }, 40 | { 41 | title: 'Victrola To Seinheiser', 42 | referenceId: '4D23C6', 43 | stops: [ 44 | { 45 | type: 'PICKUP', 46 | address: '628 Depot Street, Renton, Wa 98058', 47 | cargoDescription: '4 pallets', 48 | arrivalTime: '2016-01-20T21:00:00.000Z', 49 | tasks: [{description: 'Receive Bill of Lading', completed: false , GPS: null}, {description: 'Photograph Inventory', completed: false , GPS: null}] 50 | }, 51 | { 52 | type: 'DROPOFF', 53 | address: '58 Sunset Avenue, Seattle, Wa 98121', 54 | arrivalTime: '2016-01-21T00:10:00.000Z', 55 | tasks: [{description: 'Bill of Lading Signed', completed: false , GPS: null}, {description: 'Photograph Inventory', completed: false , GPS: null}] 56 | }, 57 | ], 58 | }, 59 | { 60 | title: 'Uber To Imprint', 61 | referenceId: 'B12311', 62 | stops: [ 63 | { 64 | type: 'PICKUP', 65 | address: '163 Cambridge Road, Renton, Wa 98058', 66 | cargoDescription: '1 box 12x10x12', 67 | arrivalTime: '2016-01-20T21:00:00.000Z', 68 | tasks: [{description: 'Receive Bill of Lading', completed: false , GPS: null}, {description: 'Photograph Inventory', completed: false , GPS: null}] 69 | }, 70 | { 71 | type: 'DROPOFF', 72 | address: '668 Jackson Avenue, Seattle, Wa 98121', 73 | arrivalTime: '2016-01-21T00:10:00.000Z', 74 | tasks: [{description: 'Bill of Lading Signed', completed: false , GPS: null}, {description: 'Photograph Inventory', completed: false , GPS: null}] 75 | }, 76 | ], 77 | }, 78 | ]; 79 | --------------------------------------------------------------------------------