├── .firebaserc ├── .gitignore ├── api ├── rentals │ └── index.js ├── tools │ └── index.js └── users │ └── index.js ├── assets └── images │ └── find-tools.png ├── auto-jobs └── updateRentalStatuses.js ├── db ├── db.js ├── helpers │ ├── dates.js │ ├── images.js │ ├── rentals.js │ ├── tools.js │ └── users.js ├── migrations │ ├── 20190206193235_images-table.js │ ├── 20190206194223_users-table.js │ ├── 20190206195013_tools-table.js │ ├── 20190206201014_tool-images-table.js │ ├── 20190206201625_reviews-table.js │ ├── 20190420232645_reserved-dates-table.js │ ├── 20190726190137_RentalStatus-table.js │ ├── 20190726202847_Rentals-Table.js │ └── 20190828152623_users-customer-id-column.js └── seeds │ └── 01-RentalStatus.js ├── index.js ├── knexfile.js ├── migrations-temp ├── 20190205223531_create-tools-table.js ├── 20190206181349_create-images-table-v2.js ├── 20190206182421_create.js └── 20190206184931_users-v3.js ├── package-lock.json ├── package.json ├── server.js └── use-my-tools ├── .gitignore ├── README.md ├── netlify.toml ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── App.test.js ├── AppContext.js ├── assets │ └── images │ │ ├── Find-tools-screenshot.png │ │ ├── Man-using-drill.jpeg │ │ ├── Man-using-grinder.jpeg │ │ ├── Tools-on-table-brown.jpeg │ │ └── Wrenches-on-wall.jpeg ├── components │ ├── AccountPage.js │ ├── Billing │ │ ├── Billing.js │ │ └── Checkout.js │ ├── Chat │ │ ├── ChatDashboard.css │ │ ├── ChatDashboard.js │ │ ├── ChatView.css │ │ ├── ChatView.js │ │ └── ConvoList │ │ │ ├── ConvoList.css │ │ │ ├── ConvoList.js │ │ │ └── Convos.js │ ├── Firebase │ │ ├── context.js │ │ ├── firebase.js │ │ └── index.js │ ├── ImageCarousel.js │ ├── LandingPage.js │ ├── LocationSearchInput.js │ ├── LoginPage.js │ ├── LogoutButton.js │ ├── Navigation │ │ ├── MobileNavMenu.js │ │ ├── NavAuth.js │ │ ├── NavNonAuth.js │ │ ├── NavigationBar.js │ │ └── css │ │ │ ├── MobileNavMenu.css │ │ │ ├── NavAuth.css │ │ │ └── NavNonAuth.css │ ├── Owner │ │ ├── AddTool.js │ │ ├── DeleteDialog.js │ │ ├── MyTools.js │ │ ├── OwnerDashboard.js │ │ ├── ToolViewOwner.js │ │ └── css │ │ │ ├── AddTool.css │ │ │ ├── MyTools.css │ │ │ ├── OwnerDashboard.css │ │ │ └── ToolViewOwner.css │ ├── ReactDates │ │ └── DateRangePicker.js │ ├── RegisterPage.js │ ├── Rentals │ │ ├── CancelDialog.js │ │ ├── RentalView.js │ │ ├── RentalsList.js │ │ ├── RentalsView.js │ │ └── css │ │ │ ├── RentalView.css │ │ │ └── RentalsList.css │ ├── Renter │ │ ├── ConfirmRental.js │ │ ├── ConfirmRentalDialog.js │ │ ├── ContactOwner.js │ │ ├── FilterMenu.js │ │ ├── FindTools.js │ │ ├── RenterDashboard.js │ │ ├── ToolViewRenter.js │ │ └── css │ │ │ ├── ConfirmRental.css │ │ │ ├── FilterMenu.css │ │ │ ├── FindTools.css │ │ │ ├── RenterDashboard.css │ │ │ └── ToolViewRenter.css │ ├── RequestTool │ │ ├── RequestDates.js │ │ └── RequestDatesPopUp.js │ ├── Routes │ │ └── PrivateRoute.js │ ├── ToolCard.js │ ├── UpdatePassword.js │ └── css │ │ ├── AccountPage.css │ │ ├── LandingPage.css │ │ ├── LocationSearchInput.css │ │ ├── LoginPage.css │ │ ├── Logout.css │ │ ├── RegisterPage.css │ │ └── UpdatePassword.css ├── index.css ├── index.js ├── logo.svg ├── serviceWorker.js └── store.js └── yarn.lock /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "use-my-tools-csr" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Mac files 61 | .DS_Store 62 | 63 | # VSCode settings 64 | .vscode/ 65 | 66 | # Firebase private keys folder 67 | firebase-private-keys/ -------------------------------------------------------------------------------- /assets/images/find-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/assets/images/find-tools.png -------------------------------------------------------------------------------- /auto-jobs/updateRentalStatuses.js: -------------------------------------------------------------------------------- 1 | const rentalsDb = require('../db/helpers/rentals'); 2 | 3 | module.exports = setInterval(updateRentalStatuses, 1000 * 60 * 1); 4 | 5 | async function updateRentalStatuses() { 6 | // get current date: 7 | const currentDate = new Date(); 8 | 9 | let updates = []; 10 | let update = null; 11 | try { 12 | // Move upcoming rentals to active or completed: 13 | const upcomingRentals = await rentalsDb.getAllRentalsByStatus(['upcoming']); 14 | 15 | for (let rental of upcomingRentals) { 16 | if (currentDate >= rental.StartDate && currentDate <= rental.EndDate) { 17 | // update rental status to active 18 | update = await rentalsDb.updateRentalStatus(rental.RentalID, 'active'); 19 | updates.push(update); 20 | } 21 | if (currentDate > rental.EndDate) { 22 | // update rental status to completed 23 | update = await rentalsDb.updateRentalStatus(rental.RentalID, 'completed'); 24 | updates.push(update); 25 | } 26 | } 27 | 28 | // Move active rentals to completed: 29 | const activeRentals = await rentalsDb.getAllRentalsByStatus(['active']); 30 | 31 | for (let rental of activeRentals) { 32 | if (currentDate > rental.EndDate) { 33 | // update rental status to completed: 34 | update = await rentalsDb.updateRentalStatus(rental.RentalID, 'completed'); 35 | updates.push(update); 36 | } 37 | } 38 | } 39 | catch(error) { 40 | console.log('Error while updating Rental statuses:', error.message); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /db/db.js: -------------------------------------------------------------------------------- 1 | const environment = process.env.ENVIRONMENT || 'development'; 2 | const config = require('../knexfile.js')[environment]; 3 | module.exports = require('knex')(config); 4 | 5 | -------------------------------------------------------------------------------- /db/helpers/dates.js: -------------------------------------------------------------------------------- 1 | const db = require('../db.js'); 2 | 3 | module.exports = { 4 | reserveDates, 5 | getRentalDates, 6 | getOwnerReservedDates, 7 | deleteReservedDates 8 | } 9 | 10 | function reserveDates(dateRange) { 11 | return db('reserved_dates') 12 | .insert(dateRange) 13 | .returning('id') 14 | .then(ids => ids[0]); 15 | } 16 | 17 | function deleteReservedDates(toolId) { 18 | return db('reserved_dates') 19 | .where('tool_id', toolId) 20 | .del(); 21 | } 22 | 23 | function getRentalDates(toolId) { 24 | return db 25 | .select([ 26 | 'Rentals.ReservedDatesID', 27 | 'reserved_dates.start_date as startDate', 28 | 'reserved_dates.end_date as endDate' 29 | ]) 30 | .from('Rentals') 31 | .innerJoin('reserved_dates', 'Rentals.ReservedDatesID', 'reserved_dates.id') 32 | .where('Rentals.ToolID', toolId) 33 | .whereNot('Rentals.Status', 'cancelledByOwner') 34 | .whereNot('Rentals.Status', 'cancelledByRenter'); 35 | } 36 | 37 | function getOwnerReservedDates(toolId) { 38 | return db 39 | .select([ 40 | 'start_date', 41 | 'end_date' 42 | ]) 43 | .from('reserved_dates') 44 | .where('tool_id', toolId) 45 | .where('res_type', 'owner'); 46 | } -------------------------------------------------------------------------------- /db/helpers/images.js: -------------------------------------------------------------------------------- 1 | const db = require('../db.js'); 2 | 3 | module.exports = { 4 | addImage, 5 | addToolImage, 6 | getToolImages, 7 | updateImage, 8 | getFirstToolImage 9 | } 10 | 11 | // add image url to images table: 12 | function addImage(image) { 13 | return db('images') 14 | .insert(image) 15 | .returning('id') 16 | .then(ids => ids[0]); 17 | } 18 | 19 | // add tool_id and image_id pair to tool-images table 20 | function addToolImage(image) { 21 | return db('tool_images') 22 | .insert(image) 23 | .returning('tool_id') 24 | .then(ids => ids[0]); 25 | } 26 | 27 | function getToolImages(id) { 28 | return db 29 | .select('i.url') 30 | .from('tool_images as ti') 31 | .join('images as i', 'ti.image_id', 'i.id') 32 | .where({tool_id: id}); 33 | } 34 | 35 | function getFirstToolImage(toolId) { 36 | return db 37 | .select('i.url') 38 | .from('tool_images as ti') 39 | .join('images as i', 'ti.image_id', 'i.id') 40 | .where({tool_id: toolId}) 41 | .first(); 42 | } 43 | 44 | function updateImage(id, url) { 45 | return db('images') 46 | .where({ id: Number(id) }) 47 | .update({ url: url }); 48 | } 49 | -------------------------------------------------------------------------------- /db/helpers/tools.js: -------------------------------------------------------------------------------- 1 | const db = require('../db.js'); 2 | 3 | module.exports = { 4 | createTool, 5 | getMyTools, 6 | getAllTools, 7 | getTool, 8 | getMyTool, 9 | deleteToolImages, 10 | deleteTool, 11 | updateToolDetails, 12 | findTools, 13 | getToolDataForRental, 14 | getToolRatings 15 | } 16 | 17 | function createTool(newTool) { 18 | return db('tools') 19 | .insert(newTool) 20 | .returning('id') 21 | .then(ids => ids[0]); 22 | } 23 | 24 | // function getMyTools(uid) { 25 | // return db 26 | // .select('*') 27 | // .from('tools') 28 | // .innerJoin('tool_images', 'tools.id', 'tool_images.tool_id') 29 | // .innerJoin('images', 'tool_images.image_id' ,'images.id') 30 | // .where('tools.owner_uid', uid); 31 | // } 32 | 33 | function getMyTools(uid) { 34 | return db 35 | .select('*') 36 | .from('tools') 37 | .where('tools.owner_uid', uid); 38 | } 39 | 40 | function getAllTools() { 41 | return db 42 | .select([ 43 | 'tools.id', 44 | 'tools.brand', 45 | 'tools.name', 46 | 'tools.description', 47 | 'tools.price', 48 | 'tools.available', 49 | 'tools.rented', 50 | 'tools.rating', 51 | 'users.first_name as ownerFirstName', 52 | 'users.full_address', 53 | 'users.city', 54 | 'users.state' 55 | ]) 56 | .from('tools') 57 | .leftJoin('users', 'tools.owner_uid', 'users.uid'); 58 | } 59 | 60 | function findTools(city) { 61 | return db 62 | .select([ 63 | 'tools.id', 64 | 'tools.brand', 65 | 'tools.name', 66 | 'tools.description', 67 | 'tools.price', 68 | 'tools.available', 69 | 'tools.rented', 70 | 'tools.rating', 71 | 'users.first_name as ownerFirstName', 72 | 'users.full_address', 73 | 'users.city', 74 | 'users.state' 75 | ]) 76 | .from('tools') 77 | .leftJoin('users', 'tools.owner_uid', 'users.uid') 78 | .where('tools.available', 'true') 79 | .where('users.city', city); 80 | } 81 | 82 | // get tool info for a renter to view: 83 | function getTool(id) { 84 | return db 85 | // .select('*') 86 | // .from('tools') 87 | // .where('id', id) 88 | // .first(); 89 | .select([ 90 | 'tools.id', 91 | 'tools.owner_uid as ownerUid', 92 | 'tools.brand', 93 | 'tools.name', 94 | 'tools.description', 95 | 'tools.price', 96 | 'tools.available', 97 | 'tools.rented', 98 | 'tools.rating', 99 | 'users.first_name as ownerFirstName', 100 | 'users.last_name as ownerLastName', 101 | 'users.city as ownerCity', 102 | 'users.state as ownerState' 103 | ]) 104 | .from('tools') 105 | .leftJoin('users', 'tools.owner_uid', 'users.uid') 106 | .where('tools.id', id) 107 | .first(); 108 | } 109 | 110 | // get tool info for an owner to view: 111 | function getMyTool(id) { 112 | return db 113 | .select([ 114 | 'tools.id', 115 | 'tools.renter_uid as renterUid', 116 | 'tools.brand', 117 | 'tools.name', 118 | 'tools.description', 119 | 'tools.price', 120 | 'tools.available', 121 | 'tools.rented', 122 | 'tools.rating', 123 | 'users.first_name as renterFirstName', 124 | 'users.last_name as renterLastName', 125 | 'users.full_address as renterAddress' 126 | ]) 127 | .from('tools') 128 | .leftJoin('users', 'tools.renter_uid', 'users.uid') 129 | .where('tools.id', id) 130 | .first(); 131 | } 132 | 133 | // get ratingFromRenter from every rental matching tool id input: 134 | function getToolRatings(toolId) { 135 | return db 136 | .select('Rentals.ratingFromRenter') 137 | .from('Rentals') 138 | .where('Rentals.ToolID', toolId) 139 | .whereNotNull('Rentals.ratingFromRenter'); 140 | } 141 | 142 | 143 | function deleteToolImages(id) { 144 | return db('tool_images') 145 | .where('tool_id', id) 146 | .del(); 147 | } 148 | 149 | function deleteTool(id) { 150 | return db('tools') 151 | .where('id', id) 152 | .del(); 153 | } 154 | 155 | function updateToolDetails(id, tool) { 156 | return db('tools') 157 | .where('id', id) 158 | .update(tool); 159 | } 160 | 161 | function getToolDataForRental(toolID) { 162 | return db 163 | .select([ 164 | 'owner_uid', 165 | 'price' 166 | ]) 167 | .from('tools') 168 | .where('tools.id', toolID) 169 | .first(); 170 | } -------------------------------------------------------------------------------- /db/helpers/users.js: -------------------------------------------------------------------------------- 1 | const db = require('../db.js'); 2 | 3 | module.exports = { 4 | createUser, 5 | getUserInfo, 6 | updateUserDetails, 7 | getUserLocation, 8 | getUserName, 9 | getUserEmail, 10 | } 11 | 12 | function createUser(newUser) { 13 | return db('users') 14 | .insert(newUser) 15 | .returning('id') 16 | .then(ids => ids[0]); 17 | } 18 | 19 | function getUserInfo(uid) { 20 | return db 21 | .select([ 22 | 'users.uid', 23 | 'users.first_name', 24 | 'users.last_name', 25 | 'users.email', 26 | 'users.full_address', 27 | 'users.image_id', 28 | 'images.url as image_url' 29 | ]) 30 | .from('users') 31 | .innerJoin('images', 'users.image_id', 'images.id') 32 | .where('users.uid', uid) 33 | .first(); 34 | } 35 | 36 | function getUserName(uid) { 37 | return db 38 | .select([ 39 | 'users.first_name', 40 | 'users.last_name' 41 | ]) 42 | .from('users') 43 | .where('users.uid', uid) 44 | .first(); 45 | // .then(users => { 46 | // return users[0]; 47 | // }); 48 | } 49 | 50 | function getUserLocation(uid) { 51 | return db 52 | .select([ 53 | 'users.full_address', 54 | 'users.street_number', 55 | 'users.street_name', 56 | 'users.city', 57 | 'users.county', 58 | 'users.state', 59 | 'users.country', 60 | 'users.zip_code', 61 | 'users.lat', 62 | 'users.lng', 63 | 'users.place_id' 64 | ]) 65 | .from('users') 66 | .where('users.uid', uid) 67 | .first(); 68 | // .then(locations => { 69 | // return locations[0]; 70 | // }); 71 | } 72 | 73 | function updateUserDetails(uid, user) { 74 | return db('users') 75 | .where('uid', uid) 76 | .update(user); 77 | } 78 | 79 | function getUserEmail(uid) { 80 | return db 81 | .select([ 82 | 'users.email', 83 | ]) 84 | .from('users') 85 | .where('users.uid', uid) 86 | .first(); 87 | // .then(users => { 88 | // return users[0]; 89 | // }); 90 | } -------------------------------------------------------------------------------- /db/migrations/20190206193235_images-table.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable('images', table => { 5 | table.increments('id').primary(); // unique id of the image 6 | 7 | table.string('url').notNullable(); // url of the image from cloudinary 8 | }) 9 | ]) 10 | }; 11 | 12 | exports.down = function(knex, Promise) { 13 | return knex.schema.dropTableIfExists('images'); 14 | }; 15 | -------------------------------------------------------------------------------- /db/migrations/20190206194223_users-table.js: -------------------------------------------------------------------------------- 1 | exports.up = function(knex, Promise) { 2 | return Promise.all([ 3 | knex.schema.createTable('users', table => { 4 | table.increments('id').primary(); 5 | table.string('uid') // Firebase uid 6 | .unique() 7 | .notNullable(); 8 | table.string('email') 9 | .unique() 10 | .notNullable(); 11 | table.string('first_name').notNullable(); 12 | table.string('last_name').notNullable(); 13 | table.string('full_address'); 14 | table.string('street_number'); 15 | table.string('street_name'); 16 | table.string('city'); 17 | table.string('county'); 18 | table.string('state'); 19 | table.string('country'); 20 | table.string('zip_code'); 21 | table.string('zip_code_ext'); 22 | table.double('lat'); 23 | table.double('lng'); 24 | table.string('place_id'); 25 | table.integer('image_id') 26 | .notNullable() 27 | .references('id') 28 | .inTable('images'); 29 | }) 30 | ]) 31 | }; 32 | 33 | exports.down = function(knex, Promise) { 34 | return Promise.all([ 35 | knex.schema.dropTableIfExists('users') 36 | ]) 37 | }; -------------------------------------------------------------------------------- /db/migrations/20190206195013_tools-table.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable('tools', table => { 5 | table.increments('id').primary(); // id of the tool 6 | table.string('owner_uid') // id of the tool's owner 7 | .references('uid') // reference's owner's uid 8 | .inTable('users'); 9 | table.string('renter_uid'); // id of the tool's current renter; a tool might be rented or it might be available 10 | // .references('id') // references the renter's uid 11 | // .inTable('users'); 12 | table.string('brand'); 13 | table.string('name').notNullable(); 14 | table.string('description').notNullable(); 15 | table.double('price') 16 | .notNullable() 17 | .defaultTo(0.0); // daily price to rent the tool 18 | table.boolean('available') 19 | .defaultTo(false); 20 | table.boolean('rented') 21 | .defaultTo(false); 22 | table.double('rating'); 23 | }) 24 | ]) 25 | }; 26 | 27 | exports.down = function(knex, Promise) { 28 | return Promise.all([ 29 | knex.schema.dropTableIfExists('tools') 30 | ]) 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /db/migrations/20190206201014_tool-images-table.js: -------------------------------------------------------------------------------- 1 | // 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable('tool_images', table => { 5 | table.integer('tool_id') 6 | .notNullable() 7 | .references('id') 8 | .inTable('tools'); 9 | table.integer('image_id') 10 | .notNullable() 11 | .references('id') 12 | .inTable('images'); 13 | 14 | table.primary(['tool_id', 'image_id']); // primary key is a composite of tool_id and image_id 15 | // syntax: table.primary(columns, [constraintName]) where constraint name defaults to `tablename_pkey` 16 | }) 17 | ]) 18 | 19 | }; 20 | 21 | exports.down = function(knex, Promise) { 22 | return Promise.all([ 23 | knex.schema.dropTableIfExists('tool_images') 24 | ]) 25 | }; 26 | -------------------------------------------------------------------------------- /db/migrations/20190206201625_reviews-table.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable('reviews', table => { 5 | table.increments('id').primary(); 6 | table.integer('reviewer_id') 7 | .references('id') 8 | .inTable('users'); 9 | table.integer('reviewed_id') 10 | .references('id') 11 | .inTable('users'); 12 | table.integer('star_rating').notNullable(); 13 | table.string('content'); 14 | }) 15 | ]) 16 | }; 17 | 18 | exports.down = function(knex, Promise) { 19 | return Promise.all([ 20 | knex.schema.dropTableIfExists('reviews') 21 | ]) 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /db/migrations/20190420232645_reserved-dates-table.js: -------------------------------------------------------------------------------- 1 | exports.up = function(knex, Promise) { 2 | return Promise.all([ 3 | knex.schema.createTable('reserved_dates', table => { 4 | table.increments('id').primary(); 5 | table.integer('tool_id') 6 | .references('id') 7 | .inTable('tools'); 8 | table.string('res_type') // res_type will be either 'rental' or 'owner_block' 9 | .notNullable(); 10 | table.string('renter_uid'); // if reservation is for a rental (not an owner block) there needs to be a renter_uid 11 | table.date('start_date') 12 | .notNullable(); 13 | table.date('end_date') 14 | .notNullable(); 15 | }) 16 | ]) 17 | }; 18 | 19 | exports.down = function(knex, Promise) { 20 | return Promise.all([ 21 | knex.schema.dropTableIfExists('reserved_dates') 22 | ]) 23 | }; -------------------------------------------------------------------------------- /db/migrations/20190726190137_RentalStatus-table.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable('RentalStatus', table => { 5 | table.increments('RentalStatusID').primary(); 6 | table.string('Status') 7 | .unique() 8 | .notNullable(); 9 | }) 10 | ]) 11 | }; 12 | 13 | exports.down = function(knex, Promise) { 14 | return Promise.all([ 15 | knex.schema.dropTableIfExists('RentalStatus') 16 | ]) 17 | }; 18 | -------------------------------------------------------------------------------- /db/migrations/20190726202847_Rentals-Table.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable('Rentals', table => { 5 | table.increments('RentalID').primary(); 6 | table.string('RenterUID') // FK ref to tool renter's uid 7 | .references('uid') 8 | .inTable('users'); 9 | table.string('OwnerUID') // FK ref to the tool owner's uid; potentially redundant but useful for easy querying 10 | .references('uid') 11 | .inTable('users'); 12 | table.integer('ToolID') // FK ref to the tool being rented 13 | .references('id') 14 | .inTable('tools'); 15 | table.integer('ReservedDatesID') // FK ref to entry in reserved_dates table (which includes start_date and end_date) 16 | .references('id') 17 | .inTable('reserved_dates'); 18 | table.string('Status') 19 | .references('Status') 20 | .inTable('RentalStatus'); 21 | table.double('DailyRentalPrice') // Price when rental is created. No FK ref to tool price b/c owner could change price after rental is created 22 | .notNullable(); 23 | table.date('CreateDate') // Date when rental is created 24 | .notNullable(); 25 | table.date('CancelDate'); // Date when rental is cancelled *if* rental is cancelled 26 | table.integer('ratingFromRenter'); // 1-5 star rating 27 | table.string('reviewFromRenter'); 28 | table.integer('ratingFromOwner'); // 1-5 star rating 29 | table.string('reviewFromOwner'); 30 | }) 31 | ]) 32 | }; 33 | 34 | exports.down = function(knex, Promise) { 35 | return Promise.all([ 36 | knex.schema.dropTableIfExists('Rentals') 37 | ]) 38 | }; 39 | -------------------------------------------------------------------------------- /db/migrations/20190828152623_users-customer-id-column.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.table('users', table => { 5 | table.string('stripe_customer_id'); 6 | }) 7 | ]); 8 | }; 9 | 10 | exports.down = function(knex, Promise) { 11 | return Promise.all([ 12 | knex.schema.table('users', table => { 13 | table.dropColumn('stripe_customer_id'); 14 | }) 15 | ]); 16 | }; 17 | -------------------------------------------------------------------------------- /db/seeds/01-RentalStatus.js: -------------------------------------------------------------------------------- 1 | 2 | exports.seed = function(knex, Promise) { 3 | // Deletes ALL existing entries 4 | 5 | // Inserts seed entries 6 | const query = knex('RentalStatus').insert([ 7 | {RentalStatusID: 1, Status: 'upcoming'}, 8 | {RentalStatusID: 2, Status: 'active'}, 9 | {RentalStatusID: 3, Status: 'completed'}, 10 | {RentalStatusID: 4, Status: 'cancelledByOwner'}, 11 | {RentalStatusID: 5, Status: 'cancelledByRenter'}, 12 | {RentalStatusID: 6, Status: 'pendingPayment'}, 13 | {RentalStatusID: 7, Status: 'cancelledByAdmin'} 14 | ]); 15 | return knex.raw(`? ON CONFLICT DO NOTHING`, [query]).then(() => true); 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const server = require('./server.js'); 2 | 3 | const PORT = process.env.PORT || 5000; // in development server will be hosted locally at localhost:5000 4 | 5 | server.listen(PORT, () => { 6 | console.log(`Server is listening on port: ${PORT}`); 7 | }); 8 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | // Database config settings 2 | 3 | const dbConnection = process.env.DATABASE_URL || 'development'; 4 | 5 | module.exports = { 6 | development: { 7 | client: 'pg', 8 | connection: { 9 | host: 'localhost', 10 | user: 'camerontools', 11 | password: 'toolshed', 12 | database: 'usemytoolsdev', 13 | charset: 'utf8' 14 | }, 15 | migrations: { 16 | directory: './db/migrations' 17 | }, 18 | seeds: { 19 | directory: './db/seeds' 20 | }, 21 | useNullAsDefault: true 22 | }, 23 | 24 | // test: { 25 | // client: 'pg', 26 | // connection:'postgres://localhost/', 27 | // migrations: { 28 | // directory: './db/migrations' 29 | // }, 30 | // seeds: { 31 | // directory: './db/seeds/test' 32 | // }, 33 | // useNullAsDefault: true 34 | // }, 35 | 36 | production: { 37 | client: 'pg', 38 | connection: dbConnection, 39 | pool: { 40 | min: 2, 41 | max: 10 42 | }, 43 | migrations: { 44 | tableName: 'knex_migrations', 45 | directory: './db/migrations', 46 | }, 47 | seeds: { directory: './db/seeds' } 48 | // connection: process.env.DATABASE_URL, 49 | // migrations: { 50 | // directory: './db/migrations' 51 | // }, 52 | // seeds: { 53 | // directory: './db/seeds/production' 54 | // }, 55 | // useNullAsDefault: true 56 | } 57 | }; -------------------------------------------------------------------------------- /migrations-temp/20190205223531_create-tools-table.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable('tools', table => { 5 | table.increments('id').primary(); // id of the tool 6 | table.integer('owner_id') // id of the tool's owner 7 | .notNullable() // every tool has an owner 8 | .references('id') // reference's owner's uid 9 | .inTable('users'); 10 | table.integer('renter_id') // id of the tool's current renter; a tool might be rented or it might be available 11 | .references('id') // references the renter's uid 12 | .inTable('users'); 13 | table.string('brand'); 14 | table.string('name').notNullable(); 15 | table.string('description').notNullable(); 16 | table.double('price') 17 | .notNullable() 18 | .defaultTo(0.0); // daily price to rent the tool 19 | table.string('home_street_address'); // home address of the owner 20 | table.string('current_street_address'); // adress of current renter 21 | table.double('home_lat'); 22 | table.double('home_lon'); 23 | table.double('current_lat'); 24 | table.double('current_lon'); 25 | table.boolean('available') 26 | .notNullable() 27 | .defaultTo(false); 28 | table.double('rating'); 29 | table.double('owner_rating'); 30 | }) 31 | ]) 32 | }; 33 | // about to run create tools table migration 34 | exports.down = function(knex, Promise) { 35 | return Promise.all([ 36 | knex.schema.dropTableIfEsists('tools') 37 | ]) 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /migrations-temp/20190206181349_create-images-table-v2.js: -------------------------------------------------------------------------------- 1 | 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable('images', table => { 5 | table.increments('id').primary(); // unique id of the image 6 | 7 | table.string('url').notNullable(); // url of the image from cloudinary 8 | }) 9 | ]) 10 | }; 11 | 12 | exports.down = function(knex, Promise) { 13 | return knex.schema.dropTableIfExists('images'); 14 | }; 15 | -------------------------------------------------------------------------------- /migrations-temp/20190206182421_create.js: -------------------------------------------------------------------------------- 1 | // 2 | exports.up = function(knex, Promise) { 3 | return Promise.all([ 4 | knex.schema.createTable('tool_images', table => { 5 | table.integer('tool_id') 6 | .notNullable() 7 | .references('id') 8 | .inTable('tools'); 9 | table.integer('image_id') 10 | .notNullable() 11 | .references('id') 12 | .inTable('images'); 13 | 14 | table.primary(['tool_id', 'image_id']); // primary key is a composite of tool_id and image_id 15 | // syntax: table.primary(columns, [constraintName]) where constraint name defaults to `tablename_pkey` 16 | }) 17 | ]) 18 | 19 | }; 20 | 21 | exports.down = function(knex, Promise) { 22 | return Promise.all([ 23 | knex.schema.dropTableIfExists('tool_images') 24 | ]) 25 | }; 26 | -------------------------------------------------------------------------------- /migrations-temp/20190206184931_users-v3.js: -------------------------------------------------------------------------------- 1 | exports.up = function(knex, Promise) { 2 | return Promise.all([ 3 | knex.schema.createTable('users', table => { 4 | table.increments('id').primary(); 5 | table.string('username') 6 | .unique() 7 | .notNullable(); 8 | table.string('password').notNullable(); 9 | table.string('email') 10 | .unique() 11 | .notNullable(); 12 | table.string('firstname').notNullable(); 13 | table.string('lastname').notNullable(); 14 | table.string('home_street_address'); 15 | table.double('home_lat'); 16 | table.double('home_lon'); 17 | table.integer('image_id') 18 | .notNullable() 19 | .references('id') 20 | .inTable('images'); 21 | }) 22 | ]) 23 | }; 24 | 25 | exports.down = function(knex, Promise) { 26 | return Promise.all([ 27 | knex.schema.dropTableIfEsists('users') 28 | ]) 29 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-my-tools-csr", 3 | "version": "1.0.0", 4 | "description": "Build Week 1 personal project", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "nodemon ./index.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/upsmancsr/Use-My-Tools-CSR.git" 14 | }, 15 | "author": "Cameron Ray", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/upsmancsr/Use-My-Tools-CSR/issues" 19 | }, 20 | "homepage": "https://github.com/upsmancsr/Use-My-Tools-CSR#readme", 21 | "dependencies": { 22 | "bcrypt": "^3.0.4", 23 | "cloudinary": "^1.14.0", 24 | "connect-multiparty": "^2.2.0", 25 | "cors": "^2.8.5", 26 | "express": "^4.16.4", 27 | "firebase": "^5.9.2", 28 | "firebase-admin": "^7.2.0", 29 | "helmet": "^3.15.0", 30 | "jsonwebtoken": "^8.4.0", 31 | "knex": "^0.16.3", 32 | "morgan": "^1.9.1", 33 | "pg": "^7.8.0", 34 | "react": "^16.8.5", 35 | "stripe": "^7.8.0" 36 | }, 37 | "devDependencies": { 38 | "dotenv": "^6.2.0", 39 | "nodemon": "^1.19.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const helmet = require('helmet'); 4 | const morgan = require('morgan'); 5 | const multipart = require("connect-multiparty")(); 6 | 7 | // Import .env config vars for dev Environment 8 | if (process.env.ENVIRONMENT === 'development') { 9 | require('dotenv').config(); 10 | } 11 | if(process.env.NODE_ENV !== 'production'){ 12 | require('dotenv').load(); 13 | } 14 | 15 | // import auto jobs that run automatically on an interval: 16 | require('./auto-jobs/updateRentalStatuses'); 17 | 18 | // Firebsae imports: 19 | const firebase = require("firebase/app"); 20 | require("firebase/auth"); 21 | require("firebase/database"); 22 | const admin = require('firebase-admin'); 23 | 24 | admin.initializeApp({ 25 | credential: admin.credential.cert({ 26 | projectId: process.env.FIREBASE_PROJECT_ID, 27 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL, 28 | privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), 29 | }), 30 | databaseURL: process.env.FIREBASE_DB_URL 31 | }); 32 | 33 | 34 | const firebaseConfig = { 35 | apiKey: process.env.FIREBASE_API_KEY, 36 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 37 | databaseURL: process.env.FIREBASE_DB_URL, 38 | projectId: process.env.FIREBASE_PROJECT_ID, 39 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET, 40 | messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, 41 | }; 42 | 43 | firebase.initializeApp(firebaseConfig); 44 | 45 | // Create Express server 46 | const server = express(); 47 | 48 | server.use(express.json()); 49 | server.use(morgan('dev')); 50 | server.use(cors()); 51 | server.use(helmet()); 52 | 53 | // Sanity check to see if base URL is live: 54 | server.get('/',(req, res) => { 55 | res.send("Server base URL is working..."); 56 | }); 57 | 58 | // Import API route/endpoint files: 59 | const usersRoutes = require('./api/users'); // All CRUD endpoints for user-specific data 60 | const toolsRoutes = require('./api/tools'); // All CRUD endpoints for tool-specific data 61 | const rentalsRoutes = require('./api/rentals'); // All CRUD endpoints for rental-specific data 62 | 63 | // Verify requests using Firebase-admin auth: 64 | server.use(multipart, async(req,res) => { 65 | console.log('server auth hit with req.body: ', req.body); 66 | const idToken = req.headers.authorization; 67 | console.log(idToken); 68 | try { 69 | await admin.auth().verifyIdToken(idToken) // verify the idToken of the incoming req 70 | .then(decodedToken => { // get the decoded token back from Firebase 71 | 72 | req.body.uid = decodedToken.uid; // add the uid from the decoded token to req.body 73 | return req.next(); // return and move to the next part of the original req 74 | }); 75 | } 76 | catch(error) { 77 | res.status(401).json({message: error.message}); 78 | } 79 | }) 80 | 81 | // Connect API base routes to corresponding api folder: 82 | server.use('/api/users', usersRoutes); 83 | server.use('/api/tools', toolsRoutes); 84 | server.use('/api/rentals', rentalsRoutes) 85 | 86 | module.exports = server; -------------------------------------------------------------------------------- /use-my-tools/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /use-my-tools/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /use-my-tools/netlify.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/use-my-tools/netlify.toml -------------------------------------------------------------------------------- /use-my-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-my-tools", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^3.9.3", 7 | "@material-ui/icons": "^3.0.2", 8 | "axios": "^0.19.0", 9 | "firebase": "^5.9.2", 10 | "lodash": "^4.17.11", 11 | "material-ui": "^0.20.2", 12 | "moment": "^2.24.0", 13 | "prop-types": "^15.7.2", 14 | "react": "^16.8.6", 15 | "react-dates": "^20.1.0", 16 | "react-dom": "^16.8.6", 17 | "react-moment-proptypes": "^1.6.0", 18 | "react-places-autocomplete": "^7.2.1", 19 | "react-router-dom": "^5.0.1", 20 | "react-scripts": "2.1.8", 21 | "react-stripe-checkout": "^2.6.3", 22 | "react-with-direction": "^1.3.0", 23 | "react-with-styles": "^3.2.1", 24 | "styled-components": "^4.3.2" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": [ 36 | ">0.2%", 37 | "not dead", 38 | "not ie <= 11", 39 | "not op_mini all" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /use-my-tools/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /use-my-tools/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/use-my-tools/public/favicon.ico -------------------------------------------------------------------------------- /use-my-tools/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 16 | 17 | 26 | React App 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /use-my-tools/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /use-my-tools/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .pageContainer { 6 | max-width: 950px; 7 | margin-left: auto; 8 | margin-right: auto; 9 | } 10 | -------------------------------------------------------------------------------- /use-my-tools/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | import { withFirebase } from "./components/Firebase"; 4 | import { FirebaseContext } from './components/Firebase'; 5 | 6 | import PrivateRoute from './components/Routes/PrivateRoute'; 7 | // import { Provider, Consumer } from './AppContext'; 8 | import axios from 'axios'; 9 | 10 | import './App.css'; 11 | 12 | import LandingPage from './components/LandingPage'; 13 | import RegisterPage from './components/RegisterPage'; 14 | import LoginPage from './components/LoginPage'; 15 | import NavigationBar from './components/Navigation/NavigationBar'; 16 | import AccountPage from './components/AccountPage'; 17 | // import Billing from './components/Billing/Billing'; 18 | import ConfirmRental from './components/Renter/ConfirmRental'; 19 | import OwnerDashboard from './components/Owner/OwnerDashboard'; 20 | import RenterDashboard from './components/Renter/RenterDashboard'; 21 | import RentalView from './components/Rentals/RentalView'; 22 | import AddTool from './components/Owner/AddTool'; 23 | import FindTools from './components/Renter/FindTools'; 24 | import ToolViewRenter from './components/Renter/ToolViewRenter'; 25 | import ToolViewOwner from './components/Owner/ToolViewOwner'; 26 | import ChatDashboard from './components/Chat/ChatDashboard'; 27 | import DateRangePickerWrapper from './components/ReactDates/DateRangePicker'; 28 | import UpdatePassword from './components/UpdatePassword'; 29 | 30 | const App = () => ( 31 |
32 | 33 | {firebase => } 34 | 35 |
36 | ); 37 | 38 | class AppComponentBase extends Component { 39 | constructor(props) { 40 | super(props); 41 | this.state = { 42 | authenticated: false, 43 | authUser: null, 44 | idToken: null, 45 | loading: true 46 | } 47 | } 48 | 49 | componentDidMount() { 50 | this.listener = this.props.firebase.auth.onAuthStateChanged(authUser => { 51 | // console.log('App CDM onAuthStateChange triggered'); 52 | if (authUser) { 53 | // console.log('onAuthStateChange authUser:' + authUser); 54 | this.props.firebase.auth.currentUser.getIdToken() 55 | .then(idToken => { 56 | axios.defaults.headers.common['Authorization'] = idToken; 57 | this.setState({ 58 | authenticated: true, 59 | authUser, 60 | loading: false 61 | }); 62 | }) 63 | .catch(error => { 64 | console.log(error.message);; 65 | }) 66 | } else { 67 | this.setState({ 68 | authenticated: false, 69 | authUser: null, 70 | loading: false 71 | }); 72 | } 73 | }); 74 | } 75 | 76 | componentWillUnmount() { 77 | this.listener(); 78 | } 79 | 80 | render() { 81 | const { authUser } = this.state; 82 | const { authenticated, loading } = this.state; 83 | 84 | if (loading) { 85 | return

Loading....

; 86 | } 87 | 88 | return ( 89 | // 90 | 91 |
92 | 93 | {!loading ? ( 94 | //
95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | //
113 | ) :

Loading...

114 | } 115 |
116 |
117 | 118 | //
119 | ); 120 | } 121 | } 122 | 123 | const AppComponent = withFirebase(AppComponentBase); 124 | 125 | export default App; 126 | 127 | export {AppComponent}; -------------------------------------------------------------------------------- /use-my-tools/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 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /use-my-tools/src/AppContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const { Provider, Consumer } = React.createContext(); 4 | 5 | export { Provider, Consumer }; -------------------------------------------------------------------------------- /use-my-tools/src/assets/images/Find-tools-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/use-my-tools/src/assets/images/Find-tools-screenshot.png -------------------------------------------------------------------------------- /use-my-tools/src/assets/images/Man-using-drill.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/use-my-tools/src/assets/images/Man-using-drill.jpeg -------------------------------------------------------------------------------- /use-my-tools/src/assets/images/Man-using-grinder.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/use-my-tools/src/assets/images/Man-using-grinder.jpeg -------------------------------------------------------------------------------- /use-my-tools/src/assets/images/Tools-on-table-brown.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/use-my-tools/src/assets/images/Tools-on-table-brown.jpeg -------------------------------------------------------------------------------- /use-my-tools/src/assets/images/Wrenches-on-wall.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/use-my-tools/src/assets/images/Wrenches-on-wall.jpeg -------------------------------------------------------------------------------- /use-my-tools/src/components/Billing/Billing.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { withFirebase } from "../Firebase"; 3 | import { withRouter } from "react-router-dom" 4 | 5 | import StripeCheckout from 'react-stripe-checkout'; 6 | import { withStyles } from "@material-ui/core/styles"; 7 | import styled from 'styled-components' 8 | 9 | import PropTypes from "prop-types"; 10 | 11 | import axios from 'axios' 12 | 13 | import { 14 | Grid, 15 | Card, 16 | CardContent, 17 | CardHeader, 18 | Typography, 19 | } from '@material-ui/core' 20 | 21 | const BillingContainer = styled.div` 22 | 23 | ` 24 | const PaymentContainer = styled.div` 25 | margin: 100px auto 100px auto; 26 | 27 | max-width: 800px; 28 | /* width: 70%; */ 29 | display: flex; 30 | flex-direction: column; 31 | 32 | h1 { 33 | margin: 0 auto; 34 | margin-bottom: 60px; 35 | color: white; 36 | } 37 | .title-thin { 38 | display: none; 39 | } 40 | @media (max-width: 600px) { 41 | margin: 50px auto; 42 | 43 | .title-thin { 44 | display: block; 45 | margin: 0 auto; 46 | 47 | &:last-of-type { 48 | margin-bottom: 40px; 49 | } 50 | } 51 | .title-wide { 52 | display: none; 53 | } 54 | } 55 | 56 | .card { 57 | box-shadow: 0 0 5px 5px rgb(230, 230, 230); 58 | border-radius: 0; 59 | /* background: linear-gradient(to right, rgb(82, 157, 248), rgb(66, 126, 199)); */ 60 | } 61 | 62 | .MuiCardHeader-root-262 { 63 | padding-bottom: 0; 64 | } 65 | ` 66 | 67 | const PaymentButton = styled.div` 68 | margin: auto; 69 | margin-top 5%; 70 | width: 40%; 71 | display: flex; 72 | flex-direction: column; 73 | ` 74 | 75 | const styles = theme => ({ 76 | cardPricing: { 77 | display: 'flex', 78 | justifyContent: 'center', 79 | marginBottom: theme.spacing.unit * 2, 80 | }, 81 | cardActions: { 82 | [theme.breakpoints.up('sm')]: { 83 | paddingBottom: theme.spacing.unit * 2, 84 | }, 85 | }, 86 | }) 87 | 88 | class BillingBase extends React.Component { 89 | constructor(props) { 90 | super(props); 91 | this.state = { 92 | isSubscribed: false, 93 | plan: null, 94 | subStatus: null 95 | } 96 | } 97 | 98 | componentDidMount() { 99 | 100 | this.props.firebase.auth.onAuthStateChanged(user => { 101 | if (user) { 102 | 103 | this.props.firebase.auth.currentUser.getIdToken() 104 | .then(idToken => { 105 | 106 | console.log("idToken after in Admin panel: ", idToken); 107 | axios.defaults.headers.common['Authorization'] = idToken; 108 | 109 | const id = this.props.company_id; 110 | axios.get(`/api/billing/getSub`) 111 | .then(response => { 112 | console.log('response from Billing getSub: ', response); 113 | if (response.data) { // if max_reps on subscription is greater than current team size 114 | this.setState({ 115 | isSubscribed: true, 116 | plan: response.data.stripe_plan_nickname, 117 | subStatus: response.data.stripe_subscription_status 118 | }, () => console.log('Billing state after getSub: ', this.state)); 119 | } else { 120 | this.setState({ 121 | isSubscribed: false, 122 | plan: 'Free', 123 | subStatus: 'active' 124 | }, () => console.log('Billing state after getSub: ', this.state)); 125 | } 126 | }) 127 | .catch(error => { 128 | this.setState({ error: error.message }); 129 | }) 130 | }) 131 | .catch(error => { // if Firebase getIdToken throws an error 132 | console.log(error.message); 133 | //this.setState({ error:error }); 134 | }) 135 | } 136 | else { 137 | this.props.history.push('/repslogin'); 138 | } 139 | }); 140 | }; 141 | 142 | 143 | // basicToken = token => { 144 | // let bodyToSend = { 145 | // ...token, 146 | // subscription: { 147 | // plan: 'plan_EXEVzE4nraOpql', // corresponds to Basic Monthly plan ID from Stripe Dashboard 148 | // }, 149 | // } 150 | // this.addSubscription(bodyToSend) 151 | // } 152 | 153 | render() { 154 | 155 | return ( 156 |
157 | 167 |
168 | ) 169 | } 170 | } 171 | 172 | BillingBase.propTypes = { 173 | classes: PropTypes.object.isRequired 174 | }; 175 | 176 | const Billing = withStyles(styles)(withRouter(withFirebase(BillingBase))); 177 | 178 | export default Billing; 179 | 180 | {/* 181 | 182 |

Our Subscription Options

183 |

Our

184 |

Subscription

185 |

Options

186 | 187 | 188 | 194 | 195 | 196 | 197 | 207 | 208 | 209 | 210 |
211 |
*/} -------------------------------------------------------------------------------- /use-my-tools/src/components/Billing/Checkout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios'; 3 | import StripeCheckout from 'react-stripe-checkout'; 4 | 5 | const STRIPE_PUBLISHABLE_KEY = 'pk_test_DOERzwvaYYRIUJAJbesVuSJ300Edj6qqZ0' 6 | const CURRENCY = 'USD'; 7 | 8 | // const successPayment = data => { 9 | // alert('Payment Successful'); 10 | // console.log(data); 11 | // }; 12 | 13 | const errorPayment = data => { 14 | alert('Payment Error. Please enter valid payment information.'); 15 | console.log(data); 16 | }; 17 | 18 | const onToken = (amount, description, rentalId, goToRentalView) => token => { 19 | axios.post('/api/rentals/rentalpayment', 20 | { 21 | description, 22 | source: token.id, 23 | currency: CURRENCY, 24 | amount: amount, 25 | rentalId: rentalId 26 | }) 27 | .then(response => { 28 | goToRentalView(); 29 | }) 30 | .catch(errorPayment); 31 | }; 32 | 33 | const Checkout = ({ name, description, amount, rentalId, goToRentalView}) => ( 34 | 42 | ); 43 | 44 | export default Checkout; -------------------------------------------------------------------------------- /use-my-tools/src/components/Chat/ChatDashboard.css: -------------------------------------------------------------------------------- 1 | /* .pageContainer { 2 | max-width: 950px; 3 | margin-left: auto; 4 | margin-right: auto; 5 | } */ 6 | 7 | .chat-dashboard-container { 8 | position: fixed; 9 | bottom: 0%; 10 | width: 100%; 11 | height: calc(100vh - 80px); 12 | display: flex; 13 | justify-content: center; 14 | overflow-y: hidden; 15 | overflow-x: hidden; 16 | } 17 | 18 | .chat-dash-left-container { 19 | width: 35%; 20 | height: 100%; 21 | } 22 | 23 | .chat-dash-right-container { 24 | border: 1px solid lightgray; 25 | width: 65%; 26 | height: 100%; 27 | /* overflow-x: hidden; */ 28 | } 29 | 30 | .chat-dash-right-container p { 31 | font-size: 22px; 32 | margin-top: 5%; 33 | } 34 | 35 | @media (max-width: 800px) { 36 | .chat-dashboard-container { 37 | margin: auto; 38 | height: calc(100vh - 120px); 39 | } 40 | } 41 | 42 | @media (max-width: 600px) { 43 | .chat-dashboard-container { 44 | margin: auto; 45 | height: calc(100vh - 60px); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Chat/ChatDashboard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import { withFirebase } from '../Firebase'; 4 | import axios from 'axios'; 5 | import ConvoList from './ConvoList/ConvoList'; 6 | import ChatView from './ChatView'; 7 | import './ChatDashboard.css'; 8 | import './ConvoList/ConvoList.css'; 9 | 10 | class ChatDashboardBase extends React.Component { 11 | constructor() { 12 | super(); 13 | this.state = { 14 | uid: null, 15 | firstName: '', 16 | lastName: '', 17 | imageURL: '', 18 | convoSelected: false, 19 | currentConvo: {}, 20 | compoundUID: null, 21 | recipientUID: null, 22 | recipientName: null, 23 | messages: [], 24 | newMessage: '', 25 | } 26 | } 27 | 28 | componentDidMount() { 29 | axios.get("/api/users/userinfo") 30 | .then(user => { 31 | this.setState({ 32 | uid: user.data.uid, 33 | imageUrl: user.data.image_url, 34 | firstName: user.data.first_name, 35 | lastName: user.data.last_name, 36 | }, () => console.log('ChatDash state after CDM get user info: ', this.state)); 37 | }) 38 | .catch(error => { 39 | console.log(error.message); 40 | //this.setState({error:error}); 41 | }); 42 | } 43 | 44 | handleOpenConvoSelect = (convo) => { 45 | console.log('handleOpenConvoSelect convo: ', convo); 46 | const compoundUID = convo.compoundUID || ' '; 47 | const uid = this.state.uid; 48 | console.log('handleOpenConvoSelect uid: ', uid); 49 | let recipientUID = null; 50 | if (convo.UIDs[0] === uid) { 51 | recipientUID = convo.UIDs[1]; 52 | } else { 53 | recipientUID = convo.UIDs[0]; 54 | } 55 | const recipientName = convo[recipientUID]; 56 | 57 | // initialize onSnapshot() listener to Firestore db and get existing messages 58 | // The first query snapshot returned contains 'added' events 59 | // for all existing documents that match the query 60 | let messages = []; 61 | this.props.firebase.db 62 | .collection('conversations') 63 | .doc(compoundUID) 64 | .collection('messages') 65 | .onSnapshot((querySnapshot) => { 66 | querySnapshot.docChanges().forEach((change) => { 67 | if (change.type === 'added') { 68 | messages.push(change.doc.data()); 69 | } 70 | }); 71 | 72 | this.setState({ 73 | convoSelected: true, 74 | currentConvo: convo, 75 | messages, 76 | uid, 77 | compoundUID, 78 | recipientUID, 79 | recipientName 80 | }); 81 | }); 82 | } 83 | 84 | sendMessage = (messageContent) => { 85 | const { compoundUID } = this.state; 86 | const timeStamp = Date.now(); 87 | const messageData = { 88 | content: messageContent, 89 | authorUID: this.state.uid, 90 | recipientUID: this.state.recipientUID, 91 | timeSent: timeStamp 92 | }; 93 | 94 | this.props.firebase.db 95 | .collection('conversations') 96 | .doc(compoundUID) 97 | .collection('messages') 98 | .doc(`${timeStamp}`) 99 | .set(messageData); 100 | this.setState({ message: '' }); 101 | } 102 | 103 | closeCurrentConvo = () => { 104 | const { compoundUID } = this.state; 105 | this.props.firebase.db.collection('conversations').doc(compoundUID).update({ isOpen: false }); 106 | this.setState({ convoSelected: false }); 107 | } 108 | 109 | render() { 110 | const convoSelected = this.state.convoSelected; 111 | return ( 112 |
113 | {this.state.uid ? ( 114 |
115 |
116 | 123 |
124 | 125 |
126 | {!convoSelected ? ( 127 |

No conversation selected.

128 | ) : ( 129 | 138 | ) 139 | } 140 |
141 |
142 | ) : ( 143 | '' 144 | )} 145 | 146 |
147 | ); 148 | } 149 | 150 | } 151 | 152 | const ChatDashboard = withRouter(withFirebase(ChatDashboardBase)); 153 | export default ChatDashboard; 154 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Chat/ChatView.css: -------------------------------------------------------------------------------- 1 | .chatview-container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | border-left: 1.5px solid lightgrey; 6 | } 7 | 8 | .chatview-container .convo-header { 9 | border-bottom: 1.5px solid lightgrey; 10 | text-align: left; 11 | padding: 0 10px 0 10px; 12 | } 13 | 14 | .chatview-container .messages-container { 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: flex-end; 18 | padding: 0 10px 10px 10px; 19 | overflow-y: scroll; 20 | overflow-x: hidden; 21 | flex-grow: 1; 22 | width: 100%; 23 | background-color: white; 24 | } 25 | 26 | .chatview-container button { 27 | width: max-content; 28 | height: 30px; 29 | border-radius: 8px; 30 | border: 1px solid grey; 31 | } 32 | 33 | .chatview-container .send-btn { 34 | background-color: #4cd863; 35 | } 36 | 37 | .chatview-container .end-btn { 38 | background-color: #e83e33; 39 | } 40 | 41 | .chatview-container .input-area { 42 | border-top: 1.5px solid lightgrey; 43 | padding: 10px 10px 10px 5px; 44 | } 45 | 46 | .chatview-container .input-area form { 47 | display: flex; 48 | flex-direction: row; 49 | justify-content: space-between; 50 | align-items: center; 51 | } 52 | 53 | .chatview-container .input-area form .message-input { 54 | width: 60%; 55 | border: 1.5px solid lightgrey; 56 | border-radius: 3px; 57 | height: 35px; 58 | padding: 0; 59 | max-width: 490px; 60 | } 61 | 62 | .chatview-container .input-area form .buttons-container { 63 | width: 39%; 64 | padding: 0; 65 | display: flex; 66 | justify-content: space-between; 67 | } 68 | 69 | .message-container { 70 | border-radius: 10px; 71 | width: max-content; 72 | max-width: 80%; 73 | clear: both; 74 | margin: 5px 0 5px 0; 75 | } 76 | 77 | .align-left { 78 | margin-right: auto; 79 | background-color: #e7e5eb; 80 | } 81 | 82 | .align-right { 83 | margin-left: auto; 84 | background-color: #4cd863; 85 | } 86 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Chat/ChatView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import PropTypes from 'prop-types'; 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import { withFirebase } from '../Firebase'; 7 | import './ChatView.css'; 8 | 9 | const styles = (theme) => ({ 10 | chatViewHeadName: { 11 | fontSize: '24px', 12 | fontWeight: '300', 13 | padding: '0', 14 | }, 15 | messageBody: { 16 | paddingLeft: 20, 17 | paddingRight: 25, 18 | paddingBottom: 0, 19 | textAlign: 'left' 20 | }, 21 | }); 22 | 23 | class ChatViewBase extends Component { 24 | constructor(props) { 25 | super(props); 26 | this.state = { 27 | message: '' 28 | }; 29 | this.messagesRef = React.createRef(); 30 | }; 31 | 32 | componentDidMount() { 33 | this.scrollDownMessages(); 34 | }; 35 | 36 | componentDidUpdate(prevProps, prevState) { 37 | this.scrollDownMessages(); 38 | }; 39 | 40 | scrollDownMessages = () => { 41 | this.messagesRef.current.scrollTop = this.messagesRef.current.scrollHeight; 42 | }; 43 | 44 | // method to send a message when user submits: 45 | onSubmit = (event) => { 46 | const messageContent = this.state.message; 47 | this.props.sendMessage(messageContent); 48 | this.setState({ message: '' }); 49 | event.preventDefault(); 50 | }; 51 | 52 | // method to update state based on user input: 53 | onChange = (event) => { 54 | this.setState({ [event.target.name]: event.target.value }); 55 | }; 56 | 57 | // method to mark the convo as closed 58 | handleCloseConvo = (event) => { 59 | this.props.closeCurrentConvo(); 60 | event.preventDefault(); 61 | }; 62 | 63 | render() { 64 | const { classes } = this.props; 65 | return ( 66 |
67 |
68 |

Chat with {this.props.recipientName}

69 |
70 |
71 | {this.props.messages.map((message, index) => { 72 | let alignClass = null; 73 | if (message.authorUID === this.props.uid) { 74 | alignClass = 'message-container align-right'; 75 | } else { 76 | alignClass = 'message-container align-left'; 77 | } 78 | return ( 79 |
80 |
81 | 82 | {message.content} 83 | 84 |
85 |
86 | ); 87 | })} 88 |
89 | {/* end messagelist */} 90 | 91 |
92 |
93 | 100 |
101 | 102 | 103 |
104 |
105 |
106 | {/* end input area */} 107 |
108 | ); 109 | } 110 | } 111 | 112 | ChatViewBase.propTypes = { 113 | classes: PropTypes.object.isRequired 114 | }; 115 | 116 | // export default withStyles(styles)(ChatView); 117 | const ChatView = withStyles(styles)(withRouter(withFirebase(ChatViewBase))); 118 | 119 | export default ChatView; -------------------------------------------------------------------------------- /use-my-tools/src/components/Chat/ConvoList/ConvoList.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/use-my-tools/src/components/Chat/ConvoList/ConvoList.css -------------------------------------------------------------------------------- /use-my-tools/src/components/Chat/ConvoList/ConvoList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // import { withRouter } from "react-router-dom" 3 | import PropTypes from "prop-types"; 4 | import { withStyles } from "@material-ui/core/styles"; 5 | // import axios from 'axios'; 6 | 7 | import Paper from "@material-ui/core/Paper"; 8 | import Tabs from "@material-ui/core/Tabs"; 9 | import Tab from "@material-ui/core/Tab"; 10 | // import Typography from "@material-ui/core/Typography"; 11 | 12 | import { withFirebase } from "../../Firebase"; 13 | 14 | import Convos from './Convos'; 15 | 16 | 17 | const styles = { 18 | root: { 19 | flexGrow: 1, 20 | // border: '1px solid blue', 21 | // Changes from David 22 | height: '100vh', 23 | display: 'flex', 24 | flexDirection: 'column' 25 | }, 26 | queueMenu: { 27 | // height: '100%', 28 | // border: '1px solid red', 29 | borderRadius: '0px', 30 | width: '100%', 31 | borderBottom: '1px gray solid', 32 | }, 33 | queueList: { 34 | overflow: 'hidden', 35 | // height: '100% ', 36 | height: '100%', 37 | display: 'flex', 38 | flexDirection: 'column' 39 | }, 40 | paper: { 41 | // height: '100%' 42 | borderRadius: '0px' 43 | }, 44 | paper2: { 45 | // height: '100%', 46 | borderRadius: '0px' 47 | }, 48 | tabs1: { 49 | height: '100%' 50 | }, 51 | tabElement: { 52 | width: '100%', 53 | minWidth: 50, 54 | // maxWidth: 200 55 | }, 56 | tab: { 57 | display: 'flex', 58 | justifyContent: 'space-around' 59 | }, 60 | tabLabel: { 61 | fontSize: 16, 62 | padding: 0 63 | }, 64 | convoCount: { 65 | padding: 0, 66 | color: '#69DB30', 67 | 'font-weight': 'bold' 68 | } 69 | }; 70 | 71 | class ConvoListBase extends React.Component { 72 | constructor(props) { 73 | super(props); 74 | this.state = { 75 | value: 0, // value corresponding to Open tab to display the Open convo list on mount 76 | newConvosCount: 0, 77 | convos: [], 78 | openConvos: [], 79 | closedConvos: [] 80 | }; 81 | // this.intervalID = 0; 82 | } 83 | 84 | componentDidMount() { 85 | const uid = this.props.uid; 86 | console.log(uid); 87 | let openConvos = []; 88 | let closedConvos = []; 89 | let convos = []; 90 | 91 | // initialize listener to Firestore db and get existing messages 92 | // listen with onSnapshot() 93 | // The first query snapshot contains 'added' events 94 | // for all existing documents that match the query 95 | this.props.firebase.db 96 | .collection('conversations') 97 | .where('UIDs', 'array-contains', `${uid}`) 98 | .onSnapshot(querySnapshot => { 99 | querySnapshot.docChanges().forEach(change => { 100 | if (change.type === 'added') { 101 | if (change.doc.data().isOpen === false) { 102 | closedConvos.push(change.doc.data()); 103 | } else { 104 | openConvos.push(change.doc.data()); 105 | } 106 | } 107 | 108 | if (change.type === 'modified') { 109 | if (change.doc.data().isOpen === false) { 110 | console.log('convo changed to closed: ', change.doc.data()); 111 | // closedConvos.push(change.doc.data()); 112 | 113 | openConvos = openConvos.filter(function(convo) { 114 | return convo.compoundUID !== change.doc.data().compoundUID; 115 | }) 116 | console.log(openConvos); 117 | } else { 118 | console.log('convo changed to open: ', change.doc.data()); 119 | openConvos.push(change.doc.data()); 120 | } 121 | } 122 | }) 123 | this.setState({ openConvos, closedConvos }); 124 | }); 125 | } 126 | 127 | handleTabSelect= (event, value) => { 128 | this.setState({ value }); 129 | }; 130 | 131 | render() { 132 | const { classes } = this.props; 133 | return ( 134 |
135 |
136 | 137 | 145 | Conversations} /> 146 | 147 | 148 |
149 | 150 |
151 | {this.state.value === 0 && 152 | 158 | } 159 |
160 | 161 |
162 | ); 163 | } 164 | } 165 | 166 | ConvoListBase.propTypes = { 167 | classes: PropTypes.object.isRequired 168 | }; 169 | 170 | const ConvoList = withStyles(styles)(withFirebase(ConvoListBase)); 171 | 172 | export default ConvoList; 173 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Chat/ConvoList/Convos.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import Paper from '@material-ui/core/Paper'; 5 | // import PropTypes from 'prop-types'; 6 | import { withStyles } from '@material-ui/core/styles'; 7 | // import { ThemeProvider, MessageList, MessageGroup, MessageText, MessageTitle, Message, AgentBar, Row } from '@livechat/ui-kit'; 8 | 9 | import { withFirebase } from "../../Firebase"; 10 | 11 | const styles = theme => ({ 12 | root: { 13 | // border: '1px dotted black', 14 | // overflowY: 'scroll', 15 | // height: '100vh', 16 | }, 17 | convoList: { 18 | overflowY: 'scroll', 19 | height: '92vh', 20 | }, 21 | paper: { 22 | height: 100, 23 | textAlign: 'left', 24 | padding: theme.spacing.unit, 25 | paddingLeft: '5%', 26 | borderRadius: '0px', 27 | border: '0.2px solid grey', 28 | borderTop: 'none', 29 | }, 30 | queueItem: { 31 | '&:hover': { 32 | cursor: 'pointer' 33 | } 34 | }, 35 | queueTitle: { 36 | fontSize: '24px', 37 | fontWeight: 300, 38 | marginTop: '5px', 39 | }, 40 | queueSummary: { 41 | fontSize: '20px', 42 | fontWeight: 200, 43 | marginTop: '0', 44 | }, 45 | listFooter: { 46 | height: '100px', 47 | marginTop: '75px', 48 | fontSize: '20px', 49 | } 50 | }); 51 | 52 | const ConvosBase = props => { 53 | const { classes } = props; 54 | return ( 55 |
56 | 59 | 60 |
61 | {props.convos.map((convo, index) => { 62 | let recipientUID = null; 63 | if (convo.UIDs[0] === props.uid) { 64 | recipientUID = convo.UIDs[1]; 65 | } else { 66 | recipientUID = convo.UIDs[0]; 67 | } 68 | return ( 69 |
70 | 71 | this.props.handleConvoSelect(convo.compoundUID)} 75 | onClick={() => props.handleConvoSelect(convo)} 76 | > 77 |

{convo[recipientUID]}

78 |
79 |
80 |
81 | ); 82 | })} 83 |
84 |

End of list

85 |
86 |
87 |
88 | 89 | ); 90 | 91 | } 92 | 93 | const Convos = withStyles(styles)(withFirebase(ConvosBase)); 94 | 95 | export default Convos; 96 | 97 | 98 | // constructor(props) { 99 | // super(props); 100 | // this.state = { 101 | // conversations: [] 102 | // } 103 | 104 | // } 105 | 106 | // componentDidMount() { 107 | // // this.getConvos(); 108 | // const uid = this.props.uid; 109 | // // console.log('convos this.props: ', this.props); 110 | // const isOpen = this.props.isOpen; 111 | 112 | // let conversations = []; 113 | // // one-time get of convos: 114 | // // this.props.firebase.db 115 | // // .collection('conversations') 116 | // // .where('isOpen', '==', isOpen) //isOpen can be true or false depending on prop 117 | // // .get() 118 | // // .then(snapshot => { 119 | // // if (snapshot.empty) { 120 | // // console.log('No matching documents.'); 121 | // // return; 122 | // // } 123 | 124 | // // snapshot.forEach(doc => { 125 | // // conversations.push(doc.data()); 126 | // // // console.log(doc.id, '=>', doc.data()); 127 | // // }); 128 | // // console.log(conversations); 129 | // // this.setState({ conversations }); 130 | // // }) 131 | // // .catch(err => { 132 | // // console.log('Error getting documents', err); 133 | // // }); 134 | 135 | 136 | // // this.props.firebase.db 137 | // // .collection('conversations') 138 | // // .where('UIDs', 'array-contains', uid) 139 | // // .where('isOpen', '==', isOpen) // isOpen can be true or false depending on prop 140 | // // .get() 141 | // // .then(snapshot => { 142 | // // if (snapshot.empty) { 143 | // // console.log('No matching documents.'); 144 | // // // return; 145 | // // } 146 | // // snapshot.forEach(doc => { 147 | // // conversations.push(doc.data()); // push each doc from the conversations collection 148 | // // // console.log(doc.id, '=>', doc.data()); 149 | // // }); 150 | // // console.log('conversations: ', conversations); 151 | // // this.setState({ conversations }); 152 | // // }) 153 | // // .catch(err => { 154 | // // console.log('Error getting documents', err.message); 155 | // // }); 156 | 157 | 158 | // } 159 | 160 | // getConvos = () => { 161 | // console.log('getConvos called'); 162 | 163 | // } 164 | 165 | // componentWillReceiveProps(newProps) { 166 | // if (newProps.currentConvoClosed !== this.props.currentConvoClosed) { 167 | // console.log('Convos currentConvoClosed changed'); 168 | // const getClosed = axios.get(`/api/chat/${this.props.convoStatus}`); 169 | // getClosed 170 | // .then(response => { 171 | // this.setState({ 172 | // conversations: response.data 173 | // }); 174 | // }) 175 | // .catch(error => { 176 | // console.log(error.message); 177 | // }) 178 | // } 179 | // } 180 | 181 | // render() { 182 | // const { classes } = this.props; 183 | 184 | // return ( 185 | //
186 | // 189 | // 190 | //
191 | // {this.state.conversations.map((convo, index) => { 192 | 193 | // return ( 194 | //
195 | // 196 | // this.props.handleConvoSelect(convo.compoundUID)} 200 | // onClick={() => this.props.handleConvoSelect(convo)} 201 | // > 202 | //

{convo.compoundUID}

203 | //
204 | //
205 | //
206 | // ); 207 | // })} 208 | //
209 | //

End of list

210 | //
211 | //
212 | //
213 | 214 | // ); 215 | // // } -------------------------------------------------------------------------------- /use-my-tools/src/components/Firebase/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FirebaseContext = React.createContext(null); 4 | 5 | export const withFirebase = Component => props => ( 6 | 7 | {firebase => } 8 | 9 | ); 10 | 11 | export default FirebaseContext; 12 | 13 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Firebase/firebase.js: -------------------------------------------------------------------------------- 1 | import app from 'firebase/app'; 2 | import 'firebase/auth'; 3 | import 'firebase/firestore'; 4 | 5 | const config = { 6 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY, 7 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, 8 | databaseURL: process.env.REACT_APP_FIREBASE_DB_URL, 9 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, 10 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, 11 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, 12 | }; 13 | 14 | 15 | class Firebase { 16 | constructor() { 17 | app.initializeApp(config); 18 | 19 | // Helper: 20 | this.fieldValue = app.firestore.FieldValue; 21 | //this.emailAuthProvider = app.auth.EmailAuthProvider; 22 | 23 | // Firebase APIs: 24 | this.auth = app.auth(); 25 | this.db = app.firestore(); 26 | 27 | } 28 | 29 | createUser = (email, password) => 30 | this.auth.createUserWithEmailAndPassword(email, password); 31 | 32 | 33 | logIn = (email, password) => 34 | this.auth.signInWithEmailAndPassword(email, password); 35 | 36 | 37 | logOut = () => { 38 | this.auth.signOut(); 39 | } 40 | 41 | updatePassword = password => 42 | this.auth.currentUser.updatePassword(password); 43 | 44 | // Convos API: 45 | convos = () => this.db.collection('conversations'); 46 | 47 | // *** Message API *** 48 | // message = uid => this.db.doc(`messages/${uid}`); 49 | // messages = () => this.db.collection('messages'); 50 | 51 | } 52 | 53 | export default Firebase; 54 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Firebase/index.js: -------------------------------------------------------------------------------- 1 | import FirebaseContext, { withFirebase } from './context'; 2 | import Firebase from './firebase'; 3 | 4 | export default Firebase; 5 | 6 | export { FirebaseContext, withFirebase }; 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /use-my-tools/src/components/ImageCarousel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import MobileStepper from "@material-ui/core/MobileStepper"; 5 | import Button from "@material-ui/core/Button"; 6 | import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft"; 7 | import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"; 8 | 9 | const styles = theme => ({ 10 | root: { 11 | minWidth: 250, 12 | maxWidth: 400, 13 | // flexGrow: 1 14 | }, 15 | header: { 16 | display: "flex", 17 | alignItems: "center", 18 | height: 50, 19 | paddingLeft: theme.spacing.unit * 4, 20 | backgroundColor: theme.palette.background.default 21 | }, 22 | imgContainer: { 23 | //border: "1px solid red", 24 | width: "100%", 25 | // height: 255, 26 | //maxWidth: 225 27 | }, 28 | img: { 29 | //border: "1px solid blue", 30 | overflow: "hidden", 31 | display: "block", 32 | width: "100%", 33 | margin: "auto" 34 | } 35 | }); 36 | 37 | class ImageCarousel extends React.Component { 38 | state = { 39 | activeStep: 0 40 | }; 41 | 42 | handleNext = () => { 43 | this.setState(prevState => ({ 44 | activeStep: prevState.activeStep + 1 45 | })); 46 | }; 47 | 48 | handleBack = () => { 49 | this.setState(prevState => ({ 50 | activeStep: prevState.activeStep - 1 51 | })); 52 | }; 53 | 54 | render() { 55 | const { classes, theme, toolImages } = this.props; 56 | const { activeStep } = this.state; 57 | const maxSteps = toolImages.length; 58 | 59 | return ( 60 |
61 |
62 | 0 66 | ? toolImages[activeStep].url 67 | : "https://openclipart.org/image/2400px/svg_to_png/298157/CrossedTools.png" 68 | } 69 | alt="tool" 70 | /> 71 |
72 | 83 | Next 84 | {theme.direction === "rtl" ? ( 85 | 86 | ) : ( 87 | 88 | )} 89 | 90 | } 91 | backButton={ 92 | 104 | } 105 | /> 106 |
107 | ); 108 | } 109 | } 110 | 111 | ImageCarousel.propTypes = { 112 | classes: PropTypes.object.isRequired, 113 | theme: PropTypes.object.isRequired 114 | }; 115 | 116 | export default withStyles(styles, { withTheme: true })(ImageCarousel); 117 | -------------------------------------------------------------------------------- /use-my-tools/src/components/LandingPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | import ManUsingDrill from '../assets/images/Man-using-drill.jpeg'; 4 | import ToolsOnWall from '../assets/images/Wrenches-on-wall.jpeg'; 5 | 6 | import './css/LandingPage.css'; 7 | 8 | class LandingPage extends Component { 9 | // constructor(props) { 10 | // super(props); 11 | // } 12 | 13 | render() { 14 | return ( 15 |
16 |
17 |
18 |

19 | Better than buying expensive tools 20 |

21 |

22 | Rent tools from owners near you 23 |

24 | 25 | 28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 | man-using-drill 40 |
41 |
42 |

43 | Need to make some home repairs or start a DIY project but don't have all the tools? 44 |

45 | 46 |

47 | Find trusted owners near you and rent their tools for a fraction of the cost. 48 |

49 |
50 |
51 | 52 |
53 |
54 | man-using-drill 59 |
60 |
61 |

62 | Have extra tools in your workshop that aren't being used? 63 |

64 | 65 |

66 | List your tools on Use My Tools and start making money by renting them to people near you. 67 |

68 |
69 | 70 |
71 |
72 |
73 | ); 74 | } 75 | } 76 | 77 | export default withRouter(LandingPage); -------------------------------------------------------------------------------- /use-my-tools/src/components/LocationSearchInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlacesAutocomplete, { 3 | geocodeByAddress, 4 | getLatLng, 5 | } from 'react-places-autocomplete'; 6 | 7 | import TextField from "@material-ui/core/TextField"; 8 | 9 | import './css/LocationSearchInput.css'; 10 | 11 | class LocationSearchInput extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | address: this.props.address 16 | }; 17 | } 18 | 19 | handleChange = address => { 20 | this.setState({ address }); 21 | }; 22 | 23 | handleSelect = address => { 24 | console.log('LocationSearchInput handleSelect address: ', address); 25 | this.handleChange(address); 26 | 27 | geocodeByAddress(address) 28 | .then(results => { 29 | 30 | console.log('geocodeByAddress results: ', results); 31 | 32 | getLatLng(results[0]) 33 | .then(latLng => { 34 | console.log('Success', latLng); 35 | 36 | let addressDetails = { 37 | formattedAddress: results[0].formatted_address, 38 | addressComponents: results[0].address_components, 39 | latLng: latLng, 40 | placeId: results[0].place_id 41 | }; 42 | 43 | this.props.handleSelectLocation(addressDetails); 44 | }) 45 | .catch(error => console.error('Error', error)); 46 | }) 47 | .catch(error => console.error('Error', error)); 48 | }; 49 | 50 | render() { 51 | return ( 52 | 57 | {({ getInputProps, suggestions, getSuggestionItemProps, loading }) => ( 58 |
59 | 68 | {/* */} 77 |
78 | {loading &&
Loading...
} 79 | {suggestions.map(suggestion => { 80 | // const className = suggestion.active 81 | // ? 'suggestion-item--active' 82 | // : 'suggestion-item'; 83 | // inline style for demonstration purpose 84 | const style = suggestion.active 85 | ? { backgroundColor: '#fafafa', cursor: 'pointer' } 86 | : { backgroundColor: '#ffffff', cursor: 'pointer' }; 87 | return ( 88 |
94 | {suggestion.description} 95 |
96 | ); 97 | })} 98 |
99 |
100 | )} 101 |
102 | ); 103 | } 104 | } 105 | 106 | export default LocationSearchInput; 107 | 108 | // class PlaceAutocompleteForm extends React.Component { 109 | // constructor(props) { 110 | // super(props); 111 | // this.state = { address: '' }; 112 | 113 | // // Specify form input field: 114 | // const autocompleteFormField = document.getElementById(`street-address-field`); 115 | 116 | // // Instantiate a new autocomplete object: 117 | // const autocomplete = new google.maps.places.Autocomplete((autocompleteFormField), { 118 | // types: [`address`], 119 | // componentRestrictions: [`us`], 120 | // }); 121 | 122 | // // Clearing the listener ensures that the results that are returned always correspond to the latest user input. 123 | // // Depending on your implementation, you might need to call this method several times 124 | // google.maps.event.clearInstanceListeners(autocompleteFormField); 125 | 126 | // // Add the custom Google Places listener, place_changed, 127 | // // which will activate when a result from the autocomplete list is selected: 128 | // google.maps.event.addListener(autocomplete, `place_changed`, () => { 129 | // // Custom methods to populate form fields. 130 | // }) 131 | 132 | // } 133 | 134 | // // componentDidMount() { 135 | 136 | // // } 137 | 138 | // } 139 | 140 | // export default PlaceAutocompleteForm; -------------------------------------------------------------------------------- /use-my-tools/src/components/LoginPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withFirebase } from "./Firebase"; 3 | import { Link, withRouter } from "react-router-dom" 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | import TextField from 'material-ui/TextField'; 7 | import Typography from "@material-ui/core/Typography"; 8 | import axios from 'axios'; 9 | 10 | import "./css/LoginPage.css"; 11 | 12 | class LoginBase extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | email: "", 17 | password: "", 18 | error: null 19 | }; 20 | } 21 | 22 | onSubmit = event => { 23 | event.preventDefault(); 24 | const {email, password } = this.state; 25 | console.log('LoginPage state on submit: ', this.state); 26 | 27 | this.props.firebase.logIn(email, password) 28 | .then(authUser => { 29 | console.log('authUser: ', authUser); 30 | 31 | this.props.firebase.auth.currentUser.getIdToken() 32 | .then(idToken => { 33 | console.log("idToken from firebase logIn: ", idToken); 34 | axios.defaults.headers.common['Authorization'] = idToken; 35 | this.props.history.push('/accountpage'); 36 | }) 37 | .catch(error => { // if Firebase getIdToken throws an error 38 | this.setState({ 39 | email: "", 40 | password: "", 41 | error:error 42 | }); 43 | }) 44 | }) 45 | .catch(error => { // if Firebase logIn throws an error 46 | this.setState({ 47 | email: "", 48 | password: "", 49 | error:error 50 | }); 51 | }); 52 | } 53 | 54 | onChange = event => { 55 | this.setState({ [event.target.name]: event.target.value }); 56 | }; 57 | 58 | render() { 59 | const { email, password, error } = this.state; 60 | const invalidCondition = password === '' || email === ''; 61 | 62 | return ( 63 | 64 | 65 |
66 | 67 | 68 | Sign In 69 | 70 | 71 |
72 | 73 | 83 |
84 | 85 | 95 |
96 | 97 | 104 | 105 |
106 |

Don't have an account?

107 | 108 | Sign Up 109 | 110 |
111 | 112 | {error &&

{error.message}

} 113 | 114 | 115 |
116 |
117 | ); 118 | } 119 | } 120 | 121 | const LoginPage = withRouter(withFirebase(LoginBase)); 122 | 123 | export default LoginPage; 124 | 125 | -------------------------------------------------------------------------------- /use-my-tools/src/components/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withFirebase } from "./Firebase"; 3 | import { FirebaseContext } from './Firebase'; 4 | import { Link, withRouter } from "react-router-dom" 5 | // import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 6 | // import RaisedButton from 'material-ui/RaisedButton'; 7 | // import TextField from 'material-ui/TextField'; 8 | // import Typography from '@material-ui/core/Typography'; 9 | // import axios from 'axios'; 10 | 11 | import "./css/Logout.css"; 12 | 13 | const Logout = () => ( 14 |
15 | 16 | {firebase => } 17 | 18 |
19 | ); 20 | 21 | class LogoutButtonBase extends Component { 22 | 23 | logOut = event => { 24 | this.props.firebase.logOut(); 25 | } 26 | 27 | render() { 28 | return ( 29 | 30 | {/*
*/} 31 | Sign Out 32 | {/*
*/} 33 | 34 | ); 35 | } 36 | } 37 | 38 | const LogoutButton= withRouter(withFirebase(LogoutButtonBase)); 39 | 40 | export default Logout; 41 | 42 | export {LogoutButton}; -------------------------------------------------------------------------------- /use-my-tools/src/components/Navigation/MobileNavMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { NavLink } from "react-router-dom"; 3 | import LogoutButton from '../LogoutButton'; 4 | import Dialog from '@material-ui/core/Dialog'; 5 | 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | import ListItemText from '@material-ui/core/ListItemText'; 8 | import ListItem from '@material-ui/core/ListItem'; 9 | import List from '@material-ui/core/List'; 10 | import Divider from '@material-ui/core/Divider'; 11 | import AppBar from '@material-ui/core/AppBar'; 12 | import Toolbar from '@material-ui/core/Toolbar'; 13 | import IconButton from '@material-ui/core/IconButton'; 14 | import Typography from '@material-ui/core/Typography'; 15 | import CloseIcon from '@material-ui/icons/Close'; 16 | import Slide from '@material-ui/core/Slide'; 17 | import './css/MobileNavMenu.css'; 18 | 19 | // const useStyles = makeStyles(theme => ({ 20 | // appBar: { 21 | // position: 'relative', 22 | // }, 23 | // title: { 24 | // marginLeft: theme.spacing(2), 25 | // flex: 1, 26 | // }, 27 | // })); 28 | 29 | // const Transition = React.forwardRef(function Transition(props, ref) { 30 | // return ; 31 | // }); 32 | 33 | // const MobileNavLink = props => { 34 | // return ( 35 | // 41 | // {props.name} 42 | // 43 | // ) 44 | // } 45 | 46 | class MobileNavMenu extends Component { 47 | constructor(props) { 48 | super(props); 49 | this.state = { 50 | open: false 51 | }; 52 | this.Transition = React.forwardRef(function Transition(props, ref) { 53 | return ; 54 | }); 55 | } 56 | 57 | 58 | handleClickOpen = () => { 59 | this.setState({ open: true }); 60 | // event.preventDefault(); 61 | // const openState = this.state.open ? false : true; 62 | // console.log(openState); 63 | // this.setState({ open: openState }); 64 | } 65 | 66 | handleClose = () => { 67 | this.setState({ open: false }); 68 | }; 69 | 70 | render() { 71 | // const classes = useStyles(); 72 | return ( 73 |
74 | 77 | 78 | 86 |
87 | 88 | Find Tools 89 | 90 | 91 | 92 | Add a tool 93 | 94 | 95 | 96 | Owner Dashboard 97 | 98 | 99 | 100 | Renter Dashboard 101 | 102 | 103 | 104 | Messages 105 | 106 | 107 | 108 | Account 109 | 110 |
111 |
112 | 115 | 118 |
119 |
120 |
121 | ) 122 | } 123 | }; 124 | 125 | export default MobileNavMenu; 126 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Navigation/NavAuth.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from "react-router-dom"; 3 | import LogoutButton from '../LogoutButton'; 4 | import MobileNavMenu from './MobileNavMenu'; 5 | import './css/NavAuth.css'; 6 | 7 | const NavAuth = () => ( 8 |
9 |

Use My Tools

10 |
11 | 12 | Find Tools 13 | 14 | 15 | 16 | Add a tool 17 | 18 | 19 | 20 | Owner Dashboard 21 | 22 | 23 | Renter Dashboard 24 | 25 | 26 | 27 | Messages 28 | 29 | 30 | 31 | Account 32 | 33 | 34 |
35 | 36 |
37 | ); 38 | 39 | export default NavAuth; -------------------------------------------------------------------------------- /use-my-tools/src/components/Navigation/NavNonAuth.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from "react-router-dom"; 3 | import './css/NavNonAuth.css'; 4 | 5 | const NavNonAuth = () => ( 6 |
7 |

Use My Tools

8 |
9 | 10 | Home 11 | 12 | 13 | Sign Up 14 | 15 | 16 | Sign In 17 | 18 |
19 |
20 | ); 21 | 22 | export default NavNonAuth; -------------------------------------------------------------------------------- /use-my-tools/src/components/Navigation/NavigationBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from "react-router-dom"; 3 | import NavNonAuth from './NavNonAuth'; 4 | import NavAuth from './NavAuth'; 5 | 6 | const NavigationBar = props => ( 7 |
{props.authUser ? : }
8 | ); 9 | 10 | export default withRouter(NavigationBar); -------------------------------------------------------------------------------- /use-my-tools/src/components/Navigation/css/MobileNavMenu.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 600px) { 2 | .mobile-nav-container { 3 | display: none; 4 | } 5 | } 6 | 7 | .mobile-nav-dialog { 8 | width: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .mobile-nav-dialog .menu-container { 14 | width: 100%; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | margin: 10px 0 10px 0; 19 | border-bottom: 1px solid black; 20 | } 21 | 22 | .mobile-nav-dialog .menu-container .mobile-nav-link { 23 | width: 100%; 24 | text-align: center; 25 | padding: 10px 10px 10px 10px; 26 | border-top: 1px solid black; 27 | text-decoration: none; 28 | color: black; 29 | } 30 | 31 | .mobile-nav-dialog .button-container { 32 | display: flex; 33 | justify-content: space-between; 34 | width: 60%; 35 | margin-left: auto; 36 | margin-right: auto; 37 | } 38 | 39 | .mobile-nav-dialog button { 40 | width: 80px; 41 | height: 40px; 42 | border: 1px solid black; 43 | border-radius: 10px; 44 | } 45 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Navigation/css/NavAuth.css: -------------------------------------------------------------------------------- 1 | .nav-container-auth { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | margin: auto; 6 | height: 80px; 7 | background-color: #3e6caa; 8 | padding: 0 10px 0 10px; 9 | } 10 | 11 | .nav-container-auth .nav-link-container { 12 | width: 70%; 13 | display: flex; 14 | justify-content: space-around; 15 | margin: 0px 30px 0px 30px; 16 | } 17 | 18 | .nav-container-auth .nav-link { 19 | margin: 0px 20px 0px 0px; 20 | font-size: 14px; 21 | text-decoration: none; 22 | color: black; 23 | } 24 | 25 | .nav-container-auth .nav-link:hover { 26 | color: white; 27 | } 28 | 29 | .nav-container-auth .logo { 30 | padding: 10px; 31 | margin: 0; 32 | } 33 | 34 | @media (max-width: 800px) { 35 | .nav-container-auth { 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: space-around; 39 | align-items: flex-start; 40 | margin: auto; 41 | height: 120px; 42 | } 43 | 44 | .nav-container-auth .nav-link-container { 45 | /* display: none; */ 46 | width: 95%; 47 | min-width: 300px; 48 | display: flex; 49 | justify-content: space-between; 50 | margin: 0px 25px 20px 15px; 51 | } 52 | 53 | .nav-container-auth .nav-link { 54 | /* display: none; */ 55 | margin: 0px 30px 0px 0px; 56 | text-decoration: none; 57 | } 58 | } 59 | 60 | @media (max-width: 600px) { 61 | .nav-container-auth { 62 | height: 60px; 63 | flex-direction: row; 64 | justify-content: space-between; 65 | align-items: center; 66 | } 67 | 68 | .nav-container-auth .nav-link-container { 69 | display: none; 70 | } 71 | 72 | .nav-container-auth .nav-lin { 73 | display: none; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Navigation/css/NavNonAuth.css: -------------------------------------------------------------------------------- 1 | .nav-non-auth-container { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | margin: auto; 6 | height: 80px; 7 | background-color: #3e6caa; 8 | padding: 0 10px 0 10px; 9 | } 10 | 11 | .nav-non-auth-container .logo { 12 | padding: 10px; 13 | margin: 0; 14 | } 15 | 16 | .nav-non-auth-container .nav-link-container { 17 | width: 40%; 18 | margin-right: 25px; 19 | display: flex; 20 | justify-content: space-between; 21 | } 22 | 23 | .nav-non-auth-container .nav-link { 24 | text-decoration: none; 25 | color: black; 26 | } 27 | 28 | .nav-non-auth-container .nav-link:hover { 29 | color: white; 30 | } 31 | 32 | @media (max-width: 800px) { 33 | .nav-non-auth-container .nav-link-container { 34 | width: 50%; 35 | margin: 0px 25px 20px 15px; 36 | } 37 | 38 | .nav-non-auth-container .nav-link-container .nav-link { 39 | margin: 0px 30px 0px 0px; 40 | } 41 | } 42 | 43 | @media (max-width: 600px) { 44 | .nav-non-auth-container { 45 | height: auto; 46 | flex-direction: row; 47 | justify-content: space-between; 48 | align-items: center; 49 | } 50 | 51 | .nav-non-auth-container .nav-link-container { 52 | width: 50%; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Owner/DeleteDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogContentText from '@material-ui/core/DialogContentText'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import axios from 'axios'; 9 | 10 | class DeleteDialog extends React.Component { 11 | state = { 12 | open: false, 13 | error: null 14 | }; 15 | 16 | handleClickOpen = () => { 17 | this.setState({ open: true }); 18 | }; 19 | 20 | handleClose = () => { 21 | this.setState({ open: false }); 22 | }; 23 | 24 | handleConfirm = () => { 25 | const id = this.props.toolId; 26 | axios.delete(`/api/tools/tool/delete/${id}`) 27 | .then(response => { 28 | this.handleClose(); 29 | this.props.handleToolDelete(); 30 | }) 31 | .catch(error => { 32 | this.setState({ error: error.message }); 33 | }) 34 | }; 35 | 36 | render() { 37 | 38 | return ( 39 |
40 | 43 | 44 | 49 |
50 | Delete Tool 51 | {!this.props.hasIncompleteRentals ? ( 52 |
53 | 54 | 55 | Are you sure you want to delete this tool? 56 | 57 | 58 | 59 | 60 | 63 | 66 | 67 |
68 | ) : ( 69 |
70 | 71 | 72 | This tool cannot be deleted because it has rentals booked that have not been completed. 73 | 74 | 75 | 76 | 77 | 80 | 81 |
82 | )} 83 |
84 | 85 |
86 | {this.state.error &&

{this.state.error}

} 87 |
88 | ); 89 | } 90 | } 91 | 92 | export default DeleteDialog; -------------------------------------------------------------------------------- /use-my-tools/src/components/Owner/MyTools.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter, Link } from "react-router-dom" 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import Grid from "@material-ui/core/Grid"; 5 | import ToolCard from '../ToolCard'; 6 | 7 | import './css/MyTools.css'; 8 | 9 | import axios from 'axios'; 10 | 11 | const styles = theme => ({ 12 | card: { 13 | height: "100%", 14 | display: "flex", 15 | flexDirection: "column" 16 | }, 17 | cardContent: { 18 | // flexGrow: 1, 19 | maxHeight: 100, 20 | // minHeight: 100, 21 | overflow: "hidden", 22 | textAlign: "left" 23 | }, 24 | cardActions: { 25 | flexGrow: 1, 26 | alignItems: "flex-end" 27 | }, 28 | cardTitle: { 29 | fontWeight: "bold" 30 | }, 31 | gridItem: { 32 | width: "24%" 33 | } 34 | 35 | }) 36 | 37 | class MyTools extends Component { 38 | constructor(props) { 39 | super(props); 40 | this.state = { 41 | tools: [] 42 | }; 43 | } 44 | 45 | componentDidMount() { 46 | axios.get('/api/tools/mytools') 47 | .then(tools => { 48 | this.setState({ 49 | tools: tools.data 50 | }, () => console.log('ToolsOwned state.tools after GET tools: ', this.state.tools)) ; 51 | }) 52 | .catch(error => { 53 | console.log(error.message); 54 | }) 55 | } 56 | 57 | render() { 58 | const { classes } = this.props; 59 | 60 | return ( 61 |
62 | {/*

Manage your tools

*/} 63 | 64 |
65 | 66 | 67 | 68 | {this.state.tools.map((tool, index) => { 69 | return ( 70 | 71 | 72 | 73 | ); 74 | })} 75 | 76 | 77 | 78 |
79 |
80 | 81 | ); 82 | } 83 | } 84 | 85 | export default withRouter(withStyles(styles)(MyTools)); -------------------------------------------------------------------------------- /use-my-tools/src/components/Owner/OwnerDashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter, Link } from "react-router-dom" 3 | import { withStyles } from "@material-ui/core/styles"; 4 | 5 | // import Paper from "@material-ui/core/Paper"; 6 | import Tabs from "@material-ui/core/Tabs"; 7 | import Tab from "@material-ui/core/Tab"; 8 | // import Typography from "@material-ui/core/Typography"; 9 | 10 | import MyTools from './MyTools'; 11 | import RentalsView from '../Rentals/RentalsView'; 12 | 13 | // import axios from 'axios'; 14 | 15 | import './css/OwnerDashboard.css'; 16 | 17 | const styles = theme => ({ 18 | tabMenu: { 19 | // height: '100%', 20 | // border: '1px solid red', 21 | borderRadius: '0px', 22 | width: '100%', 23 | borderBottom: '1px gray solid', 24 | }, 25 | paper: { 26 | // height: '100%' 27 | borderRadius: '0px' 28 | }, 29 | paper2: { 30 | // height: '100%', 31 | borderRadius: '0px' 32 | }, 33 | tabs1: { 34 | height: '100%' 35 | }, 36 | // tabElement: { 37 | // width: '30%', 38 | // minWidth: 50, 39 | // maxWidth: 200 40 | // }, 41 | // tab: { 42 | // display: 'flex', 43 | // justifyContent: 'space-around' 44 | // }, 45 | tabLabel: { 46 | fontSize: 16, 47 | padding: 0 48 | }, 49 | 50 | }) 51 | 52 | const VerticalTabs = withStyles(theme => ({ 53 | flexContainer: { 54 | flexDirection: "column", 55 | height: "100%", 56 | }, 57 | indicator: { 58 | display: "none" 59 | } 60 | }))(Tabs); 61 | 62 | const VerticalTab = withStyles(theme => ({ 63 | selected: { 64 | color: "tomato", 65 | borderRight: "5px solid tomato" 66 | } 67 | }))(Tab); 68 | 69 | class OwnerDashboard extends Component { 70 | constructor(props) { 71 | super(props); 72 | this.state = { 73 | activeTabIndex: 0 74 | }; 75 | } 76 | 77 | handleTabSelect = (_, activeTabIndex) => this.setState({ activeTabIndex }); 78 | 79 | render() { 80 | const { classes } = this.props; 81 | const { activeTabIndex } = this.state 82 | return ( 83 |
84 |
85 |
95 |

Owner Dashboard

96 | 97 | Rentals} /> 98 | Tools} /> 99 | 100 |
101 |
102 | 103 |
104 | {activeTabIndex === 0 && } 105 | {activeTabIndex === 1 && } 106 |
107 | 108 |
109 | ); 110 | } 111 | } 112 | 113 | export default withRouter(withStyles(styles)(OwnerDashboard)); 114 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Owner/css/AddTool.css: -------------------------------------------------------------------------------- 1 | .add-tool-container { 2 | margin: auto; 3 | width: 50%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: flex-start; 7 | } 8 | 9 | form { 10 | height: 700px; 11 | } 12 | 13 | form .add-image { 14 | display: flex; 15 | justify-content: space-around; 16 | } 17 | 18 | form .register-button { 19 | width: 100px; 20 | } 21 | 22 | @media (max-width: 800px) { 23 | .add-image { 24 | display: flex; 25 | flex-direction: column; 26 | align-items: flex-start; 27 | } 28 | } -------------------------------------------------------------------------------- /use-my-tools/src/components/Owner/css/MyTools.css: -------------------------------------------------------------------------------- 1 | .mytools-page-container { 2 | margin: 0; 3 | } 4 | 5 | .mytools-page-container .tools-list-container { 6 | width: 100%; 7 | } -------------------------------------------------------------------------------- /use-my-tools/src/components/Owner/css/OwnerDashboard.css: -------------------------------------------------------------------------------- 1 | .owner-dashboard-container { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | height: 100vh; 6 | } 7 | 8 | .vertical-tabs-container { 9 | width: max-content; 10 | } 11 | 12 | .selected-view-container { 13 | flex-grow: 1; 14 | } -------------------------------------------------------------------------------- /use-my-tools/src/components/Owner/css/ToolViewOwner.css: -------------------------------------------------------------------------------- 1 | .pageContainer { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .title { 7 | padding: 10px; 8 | text-align: left; 9 | } 10 | 11 | .main-container { 12 | margin: 10px; 13 | height: 100%; 14 | display: flex; 15 | justify-content: space-around; 16 | } 17 | 18 | .left-container { 19 | width: 40%; 20 | min-width: 270px; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: flex-start; 24 | } 25 | 26 | .add-image { 27 | height: 50px; 28 | display: flex; 29 | justify-content: space-between; 30 | align-items: center; 31 | } 32 | 33 | .right-container { 34 | display: flex; 35 | flex-direction: column; 36 | align-items: flex-start; 37 | /* height: max-content; */ 38 | width: 50%; 39 | } 40 | 41 | .tool-info { 42 | width: 100%; 43 | margin: 0 10px 0 10px; 44 | padding: 0; 45 | } 46 | 47 | form { 48 | width: 100%; 49 | display: flex; 50 | flex-direction: column; 51 | height: max-content; 52 | } 53 | 54 | .available-container { 55 | width: 150px; 56 | padding: 12px; 57 | display: flex; 58 | justify-content: space-between; 59 | } 60 | 61 | .tool-info .button { 62 | margin: 10px; 63 | width: 150px; 64 | } 65 | 66 | .right-container .tool-management { 67 | height: 100px; 68 | width: 100%; 69 | padding-top: 15px; 70 | margin: 10px 10px 10px 10px; 71 | display: flex; 72 | /* flex-direction: column; 73 | align-items: flex-start; */ 74 | justify-content: space-between; 75 | border-top: 1px solid grey; 76 | } 77 | 78 | @media (max-width: 600px) { 79 | .main-container { 80 | flex-direction: column; 81 | align-items: space-around; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /use-my-tools/src/components/ReactDates/DateRangePicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import moment from "moment"; 3 | import PropTypes from "prop-types"; 4 | import momentPropTypes from "react-moment-proptypes"; 5 | // import "moment/locale/zh-cn"; 6 | import "react-dates/initialize"; 7 | import "react-dates/lib/css/_datepicker.css"; 8 | import omit from "lodash/omit"; 9 | import { DayPickerRangeController } from "react-dates"; 10 | import { withStyles, withStylesPropTypes, css } from "react-with-styles"; 11 | // import axios from 'axios'; 12 | 13 | const propTypes = { 14 | ...withStylesPropTypes, 15 | 16 | // example props for the demo 17 | // blockedDays: PropTypes.arrayOf( 18 | // momentPropTypes.momentObj 19 | // ), 20 | autoFocus: PropTypes.bool, 21 | autoFocusEndDate: PropTypes.bool, 22 | initialStartDate: momentPropTypes.momentObj, 23 | initialEndDate: momentPropTypes.momentObj, 24 | presets: PropTypes.arrayOf( 25 | PropTypes.shape({ 26 | text: PropTypes.string, 27 | start: momentPropTypes.momentObj, 28 | end: momentPropTypes.momentObj 29 | }) 30 | ), 31 | ...omit({}, [ 32 | "startDate", 33 | "endDate", 34 | "onDatesChange", 35 | "focusedInput", 36 | "onFocusChange" 37 | ]) 38 | }; 39 | 40 | const defaultProps = { 41 | // example props for the demo 42 | autoFocus: false, 43 | autoFocusEndDate: false, 44 | initialStartDate: null, 45 | initialEndDate: null, 46 | presets: [ 47 | { 48 | text: "Week", 49 | start: moment(), 50 | end: moment().add(6, "day") 51 | }, 52 | { 53 | text: "Month", 54 | start: moment(), 55 | end: moment().add(29, "day") 56 | }, 57 | { 58 | text: "3 Months", 59 | start: moment(), 60 | end: moment().add(89, "day") 61 | } 62 | ], 63 | 64 | // input related props 65 | // startDateId: "startDate", 66 | // startDatePlaceholderText: "Start Date", 67 | // endDateId: "endDate", 68 | // endDatePlaceholderText: "End Date", 69 | disabled: false, 70 | // required: false, 71 | // screenReaderInputMessage: "", 72 | // showClearDates: true, 73 | // showDefaultInputIcon: false, 74 | // customInputIcon: null, 75 | // customArrowIcon: null, 76 | // customCloseIcon: null, 77 | 78 | // calendar presentation and interaction related props 79 | renderMonthText: null, 80 | orientation: "horizontal", 81 | // anchorDirection: "left", 82 | // horizontalMargin: 0, 83 | withPortal: false, 84 | // withFullScreenPortal: false, 85 | initialVisibleMonth: null, 86 | numberOfMonths: 1, 87 | keepOpenOnDateSelect: true, 88 | // reopenPickerOnClearDates: true, 89 | isRTL: false, 90 | 91 | // navigation related props 92 | navPrev: null, 93 | navNext: null, 94 | onPrevMonthClick() {}, 95 | onNextMonthClick() {}, 96 | onClose() {}, 97 | 98 | // day presentation and interaction related props 99 | renderDayContents: null, 100 | minimumNights: 0, 101 | enableOutsideDays: false, 102 | // isDayBlocked: day => this.props.blockedDays.includes(day), 103 | // isDayBlocked: () => false, 104 | isOutsideRange: day => (moment().diff(day) > 0), 105 | isDayHighlighted: () => false, 106 | 107 | // internationalization 108 | // displayFormat: () => moment.localeData().longDateFormat("L"), 109 | monthFormat: "MMMM YYYY" 110 | }; 111 | 112 | // ****** DRP Wrapper Component ****** 113 | 114 | class DateRangePickerWrapper extends Component { 115 | constructor(props) { 116 | super(props); 117 | this.state = { 118 | startDate: new moment(), 119 | endDate: new moment().add(1, "day"), 120 | focusedInput: 'startDate', 121 | blockedDays: [] 122 | } 123 | } 124 | 125 | onDatesChange = ({ startDate, endDate }) => { 126 | this.props.onDatesChange({ startDate, endDate }); 127 | // console.log('DateRangePicker startDate in onDatesChange: ', startDate); 128 | this.setState({ startDate, endDate }); 129 | }; 130 | 131 | onFocusChange = focusedInput => this.setState({ focusedInput }); 132 | 133 | // isDayBlocked = day => { 134 | // this.props.blockedDays.includes(day); 135 | // } 136 | 137 | renderDatePresets = () => { 138 | const { presets, styles } = this.props; 139 | const { startDate, endDate } = this.state; 140 | // console.log(styles); 141 | 142 | return ( 143 |
144 | {presets.map(({ text, start, end }) => { 145 | const isSelected = 146 | isSameDay(start, startDate) && isSameDay(end, endDate); 147 | return ( 148 | 161 | ); 162 | })} 163 |
164 | ); 165 | }; 166 | 167 | // onOutsideClick = () => { 168 | // this.setState({ focusedInput: 'startDate' }); 169 | // } 170 | 171 | render() { 172 | const { focusedInput, startDate, endDate } = this.state; 173 | const props = omit(this.props, [ 174 | "autoFocus", 175 | "autoFocusEndDate", 176 | "initialStartDate", 177 | "initialEndDate", 178 | "presets", 179 | ]); 180 | return ( 181 | 182 | 192 | 193 | ); 194 | } 195 | } 196 | 197 | DateRangePickerWrapper.propTypes = propTypes; 198 | DateRangePickerWrapper.defaultProps = defaultProps; 199 | 200 | function isSameDay(a, b) { 201 | if (!moment.isMoment(a) || !moment.isMoment(b)) return false; 202 | // Compare least significant, most likely to change units first 203 | // Moment's isSame clones moment inputs and is a tad slow 204 | return ( 205 | a.date() === b.date() && a.month() === b.month() && a.year() === b.year() 206 | ); 207 | } 208 | 209 | export default withStyles(({ reactDates: { color } }) => ({ 210 | PresetDateRangePicker_panel: { 211 | padding: "0 22px 11px 22px" 212 | }, 213 | 214 | PresetDateRangePicker_button: { 215 | position: "relative", 216 | height: "100%", 217 | textAlign: "center", 218 | background: "none", 219 | border: `2px solid ${color.core.primary}`, 220 | color: color.core.primary, 221 | padding: "4px 12px", 222 | marginRight: 8, 223 | font: "inherit", 224 | fontWeight: 700, 225 | lineHeight: "normal", 226 | overflow: "visible", 227 | boxSizing: "border-box", 228 | cursor: "pointer", 229 | 230 | ":active": { 231 | outline: 0 232 | } 233 | }, 234 | 235 | PresetDateRangePicker_button__selected: { 236 | color: color.core.white, 237 | background: color.core.primary 238 | } 239 | }))(DateRangePickerWrapper); 240 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Rentals/CancelDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogContentText from '@material-ui/core/DialogContentText'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import axios from 'axios'; 9 | 10 | 11 | class CancelDialog extends React.Component { 12 | state = { 13 | open: false, 14 | error: null 15 | }; 16 | 17 | handleClickOpen = () => { 18 | this.setState({ open: true }); 19 | }; 20 | 21 | handleClose = () => { 22 | this.setState({ open: false }); 23 | }; 24 | 25 | handleConfirm = () => { 26 | this.handleClose(); 27 | this.props.confirmCancelRental(); 28 | // const { rentalId, cancelStatus } = this.props; 29 | // const updateData = { rentalId, status: cancelStatus }; 30 | // axios.put(`/api/rentals/updatestatus`, updateData) 31 | // .then(response => { 32 | // console.log('resonse from cancel request: ', response); 33 | // this.handleClose(); 34 | // }) 35 | // .catch(error => { 36 | // this.setState({ error: error.message }); 37 | // }) 38 | }; 39 | 40 | render() { 41 | 42 | return ( 43 |
44 | 47 | 48 | 53 | 54 |
55 | Cancel Rental 56 | 57 | 58 | 59 | Are you sure you want to cancel this rental? 60 | 61 | 62 | 63 | 64 | 67 | 70 | 71 |
72 | 73 |
74 | {this.state.error &&

{this.state.error}

} 75 |
76 | ); 77 | } 78 | } 79 | 80 | export default CancelDialog; -------------------------------------------------------------------------------- /use-my-tools/src/components/Rentals/RentalsList.js: -------------------------------------------------------------------------------- 1 | // RentalsList displays list of rentals of specific category (Upcoming, Active, or History) 2 | // takes a prop from RentalsView that specifies category 3 | // CDM get request for rentals uses a variable for Rental Status based on category prop 4 | // for Upcoming, get Rentals with Status 'upcoming' 5 | // for Active, get Rentals with Status 'active' 6 | // for History, get Rentals with Status 'completed', 'cancelledByOwner', or 'cancelledByRenter 7 | 8 | import React, { Component } from 'react'; 9 | import { withRouter, Link } from "react-router-dom" 10 | // import { withStyles } from "@material-ui/core/styles"; 11 | // import Card from "@material-ui/core/Card"; 12 | // import CardActions from "@material-ui/core/CardActions"; 13 | // import CardContent from "@material-ui/core/CardContent"; 14 | // import Grid from "@material-ui/core/Grid"; 15 | import Button from "@material-ui/core/Button"; 16 | import Typography from "@material-ui/core/Typography"; 17 | // import Paper from "@material-ui/core/Paper"; 18 | // import Tabs from "@material-ui/core/Tabs"; 19 | // import Tab from "@material-ui/core/Tab"; 20 | 21 | import axios from 'axios'; 22 | // import moment from 'moment'; 23 | 24 | import './css/RentalsList.css'; 25 | 26 | class RentalsList extends Component { 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | rentals: [] 31 | } 32 | } 33 | 34 | componentDidMount() { 35 | // get rentals 36 | const { userType, tabName } = this.props; 37 | let statuses = []; 38 | if (tabName === 'upcoming') { 39 | statuses = ['upcoming'] 40 | } else if (tabName === 'active') { 41 | statuses = ['active'] 42 | } else if (tabName === 'history') { 43 | statuses = ['completed', 'cancelledByOwner', 'cancelledByRenter'] 44 | } 45 | 46 | let rentalRequestData = { 47 | statuses 48 | } 49 | 50 | axios.post(`/api/rentals/${userType}/getrentals`, rentalRequestData) 51 | .then(rentals => { 52 | // console.log('RentalsList CDM rental data: ', rentals.data); 53 | const dateFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }; // format options for dates used below 54 | // convert dates into correct format for display: 55 | for (let rental of rentals.data) { 56 | const formattedStartDate = this.formatDate(rental.StartDate, dateFormatOptions); 57 | const formattedEndDate = this.formatDate(rental.EndDate, dateFormatOptions); 58 | rental.StartDate = formattedStartDate; 59 | rental.EndDate = formattedEndDate; 60 | } 61 | this.setState({ rentals: rentals.data }, () => console.log(this.state)); 62 | }) 63 | .catch(error => { 64 | console.log(error.message); 65 | }) 66 | } 67 | 68 | formatDate = (dateData, dateFormatOptions) =>{ 69 | const date = new Date(dateData); 70 | // console.log(date); 71 | const formattedDate = date.toLocaleDateString("en-US", dateFormatOptions); 72 | // console.log(formattedDate); 73 | return formattedDate; 74 | } 75 | 76 | render() { 77 | const { rentals } = this.state; 78 | if (rentals.length === 0) { 79 | return ( 80 |

You do not have any rentals in {this.props.tabName}.

81 | ) 82 | } else { 83 | return ( 84 |
85 | 86 | {rentals.map((rental, index) => { 87 | 88 | return ( 89 | 94 | 95 | tool 100 |
101 | 104 | {rental.ToolBrand}{' '}{rental.ToolName} 105 | 106 |
107 | 108 | 111 | {rental.StartDate}{' - '}{rental.EndDate} 112 | 113 |
114 | 115 | {rental.Status === 'completed' && 116 | 119 | Completed 120 | 121 | } 122 | {rental.Status === 'cancelledByRenter' && 123 | 126 | Cancelled 127 | 128 | } 129 | {rental.Status === 'cancelledByOwner' && 130 | 133 | Cancelled 134 | 135 | } 136 |
137 | 138 | 139 | ) 140 | })} 141 |
142 | ) 143 | } 144 | } 145 | } 146 | 147 | export default withRouter(RentalsList); -------------------------------------------------------------------------------- /use-my-tools/src/components/Rentals/RentalsView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter, Link } from "react-router-dom" 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import Card from "@material-ui/core/Card"; 5 | import CardActions from "@material-ui/core/CardActions"; 6 | import CardContent from "@material-ui/core/CardContent"; 7 | // import Grid from "@material-ui/core/Grid"; 8 | import Button from "@material-ui/core/Button"; 9 | import Typography from "@material-ui/core/Typography"; 10 | import Paper from "@material-ui/core/Paper"; 11 | import Tabs from "@material-ui/core/Tabs"; 12 | import Tab from "@material-ui/core/Tab"; 13 | 14 | import axios from 'axios'; 15 | 16 | import RentalsList from './RentalsList'; 17 | 18 | const styles = { 19 | tabMenu: { 20 | // height: '100%', 21 | // border: '1px solid red', 22 | borderRadius: '0px', 23 | width: '100%', 24 | borderBottom: '1px gray solid', 25 | }, 26 | paper: { 27 | // height: '100%', 28 | borderRadius: '0px' 29 | }, 30 | paper2: { 31 | // height: '100%', 32 | borderRadius: '0px' 33 | }, 34 | tabs1: { 35 | height: '100%' 36 | }, 37 | tabElement: { 38 | width: '30%', 39 | minWidth: 50, 40 | maxWidth: 200 41 | }, 42 | tab: { 43 | display: 'flex', 44 | justifyContent: 'space-around' 45 | }, 46 | tabLabel: { 47 | fontSize: 16, 48 | padding: 0 49 | } 50 | } 51 | 52 | class RentalsView extends Component { 53 | constructor(props) { 54 | super(props); 55 | this.state = { 56 | activeTabIndex: 0 57 | } 58 | } 59 | 60 | handleTabSelect = (_, activeTabIndex) => this.setState({ activeTabIndex }); 61 | 62 | render() { 63 | const { classes } = this.props; 64 | const { activeTabIndex } = this.state 65 | 66 | return ( 67 |
68 | 69 |
70 | 71 | 79 | 80 | Upcoming} /> 81 | Active} /> 82 | History} /> 83 | 84 | 85 | 86 |
87 | 88 |
89 | {activeTabIndex === 0 && } 90 | {activeTabIndex === 1 && } 91 | {activeTabIndex === 2 && } 92 |
93 |
94 | ) 95 | } 96 | } 97 | 98 | export default withStyles(styles)(RentalsView); -------------------------------------------------------------------------------- /use-my-tools/src/components/Rentals/css/RentalView.css: -------------------------------------------------------------------------------- 1 | .page-container { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .title { 7 | padding: 10px; 8 | text-align: left; 9 | } 10 | 11 | .main-container { 12 | margin: 10px; 13 | height: 100%; 14 | display: flex; 15 | justify-content: space-around; 16 | } 17 | 18 | .left-container { 19 | /* border: 1px solid black; */ 20 | width: 40%; 21 | min-width: 270px; 22 | display: flex; 23 | flex-direction: column; 24 | align-items: flex-start; 25 | } 26 | 27 | .left-container .image-container { 28 | border: 1px solid black; 29 | padding: 20px; 30 | } 31 | 32 | .left-container img { 33 | border: none; 34 | height: auto; 35 | width: auto; 36 | max-width: 275px; 37 | /* margin: auto; */ 38 | } 39 | 40 | .tool-info { 41 | width: 100%; 42 | margin: 20px 10px 10px 0px; 43 | padding-top: 15px; 44 | /* border-top: 2px solid grey; */ 45 | display: flex; 46 | flex-direction: column; 47 | align-items: flex-start; 48 | justify-content: space-between; 49 | } 50 | 51 | .right-container { 52 | /* border: 1px solid black; */ 53 | display: flex; 54 | flex-direction: column; 55 | align-items: flex-start; 56 | /* height: max-content; */ 57 | width: 50%; 58 | } 59 | 60 | .rental-management { 61 | height: 100px; 62 | width: 100%; 63 | padding-top: 15px; 64 | margin: 30px 10px 10px 0px; 65 | display: flex; 66 | flex-direction: column; 67 | align-items: flex-start; 68 | justify-content: space-between; 69 | border-top: 1px solid grey; 70 | } 71 | 72 | .rating-container { 73 | width: 100%; 74 | display: flex; 75 | justify-content: flex-start; 76 | align-items: flex-end; 77 | } 78 | 79 | @media (max-width: 600px) { 80 | .main-container { 81 | flex-direction: column; 82 | align-items: space-around; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Rentals/css/RentalsList.css: -------------------------------------------------------------------------------- 1 | .rentals-list-container { 2 | /* height: 100vh; */ 3 | flex-grow: 1; 4 | overflow: scroll; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | /* background-color: #D5D5D3; */ 9 | } 10 | 11 | .rental-container { 12 | width: 90%; 13 | max-width: 800px; 14 | height: 160px; 15 | margin: 10px 0 0 0; 16 | display: flex; 17 | justify-content: space-around; 18 | border: 3px solid grey; 19 | background-color: white; 20 | text-decoration: none; 21 | } 22 | 23 | .tool-image { 24 | width: auto; 25 | height: auto; 26 | max-height: 100%; 27 | border: 0; 28 | } 29 | 30 | .rental-info { 31 | text-align: left; 32 | } -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/ConfirmRental.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from "react-router-dom"; 3 | import { withStyles } from "@material-ui/core/styles"; 4 | 5 | import Checkout from '../Billing/Checkout'; 6 | import { 7 | Grid, 8 | Card, 9 | CardContent, 10 | CardHeader, 11 | Typography, 12 | } from '@material-ui/core'; 13 | 14 | import './css/ConfirmRental.css'; 15 | 16 | import axios from 'axios'; 17 | 18 | const styles = theme => ({ 19 | cardPricing: { 20 | display: 'flex', 21 | justifyContent: 'center', 22 | marginBottom: theme.spacing.unit * 2, 23 | }, 24 | cardActions: { 25 | [theme.breakpoints.up('sm')]: { 26 | paddingBottom: theme.spacing.unit * 2, 27 | }, 28 | }, 29 | }) 30 | 31 | class ConfirmRental extends Component { 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | rental: {} 36 | }; 37 | }; 38 | 39 | componentDidMount() { 40 | const { rentalId } = this.props.match.params; 41 | const userType = 'renter'; 42 | this.getRentalInfo(rentalId, userType); 43 | }; 44 | 45 | getRentalInfo = (rentalId, userType) => { 46 | console.log('getRentalInfo called'); 47 | axios.get(`/api/rentals/${userType}/rental/${rentalId}`) 48 | .then(rental => { 49 | const dateFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }; // format options for dates 50 | // convert dates into correct format for display: 51 | const formattedStartDate = this.formatDate(rental.data.StartDate, dateFormatOptions); 52 | const formattedEndDate = this.formatDate(rental.data.EndDate, dateFormatOptions); 53 | rental.data.formattedStartDate = formattedStartDate; 54 | rental.data.formattedEndDate = formattedEndDate; 55 | this.setState({ 56 | rental: rental.data, 57 | // renterUid: tool.data.renter_uid, 58 | // brand: tool.data.brand, 59 | // name: tool.data.name, 60 | // description: tool.data.description, 61 | // price: tool.data.price, 62 | // available: tool.data.available, 63 | // rented: tool.data.rented, 64 | // rating: tool.data.rating, 65 | }, () => { 66 | console.log("ConfirmRental state.rental after getRentalInfo:", this.state.rental); 67 | }); 68 | }) 69 | .catch(error => { 70 | console.log(error.message); 71 | }) 72 | }; 73 | 74 | formatDate = (dateData, dateFormatOptions) => { 75 | const date = new Date(dateData); 76 | // console.log(date); 77 | const formattedDate = date.toLocaleDateString("en-US", dateFormatOptions); 78 | // console.log(formattedDate); 79 | return formattedDate; 80 | }; 81 | 82 | goToRentalView = () => { 83 | const { rentalId } = this.props.match.params; 84 | this.props.history.push({ 85 | pathname: `/rentalview/${rentalId}/renter` 86 | }); 87 | }; 88 | 89 | render() { 90 | const { rental } = this.state; 91 | return ( 92 |
93 | 94 |
95 | 96 | 97 | 103 | 104 |
105 |
106 | {rental.ToolImageURL ? ( 107 | tool 108 | ) : ( 109 | '' 110 | )} 111 |
112 | 113 |
114 | 115 | {rental.ToolBrand}{' '}{rental.ToolName} 116 | 117 | 118 | 119 | {rental.formattedStartDate}{' - '}{rental.formattedEndDate} 120 | 121 | 122 | 123 | Owner: {rental.OwnerFirstName}{' '}{rental.OwnerLastName} 124 | 125 |
126 |
127 | 134 |
135 |
136 |
137 |
138 | ); 139 | }; 140 | }; 141 | 142 | export default withRouter(withStyles(styles)(ConfirmRental)); -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/ConfirmRentalDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogContentText from '@material-ui/core/DialogContentText'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | 9 | // import StripeCheckout from 'react-stripe-checkout'; 10 | import Checkout from '../Billing/Checkout'; 11 | import axios from 'axios'; 12 | 13 | 14 | class ConfirmRentalDialog extends React.Component { 15 | state = { 16 | open: false, 17 | error: null 18 | }; 19 | 20 | handleClickOpen = () => { 21 | this.setState({ open: true }, () => console.log(this.props)); 22 | }; 23 | 24 | handleClose = () => { 25 | this.setState({ open: false }); 26 | }; 27 | 28 | handleConfirm = () => { 29 | // const id = this.props.toolId; 30 | // axios.delete(`/api/tools/tool/delete/${id}`) 31 | // .then(response => { 32 | // this.handleClose(); 33 | // this.props.handleToolDelete(); 34 | // }) 35 | // .catch(error => { 36 | // this.setState({ error: error.message }); 37 | // }) 38 | 39 | // create new Rental; API creates reserved dates then Rental: 40 | // axios.post('/api/rentals/newrental', reservationData) 41 | // .then(response => { 42 | // console.log('Rental created with response: ', response); 43 | // this.props.history.push({ 44 | // pathname: `/rentalview/${response.data}/renter` 45 | // }); 46 | // }) 47 | // .catch(error => { 48 | // console.log(error.message); 49 | // this.setState({ error: error.message }); 50 | // }); 51 | }; 52 | 53 | render() { 54 | 55 | return ( 56 |
57 | 60 | 61 | 66 |
67 | Confirm Rental 68 | 69 | 70 | 71 | Click the button below to enter card details: 72 | 73 | 78 | 79 | 80 | 81 | 84 | 87 | 88 |
89 |
90 | {this.state.error &&

{this.state.error}

} 91 |
92 | ); 93 | } 94 | } 95 | 96 | export default ConfirmRentalDialog; -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/ContactOwner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogContentText from '@material-ui/core/DialogContentText'; 7 | import DialogTitle from '@material-ui/core/DialogTitle'; 8 | import TextField from '@material-ui/core/TextField'; 9 | 10 | import { withFirebase } from "../Firebase"; 11 | import axios from 'axios'; 12 | 13 | class ContactOwnerBase extends React.Component { 14 | state = { 15 | open: false, 16 | renterName: '', 17 | ownerName: '', 18 | message: '', 19 | error: null 20 | }; 21 | 22 | handleClickOpen = () => { 23 | const renterUID = this.props.renterUID; // get uid of current user b/c request is going to firebase and not through built-in server auth 24 | const ownerUID = this.props.ownerUID; 25 | let renterName = null; 26 | let ownerName = null; 27 | 28 | axios.get(`/api/users/username/${renterUID}`) 29 | .then(renter => { 30 | renterName = renter.data.first_name + ' ' + renter.data.last_name; 31 | // console.log('renter name: ', renterName); 32 | 33 | axios.get(`/api/users/username/${ownerUID}`) 34 | .then(owner => { 35 | ownerName = owner.data.first_name + ' ' + owner.data.last_name; 36 | this.setState({ 37 | open: true, 38 | renterName, 39 | ownerName 40 | }, () => console.log(this.state)); 41 | // console.log('owner name: ', ownerName); 42 | }) 43 | .catch(error => { 44 | console.log(error.message); 45 | }) 46 | 47 | }) 48 | .catch(error => { 49 | console.log(error.message); 50 | }) 51 | 52 | 53 | }; 54 | 55 | handleClose = () => { 56 | this.setState({ open: false }); 57 | }; 58 | 59 | onChange = event => { 60 | this.setState({ [event.target.name]: event.target.value }); 61 | }; 62 | 63 | handleConfirm = event => { 64 | const renterUID = this.props.renterUID; // get uid of current user b/c request is going to firebase and not through built-in server auth 65 | const ownerUID = this.props.ownerUID; 66 | // create compoundUID from owner and renter uid 67 | let compoundUID = null; 68 | if (renterUID < ownerUID) { 69 | compoundUID = renterUID + ownerUID; 70 | } else { 71 | compoundUID = ownerUID + renterUID; 72 | } 73 | // console.log(compoundUID); 74 | 75 | // define convo data for conversations collection: 76 | let convoData = { 77 | // UIDOne: renterUID, 78 | // UIDTwo: ownerUID, 79 | UIDs: [renterUID, ownerUID], // store UIDs in array so that convos can be queried on the array containing a uid 80 | [renterUID]: this.state.renterName, // store name as value with uid as key so that each name is tied to the correct uid 81 | [ownerUID]: this.state.ownerName, // store name as value with uid as key so that each name is tied to the correct uid 82 | compoundUID, 83 | isOpen: true, 84 | } 85 | 86 | // add a document with id === compoundUID in the conversations collection in firestore: 87 | this.props.firebase.db 88 | .collection('conversations') 89 | .doc(`${compoundUID}`) 90 | .set(convoData, { merge: true }); // merge if there is existing doc with same id, i.e., convo already started between the two users 91 | 92 | 93 | // define message data for firestore: 94 | let timeStamp = Date.now(); 95 | let messageData = { 96 | authorUID: renterUID, 97 | recipientUID: ownerUID, 98 | content: this.state.message, 99 | timeSent: timeStamp 100 | } 101 | // add a messages collection to the new conversation and 102 | // add a document to the messages collection with id === timestamp and content === state.message 103 | this.props.firebase.db 104 | .collection('conversations') 105 | .doc(compoundUID) 106 | .collection('messages') 107 | .doc(`${timeStamp}`) 108 | .set(messageData); 109 | 110 | this.setState({ message: '', open: false }) 111 | 112 | event.preventDefault(); 113 | }; 114 | 115 | render() { 116 | 117 | return ( 118 |
119 | 122 | 123 | 128 | 129 |
130 | Contact Owner 131 | 132 | 133 | 134 | Write a message to the owner of this tool 135 | 136 | 148 | 149 | 150 | 151 | 154 | 157 | 158 |
159 | 160 |
161 | {this.state.error &&

{this.state.error}

} 162 |
163 | ); 164 | } 165 | } 166 | 167 | const ContactOwner = withFirebase(ContactOwnerBase); 168 | 169 | 170 | export default ContactOwner; -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/FilterMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import './css/FilterMenu.css'; 4 | 5 | const FilterMenu = props => { 6 | 7 | 8 | // handleChange = event => { 9 | // this.setState({ 10 | // [event.target.name]: event.target.value 11 | // }); 12 | // } 13 | 14 | const handleUpdateFilter = name => event => { 15 | const value = props[name]; 16 | //console.log(value); 17 | props.updateFilter(name, value); 18 | event.preventDefault(); 19 | } 20 | 21 | // handleClearAllKeywords = event => { 22 | // // this.props.clearAllKeywords(); 23 | // this.setState({ searchString: null }, () => this.props.clearAllKeywords()); 24 | // event.preventDefault(); 25 | // } 26 | 27 | 28 | 29 | return ( 30 |
31 |

Filter Results

32 | 33 | 34 |
38 | 39 | 40 | 41 | 49 | 50 | 56 | 57 |
58 | 59 |
63 | 64 | 65 | 66 | 74 | 75 | 81 | 82 | 89 |
90 | 91 | 92 |
93 | ) 94 | 95 | } 96 | 97 | export default FilterMenu; 98 | 99 | // class FilterMenu extends Component { 100 | // constructor(props) { 101 | // super(props); 102 | // this.state = { 103 | // // currentMaxPriceInput: 100, 104 | // // currentKeywordInput: '', 105 | // maxPrice: 100, 106 | // searchString: null, 107 | // // keywords: [] 108 | // }; 109 | // } 110 | 111 | // handleChange = event => { 112 | // this.setState({ 113 | // [event.target.name]: event.target.value 114 | // }); 115 | // } 116 | 117 | // handleUpdateFilter = name => event => { 118 | // const value = this.state[name]; 119 | // //console.log(value); 120 | // this.props.updateFilter(name, value); 121 | // event.preventDefault(); 122 | // } 123 | 124 | // handleClearAllKeywords = event => { 125 | // // this.props.clearAllKeywords(); 126 | // this.setState({ searchString: null }, () => this.props.clearAllKeywords()); 127 | // event.preventDefault(); 128 | // } 129 | 130 | 131 | // render() { 132 | // return ( 133 | //
134 | //

Filter Results

135 | 136 | 137 | //
138 | 139 | // 140 | 141 | // 149 | 150 | // 151 | //
152 | 153 | //
154 | 155 | // 156 | 157 | // 165 | 166 | // 173 | 174 | // 181 | //
182 | 183 | 184 | //
185 | // ) 186 | // } 187 | // } 188 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/FindTools.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from "react-router-dom" 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import Grid from "@material-ui/core/Grid"; 5 | 6 | import FilterMenu from './FilterMenu'; 7 | import ToolCard from '../ToolCard'; 8 | import './css/FindTools.css'; 9 | 10 | import axios from 'axios'; 11 | 12 | const styles = theme => ({ 13 | gridContainer: { 14 | 15 | }, 16 | card: { 17 | height: "100%", 18 | width: "100%", 19 | display: "flex", 20 | flexDirection: "column" 21 | }, 22 | cardContent: { 23 | // flexGrow: 1, 24 | maxHeight: 100, 25 | // minHeight: 100, 26 | overflow: "hidden", 27 | textAlign: "left" 28 | }, 29 | cardActions: { 30 | flexGrow: 1, 31 | alignItems: "flex-end" 32 | }, 33 | cardTitle: { 34 | fontWeight: "bold" 35 | } 36 | 37 | }) 38 | 39 | class FindTools extends Component { 40 | constructor(props) { 41 | super(props); 42 | this.state = { 43 | tools: [], 44 | maxPriceInput: 100, 45 | maxPrice: 100, 46 | searchString: '', 47 | keywords: [] 48 | }; 49 | } 50 | 51 | componentDidMount() { 52 | // Get available tools in the renter's city: 53 | const criteria = { city: 'renter' }; 54 | axios.post('/api/tools/findtools', criteria) 55 | .then(tools => { 56 | this.setState({ 57 | tools: tools.data 58 | }, () => console.log('FindTools state.tools after GET /findtools: ', this.state.tools)) ; 59 | }) 60 | .catch(error => { 61 | console.log(error.message); 62 | }) 63 | } 64 | 65 | handleFilterInputChange = event => { 66 | this.setState({ 67 | [event.target.name]: event.target.value 68 | }, () => {console.log(this.state)}); 69 | } 70 | 71 | // Method to update max price and search/keyword filter based on input: 72 | updateFilter = (name, value) => { 73 | // if updating the keywords filter with a search string, 74 | // split the string and put each word into the state.keywords array: 75 | if (name === 'searchString') { 76 | let keywords = []; 77 | let searchString = value; 78 | // Return if searchString is null: 79 | if (searchString === null || searchString === '') { 80 | this.setState({ keywords: [], searchString: '' }); 81 | return; 82 | } 83 | // If searchString is not null, 84 | // split searchString into words 85 | // and push each word in lowerCase into state.keywords array: 86 | for (let word of searchString.split(' ')) { 87 | keywords.push(word.toLowerCase()); 88 | } 89 | // console.log(keywords); 90 | this.setState({ keywords }) 91 | } else if (name === 'maxPriceInput') { 92 | this.setState({ maxPrice: value }, () => {console.log(this.state.maxPrice)}); 93 | } 94 | } 95 | 96 | clearAllKeywords = event => { 97 | this.setState({ keywords: [], searchString: ''}); 98 | } 99 | 100 | render() { 101 | const { classes } = this.props; 102 | const { tools, maxPrice, keywords } = this.state; 103 | 104 | const filteredTools = tools.filter(tool => 105 | tool.price <= maxPrice 106 | && ( 107 | keywords.length === 0 108 | || tool.brand.split(' ').some(word => keywords.indexOf(word.toLowerCase()) >= 0) 109 | || tool.name.split(' ').some(word => keywords.indexOf(word.toLowerCase()) >= 0) 110 | || tool.description.split(' ').some(word => keywords.indexOf(word.toLowerCase()) >= 0) 111 | ) 112 | ); 113 | 114 | return ( 115 |
116 |

Showing Tools in Your City:

117 | 118 |
119 | 120 |
121 | 128 |
129 | 130 |
131 | 132 | 133 | 134 | {filteredTools.map((tool, index) => { 135 | return ( 136 | 137 | 138 | 139 | ); 140 | })} 141 | 142 | 143 | 144 |
145 |
146 |
147 | 148 | ); 149 | }; 150 | }; 151 | 152 | export default withRouter(withStyles(styles)(FindTools)); -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/RenterDashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import RentalsView from '../Rentals/RentalsView'; 4 | 5 | import './css/RenterDashboard.css'; 6 | 7 | const RenterDashboard = props => { 8 | return ( 9 | 10 | ) 11 | } 12 | 13 | export default RenterDashboard; -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/ToolViewRenter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter, Route, BrowserRouter as Router } from "react-router-dom"; 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import Typography from "@material-ui/core/Typography"; 5 | 6 | import ImageCarousel from '../ImageCarousel'; 7 | import RequestDates from '../RequestTool/RequestDates'; 8 | import ContactOwner from './ContactOwner.js'; 9 | // import ConfirmRental from './ConfirmRental'; 10 | 11 | 12 | import axios from 'axios'; 13 | 14 | import './css/ToolViewRenter.css'; 15 | 16 | const styles = theme => ({ 17 | 18 | }) 19 | 20 | class ToolViewRenter extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | tool: {} 25 | }; 26 | } 27 | 28 | componentDidMount() { 29 | let tool_id = this.props.match.params.id; 30 | this.getToolInfo(tool_id); 31 | console.log(this.state.tool); 32 | } 33 | 34 | getToolInfo = tool_id => { 35 | axios.get(`/api/tools/renter/singletool/${tool_id}`) 36 | .then(tool => { 37 | this.setState({ 38 | tool: tool.data 39 | }); 40 | }) 41 | .catch(error => { 42 | console.log(error.message); 43 | }) 44 | } 45 | 46 | render() { 47 | const { tool } = this.state; 48 | const toolId = this.props.match.params.id; 49 | 50 | return ( 51 |
52 |
53 |
54 |
55 | 56 | {tool.brand}{' '}{tool.name} 57 | 58 |
59 | {tool.images ? ( 60 | 61 | ) : ( 62 | '' 63 | )} 64 |
65 | 66 |
67 |
68 | 69 | Description 70 | 71 | 72 | {tool.description} 73 | 74 |
75 | 76 | Rating: {tool.rating} Stars 77 | 78 |
79 | 80 | Location: {tool.ownerCity}{', '}{tool.ownerState} 81 | 82 |
83 | 84 | Daily rental price: ${tool.price} 85 | 86 |
87 | 88 |
89 | 90 | {tool.id && 91 | 92 | } 93 | 94 |
95 | 96 | 97 |
98 |
99 | 100 |
101 | ) 102 | } 103 | } 104 | 105 | export default withRouter(withStyles(styles)(ToolViewRenter)); -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/css/ConfirmRental.css: -------------------------------------------------------------------------------- 1 | .confirm-rental-panel { 2 | width: 400px; 3 | margin: 30px auto 30px auto; 4 | } 5 | 6 | .confirm-rental-panel .rental-info-container { 7 | width: 100%; 8 | } 9 | 10 | .confirm-rental-panel .rental-info-container .image-container { 11 | width: 80%; 12 | } 13 | 14 | .confirm-rental-panel .rental-info-container .image-container img { 15 | width: auto; 16 | height: auto; 17 | max-height: 250px; 18 | } -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/css/FilterMenu.css: -------------------------------------------------------------------------------- 1 | .filter-menu { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | } 6 | 7 | .filter-form-container { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: flex-start; 11 | margin-bottom: 15px; 12 | } 13 | 14 | .filter-label { 15 | margin-bottom: 5px; 16 | } 17 | 18 | .filter-input { 19 | margin-bottom: 5px; 20 | } 21 | 22 | .filter-button { 23 | margin-bottom: 5px; 24 | } 25 | 26 | @media (max-width: 700px) { 27 | .filter-menu { 28 | display: flex; 29 | flex-direction: row; 30 | justify-content: space-around; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/css/FindTools.css: -------------------------------------------------------------------------------- 1 | .main-container { 2 | display: flex; 3 | flex-direction: space-between; 4 | } 5 | 6 | .filter-menu-container { 7 | border-right: 2px solid grey; 8 | width: 15%; 9 | min-width: 150px; 10 | } 11 | 12 | .tools-list-container { 13 | /* margin-left: 20px; */ 14 | width: 80%; 15 | margin: 20px 10px 20px 10px; 16 | } 17 | 18 | .grid-item { 19 | width: 25%; 20 | } 21 | 22 | @media (max-width: 700px) { 23 | .main-container { 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | 28 | .filter-menu-container { 29 | border-right: none; 30 | border-bottom: 2px solid grey; 31 | width: 100%; 32 | min-width: 150px; 33 | } 34 | 35 | .tools-grid { 36 | margin: auto; 37 | width: 100%; 38 | } 39 | 40 | .grid-item { 41 | width: 45%; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/css/RenterDashboard.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fhrryDeveloper/eCommerce-React/c627dd381e34721b5836c53b23563b1ec37200fd/use-my-tools/src/components/Renter/css/RenterDashboard.css -------------------------------------------------------------------------------- /use-my-tools/src/components/Renter/css/ToolViewRenter.css: -------------------------------------------------------------------------------- 1 | /* .pageContainer { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: 950px; 5 | } */ 6 | 7 | .tool-title-container { 8 | padding: 10px; 9 | text-align: left; 10 | } 11 | 12 | .mainContainer { 13 | margin: 10px; 14 | height: 100%; 15 | display: flex; 16 | justify-content: space-between; 17 | } 18 | 19 | .leftContainer { 20 | width: 40%; 21 | min-width: 270px; 22 | } 23 | 24 | .rightContainer { 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: space-between; 28 | align-items: flex-start; 29 | width: 50%; 30 | padding-top: 10px; 31 | } 32 | 33 | .toolInfo { 34 | text-align: left; 35 | flex-grow: 1; 36 | /* max-height: 100px; 37 | min-height: 100px; */ 38 | overflow: hidden; 39 | } 40 | 41 | @media (max-width: 600px) { 42 | .mainContainer { 43 | flex-direction: column; 44 | justify-content: space-around; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /use-my-tools/src/components/Routes/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route } from "react-router-dom"; 3 | import LoginPage from '../LoginPage'; 4 | 5 | export default function PrivateRoute({ 6 | component: Component, 7 | authenticated, 8 | ...rest 9 | }) { 10 | // console.log('PrivateRoute authenticated: ' + authenticated); 11 | return ( 12 | 15 | authenticated === true ? ( 16 | 17 | ) : ( 18 | // 19 | 20 | ) 21 | } 22 | /> 23 | ); 24 | } -------------------------------------------------------------------------------- /use-my-tools/src/components/ToolCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter, Link } from "react-router-dom" 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import Card from "@material-ui/core/Card"; 5 | import CardActions from "@material-ui/core/CardActions"; 6 | import CardContent from "@material-ui/core/CardContent"; 7 | import Grid from "@material-ui/core/Grid"; 8 | import Button from "@material-ui/core/Button"; 9 | import Typography from "@material-ui/core/Typography"; 10 | 11 | const styles = theme => ({ 12 | card: { 13 | height: "100%", 14 | width: "100%", 15 | display: "flex", 16 | flexDirection: "column" 17 | }, 18 | cardContent: { 19 | // flexGrow: 1, 20 | maxHeight: 100, 21 | // minHeight: 100, 22 | overflow: "hidden", 23 | textAlign: "left" 24 | }, 25 | cardActions: { 26 | flexGrow: 1, 27 | alignItems: "flex-end" 28 | }, 29 | cardTitle: { 30 | fontWeight: "bold" 31 | }, 32 | toolImage: { 33 | width: "100%", 34 | border: "2px solid grey", 35 | borderradius: "2px" 36 | } 37 | 38 | }); 39 | 40 | const ToolCard = props => { 41 | const { classes, tool, userType } = props; 42 | return ( 43 | 44 | 45 | tool 46 | 47 | 48 | {tool.brand}{' '}{tool.name} 49 | 50 | 51 | ${tool.price} / day 52 | 53 | 54 | 55 | 64 | 65 | 66 | 67 | ) 68 | }; 69 | 70 | export default withRouter(withStyles(styles)(ToolCard)); -------------------------------------------------------------------------------- /use-my-tools/src/components/UpdatePassword.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withFirebase } from "./Firebase"; 3 | import { withRouter } from "react-router-dom" 4 | import { FirebaseContext } from './Firebase'; 5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | import TextField from 'material-ui/TextField'; 8 | 9 | // import axios from 'axios'; 10 | 11 | import './css/UpdatePassword.css'; 12 | 13 | const UpdatePasswordPage = () => ( 14 |
15 | 16 | {firebase => } 17 | 18 |
19 | ); 20 | 21 | class UpdatePasswordFormBase extends React.Component { 22 | constructor(props){ 23 | super(props); 24 | this.state = { 25 | email: "", 26 | oldPassword: "", 27 | newPassword1: "", 28 | newPassword2: "", 29 | error: null, 30 | status: "Enter current credentials and new password." 31 | } 32 | } 33 | 34 | onChange = event => { 35 | this.setState({ [event.target.name]: event.target.value }); 36 | }; 37 | 38 | onSubmit = event => { 39 | const { email, oldPassword, newPassword1 } = this.state; 40 | this.props.firebase 41 | .doSignInWithEmailAndPassword(email, oldPassword) 42 | .then(signInResponse => { 43 | console.log('Sign in response: ', signInResponse); 44 | this.props.firebase.doPasswordUpdate(newPassword1) 45 | .then(updateResponse => { 46 | console.log("Update response: ", updateResponse); 47 | this.setState({ 48 | email: "", 49 | oldPassword: "", 50 | newPassword1: "", 51 | newPassword2: "", 52 | status: "Your password was updated." 53 | }); 54 | }) 55 | .catch(error => { // if updatePassword throws error 56 | console.log(error.message); 57 | this.setState({ 58 | error: error 59 | }); 60 | }) 61 | }) 62 | .catch(error => { // if signIn throws error 63 | console.log(error.message); 64 | this.setState({ 65 | error: error 66 | }); 67 | }) 68 | event.preventDefault(); 69 | }; 70 | 71 | render() { 72 | const { email, oldPassword, newPassword1, newPassword2, error } = this.state; 73 | 74 | const condition = email === '' || oldPassword === '' || oldPassword === newPassword1 || newPassword1 === '' || newPassword1 !== newPassword2; 75 | return ( 76 |
77 | 78 | 79 |
80 |
81 |
82 |
{this.state.status}
83 | 92 |
93 | 94 | 103 |
104 | 105 | 114 |
115 | 116 | 125 |
126 | 133 | {error &&

{error.message}

} 134 | 135 |
136 |
137 |
138 | ) 139 | } 140 | } 141 | 142 | const UpdatePasswordForm = withRouter(withFirebase(UpdatePasswordFormBase)); 143 | 144 | export default UpdatePasswordPage; 145 | 146 | export { UpdatePasswordForm }; 147 | -------------------------------------------------------------------------------- /use-my-tools/src/components/css/AccountPage.css: -------------------------------------------------------------------------------- 1 | .account-page-container { 2 | /* background-color: #EAEAEA; */ 3 | height: 100%; 4 | margin: auto; 5 | padding-left: 10px; 6 | padding-right: 10px; 7 | display: flex; 8 | align-items: flex-start; 9 | justify-content: space-between; 10 | } 11 | 12 | .account-page-container .left-container { 13 | display: flex; 14 | flex-direction: column; 15 | width: 45%; 16 | /* height: 100%; */ 17 | } 18 | 19 | .account-page-container .left-container form { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: flex-start; 23 | height: 100%; 24 | width: 200px; 25 | padding-bottom: 20px; 26 | } 27 | 28 | .account-page-container .left-container button { 29 | margin-top: 10px; 30 | margin-left: 10px; 31 | width: max-content; 32 | /* height: 8%; */ 33 | align-self: flex-start; 34 | } 35 | 36 | .account-page-container .left-container .account-links-container { 37 | margin-top: 20px; 38 | display: flex; 39 | flex-direction: column; 40 | align-items: flex-start; 41 | } 42 | 43 | .account-page-container .left-container .account-links-container .account-link-button { 44 | margin: 10px; 45 | } 46 | 47 | .location-input { 48 | margin-left: 10px; 49 | width: 200px; 50 | } 51 | 52 | .account-page-container .right-container { 53 | width: 35%; 54 | display: flex; 55 | flex-direction: column; 56 | align-items: center; 57 | } 58 | 59 | .account-page-container .right-container .profile-image-container { 60 | width: 100%; 61 | display: flex; 62 | flex-direction: column; 63 | align-items: center; 64 | } 65 | 66 | .profile-image-container form { 67 | display: flex; 68 | justify-content: flex-start; 69 | } 70 | 71 | .account-page-container .right-container .profile-image-container img { 72 | border-radius: 50%; 73 | height: auto; 74 | width: auto; 75 | max-width: 150px; 76 | } 77 | 78 | @media (max-width: 600px) { 79 | .account-page-container { 80 | flex-direction: column-reverse; 81 | } 82 | 83 | .account-page-container .left-container { 84 | width: 100%; 85 | } 86 | 87 | .account-page-container .right-container { 88 | width: 100%; 89 | align-items: flex-start; 90 | } 91 | 92 | .account-page-container .right-container .profile-image-container { 93 | display: flex; 94 | flex-direction: column; 95 | align-items: flex-start; 96 | } 97 | .account-page-container .right-container .profile-image-container img { 98 | border-radius: 50%; 99 | height: auto; 100 | width: auto; 101 | max-width: 100px; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /use-my-tools/src/components/css/LandingPage.css: -------------------------------------------------------------------------------- 1 | .landingpage { 2 | /* max-width: 1000px; */ 3 | margin: auto; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .landingpage .nav-bar { 9 | display: flex; 10 | justify-content: flex-end; 11 | height: 70px; 12 | background-color: #3E6CAA; 13 | } 14 | 15 | .landingpage .nav-bar .link-container { 16 | display: flex; 17 | flex-direction: space-between; 18 | align-items: center; 19 | } 20 | 21 | .landingpage .signup-button { 22 | height: 40px; 23 | font-size: 20px; 24 | border-radius: 5px; 25 | background-color: #77BF44; 26 | border: 1px solid grey; 27 | } 28 | 29 | .nav-link { 30 | margin: 0px 0px 0px 0px; 31 | } 32 | 33 | .title-text { 34 | color: white; 35 | font-size: 40px; 36 | } 37 | 38 | .subtitle-text { 39 | color: white; 40 | font-size: 25px; 41 | } 42 | 43 | .landingpage .welcome-section { 44 | height: 500px; 45 | width: 100%; 46 | margin-bottom: 50px; 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | background-image: url("../../assets/images/Tools-on-table-brown.jpeg"); 51 | background-repeat: no-repeat; 52 | background-size: cover; 53 | } 54 | 55 | .landingpage .intro-section { 56 | display: flex; 57 | flex-direction: column; 58 | } 59 | 60 | .landingpage .intro-section .intro-row { 61 | display: flex; 62 | justify-content: space-between; 63 | margin-bottom: 30px; 64 | } 65 | 66 | .landingpage .intro-section .image-right { 67 | flex-direction: row-reverse; 68 | } 69 | 70 | .landingpage .intro-section .intro-row .intro-image-container { 71 | width: 47%; 72 | } 73 | 74 | .landingpage .intro-section .intro-row .intro-image-container .intro-image { 75 | height: auto; 76 | max-width: 100%; 77 | } 78 | 79 | .landingpage .intro-section .intro-row .intro-text-container { 80 | width: 45%; 81 | margin: 0px 10px 0px 10px; 82 | text-align: left; 83 | display: flex; 84 | flex-direction: column; 85 | align-items: flex-start; 86 | justify-content: center; 87 | } 88 | 89 | @media (max-width: 700px) { 90 | .landingpage .intro-section .intro-row { 91 | flex-direction: column; 92 | margin-bottom: 30px; 93 | } 94 | 95 | .landingpage .intro-section .intro-row .intro-image-container { 96 | width: 100%; 97 | overflow: hidden; 98 | } 99 | 100 | .landingpage .intro-section .intro-row .intro-image-container .intro-image { 101 | height: auto; 102 | width: 100%; 103 | margin: -20% 0 -10% 0; 104 | } 105 | 106 | .landingpage .intro-section .intro-row .intro-text-container { 107 | width: 95%; 108 | margin: 10px 10px 10px 10px; 109 | display: flex; 110 | flex-direction: row; 111 | align-items: center; 112 | justify-content: space-between; 113 | } 114 | 115 | .landingpage .intro-section .intro-row .intro-text-container h3:first-of-type { 116 | margin-right: 25px; 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /use-my-tools/src/components/css/LocationSearchInput.css: -------------------------------------------------------------------------------- 1 | .location-search-input { 2 | width: 100%; 3 | font-size: 16px; 4 | line-height: 24px; 5 | 6 | } 7 | 8 | .suggestion { 9 | background-color: #f1f1f1; 10 | color: gray; 11 | font-size: 80%; 12 | padding: 0.25em 0.7em; 13 | text-align: left; 14 | } -------------------------------------------------------------------------------- /use-my-tools/src/components/css/LoginPage.css: -------------------------------------------------------------------------------- 1 | .login { 2 | /* background-color: #eaeaea; */ 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | } 8 | 9 | .login form { 10 | height: max-content; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-evenly; 14 | align-items: center; 15 | border: 1px lightgray solid; 16 | width: 380px; 17 | margin: 0; 18 | background-color: white; 19 | padding-top: 1%; 20 | padding-bottom: 1%; 21 | } 22 | 23 | .login form .login-button { 24 | margin-top: 5%; 25 | } 26 | 27 | .login form p { 28 | color: gray; 29 | font-size: .8em; 30 | /* padding: 5% 25% 5% 25%; */ 31 | } 32 | 33 | .login form .sign-up-container { 34 | width: 60%; 35 | display: flex; 36 | justify-content: space-around; 37 | align-items: center; 38 | } 39 | 40 | .login form .sign-up-container .sign-up-link { 41 | color: #4c7af1; 42 | text-decoration: none; 43 | padding: 3px; 44 | border-radius: 3px; 45 | } 46 | 47 | .login form .sign-up-container .sign-up-link:hover { 48 | background-color: #4c7af1; 49 | color: white; 50 | } 51 | 52 | @media (max-width: 800px) { 53 | .login { 54 | height: 100%; 55 | } 56 | .login form { 57 | margin-bottom: 15%; 58 | padding-bottom: 5%; 59 | } 60 | .login .login-top-bar img { 61 | margin-left: 15%; 62 | } 63 | .login .header { 64 | margin-top: 15%; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /use-my-tools/src/components/css/Logout.css: -------------------------------------------------------------------------------- 1 | .log-out-link { 2 | text-decoration: none; 3 | color: black; 4 | } 5 | 6 | .log-out-link:hover { 7 | color: white; 8 | } 9 | -------------------------------------------------------------------------------- /use-my-tools/src/components/css/RegisterPage.css: -------------------------------------------------------------------------------- 1 | .register { 2 | /* background-color: #eaeaea; */ 3 | height: auto; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | } 8 | 9 | .register form { 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: space-evenly; 13 | align-items: center; 14 | border: 1px solid lightgray; 15 | width: 380px; 16 | margin: auto; 17 | background-color: white; 18 | padding-top: 1%; 19 | padding-bottom: 1%; 20 | } 21 | 22 | .register form .register-button { 23 | margin-top: 5%; 24 | } 25 | 26 | .register form p { 27 | color: gray; 28 | font-size: .8em; 29 | /* padding: 5% 25% 3% 25%; */ 30 | } 31 | 32 | .register form p:last-child { 33 | color: gray; 34 | font-size: .9em; 35 | padding: 5% 25% 3% 25%; 36 | } 37 | 38 | .location-input { 39 | width: 65%; 40 | } 41 | 42 | .register form .sign-in-container { 43 | width: 60%; 44 | display: flex; 45 | justify-content: space-around; 46 | align-items: center; 47 | } 48 | 49 | .register form .sign-in-container .sign-in-link { 50 | color: #4c7af1; 51 | text-decoration: none; 52 | padding: 3px; 53 | border-radius: 3px; 54 | } 55 | 56 | .register form .sign-in-container .sign-in-link:hover { 57 | background-color: #4c7af1; 58 | color: white; 59 | } 60 | 61 | @media (max-width: 800px) { 62 | .register { 63 | height: 100%; 64 | } 65 | .register form { 66 | margin-bottom: 15%; 67 | padding-bottom: 5%; 68 | } 69 | .register .register-top-bar img { 70 | margin-left: 15%; 71 | } 72 | .register .header { 73 | margin-top: 15%; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /use-my-tools/src/components/css/UpdatePassword.css: -------------------------------------------------------------------------------- 1 | .update-password { 2 | background-color: #EAEAEA; 3 | height: 100vh; 4 | } 5 | 6 | .update-password .header { 7 | font-size: 2em; 8 | margin-bottom: 3%; 9 | } 10 | 11 | .update-password form { 12 | height: max-content; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: space-arounds; 16 | align-items: center; 17 | border: 1px lightgray solid; 18 | width: 380px; 19 | margin: auto; 20 | background-color: white; 21 | padding-top: 1%; 22 | padding-bottom: 1%; 23 | } 24 | 25 | .update-password form .update-password-button { 26 | margin-top: 5%; 27 | } 28 | 29 | .update-password form a { 30 | color: #4C7AF1; 31 | font-size: .9em; 32 | padding-bottom: 5%; 33 | } 34 | 35 | @media (max-width: 800px) { 36 | 37 | .update-password { 38 | margin: auto; 39 | align-items: center; 40 | width: 100%; 41 | height: 100%; 42 | padding-top: 0; 43 | padding-bottom: 18%; 44 | } 45 | 46 | .update-password form { 47 | width: 380px; 48 | margin: auto; 49 | margin-top: 15%; 50 | margin-bottom: 15%; 51 | padding-top: 5%; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /use-my-tools/src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | min-height: 100%; 4 | } 5 | 6 | *, 7 | *:before, 8 | *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body { 13 | min-height: 100%; 14 | margin: 0; 15 | padding: 0; 16 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", 17 | "Droid Sans", "Helvetica Neue", sans-serif; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | code { 23 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 24 | } 25 | -------------------------------------------------------------------------------- /use-my-tools/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import { BrowserRouter as Router } from "react-router-dom"; 7 | import Firebase, { FirebaseContext } from './components/Firebase'; 8 | import axios from 'axios'; 9 | 10 | axios.defaults.baseURL = 11 | process.env.NODE_ENV === 'production' 12 | ? 'https://use-my-tools-csr.herokuapp.com/' 13 | : 'http://localhost:5000'; 14 | 15 | 16 | ReactDOM.render( 17 | 18 | 19 | 20 | 21 | , 22 | document.getElementById('root') 23 | ); 24 | 25 | // If you want your app to work offline and load faster, you can change 26 | // unregister() to register() below. Note this comes with some pitfalls. 27 | // Learn more about service workers: https://bit.ly/CRA-PWA 28 | serviceWorker.unregister(); 29 | -------------------------------------------------------------------------------- /use-my-tools/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /use-my-tools/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /use-my-tools/src/store.js: -------------------------------------------------------------------------------- 1 | // export const initialState = { 2 | // uid: null 3 | // } 4 | 5 | // export const reducer = (state, action) => { 6 | // switch (action.type) { 7 | // case "logIn": 8 | // return 9 | // } 10 | // } --------------------------------------------------------------------------------