diff --git a/README.md b/README.md index 76d6d581..a5894fbb 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,18 @@ -# Welcome to the Integrating With HubSpot I: Foundations Practicum +# Integrating With HubSpot I Practicum -This repository is for the Integrating With HubSpot I: Foundations course. This practicum is one of two requirements for receiving your Integrating With HubSpot I: Foundations certification. You must also take the exam and receive a passing grade (at least 75%). +This project demonstrates integration with HubSpot's API to create and manage a custom "Guitar" object. -To read the full directions, please go to the [practicum instructions](https://app.hubspot.com/academy/l/tracks/1092124/1093824/5493?language=en). +## Custom Object Link +[View My Guitars Custom Object](https://app.hubspot.com/contacts/39596821/objects/2-43176702/views/all/list) -**Put your HubSpot developer test account custom objects URL link here:** https://app.hubspot.com/contacts/l/objects/${custom-obj-number}/views/all/list +## Features +- Display all guitars in a table format +- Add new guitars through a form +- Integration with HubSpot CRM using Private App -___ -## Tips: -- Commit to your repository often. Even if you make small tweaks to your code, it’s best to be committing to your repository frequently. -- The subject of the custom object is up to you. Feel free to get creative! -- Please create a test account and include your private app access token in your repo. -- Ensure you re-merge any working branches into the main branch. -- DO NOT ADD YOUR PRIVATE APP TOKEN TO YOUR REPOSITORY. - -## Pre-requisites: -- Using [Node](https://nodejs.org/en/download) and node packages -- Using [Express](https://expressjs.com/en/starter/installing.html) -- Using [Axios](https://axios-http.com/docs/intro) -- Using [Pug templating system](https://pugjs.org/api/getting-started.html) -- Using the command line -- Using [Git and GitHub](https://product.hubspot.com/blog/git-and-github-tutorial-for-beginners) - -## Requirements -- All work must be your own. During the grading process we will check the revision history. Submissions that do not meet this requirement will not be considered. -- You must have at least two new routes in your index.js file and one new pug template for the homepage. -- You must create a developer test account and link to it in your README.md file. Submissions that do not meet this requirement will not be considered. +## Setup +1. Clone this repository +2. Create a .env file with your HubSpot Private App token and object type +3. Run `npm install` +4. Run `node index.js` +5. Visit http://localhost:3000 in your browser \ No newline at end of file diff --git a/index.js b/index.js index f337a32d..ec72efdb 100644 --- a/index.js +++ b/index.js @@ -1,71 +1,91 @@ +// Load environment variables +require('dotenv').config(); + +// Import required packages const express = require('express'); const axios = require('axios'); +const path = require('path'); + +// Initialize Express const app = express(); +// Set up middleware app.set('view engine', 'pug'); -app.use(express.static(__dirname + '/public')); +app.set('views', path.join(__dirname, 'views')); +app.use(express.static('public')); app.use(express.urlencoded({ extended: true })); -app.use(express.json()); - -// * Please DO NOT INCLUDE the private app access token in your repo. Don't do this practicum in your normal account. -const PRIVATE_APP_ACCESS = ''; - -// TODO: ROUTE 1 - Create a new app.get route for the homepage to call your custom object data. Pass this data along to the front-end and create a new pug template in the views folder. - -// * Code for Route 1 goes here - -// TODO: ROUTE 2 - Create a new app.get route for the form to create or update new custom object data. Send this data along in the next route. - -// * Code for Route 2 goes here -// TODO: ROUTE 3 - Create a new app.post route for the custom objects form to create or update your custom object data. Once executed, redirect the user to the homepage. - -// * Code for Route 3 goes here - -/** -* * This is sample code to give you a reference for how you should structure your calls. - -* * App.get sample -app.get('/contacts', async (req, res) => { - const contacts = 'https://api.hubspot.com/crm/v3/objects/contacts'; - const headers = { - Authorization: `Bearer ${PRIVATE_APP_ACCESS}`, - 'Content-Type': 'application/json' - } - try { - const resp = await axios.get(contacts, { headers }); - const data = resp.data.results; - res.render('contacts', { title: 'Contacts | HubSpot APIs', data }); - } catch (error) { - console.error(error); - } +// Get environment variables +const PRIVATE_APP_TOKEN = process.env.PRIVATE_APP_TOKEN; +const OBJECT_TYPE = "2-43176702"; // Replace with your actual object type + +// API base URL +const API_BASE_URL = 'https://api.hubapi.com'; + +// Set up headers for API requests +const headers = { + 'Authorization': `Bearer ${PRIVATE_APP_TOKEN}`, + 'Content-Type': 'application/json' +}; + +// Home page route - List all guitars +app.get('/', async (req, res) => { + try { + // Make API call to get all guitar records + const response = await axios.get( + `${API_BASE_URL}/crm/v3/objects/${OBJECT_TYPE}?properties=name,year,description`, + { headers } + ); + + // Render homepage with guitar data + res.render('homepage', { + guitars: response.data.results + }); + } catch (error) { + console.error('Error fetching guitar data:', error.response ? error.response.data : error.message); + res.status(500).send('Error fetching guitar data. Check console for details.'); + } }); -* * App.post sample -app.post('/update', async (req, res) => { - const update = { - properties: { - "favorite_book": req.body.newVal - } - } - - const email = req.query.email; - const updateContact = `https://api.hubapi.com/crm/v3/objects/contacts/${email}?idProperty=email`; - const headers = { - Authorization: `Bearer ${PRIVATE_APP_ACCESS}`, - 'Content-Type': 'application/json' - }; - - try { - await axios.patch(updateContact, update, { headers } ); - res.redirect('back'); - } catch(err) { - console.error(err); - } - +// Form page route - Display the form to add a new guitar +app.get('/update-cobj', (req, res) => { + res.render('updates', { + title: 'Update Custom Object Form | Integrating With HubSpot I Practicum' + }); }); -*/ +// Form submission route - Process the form data +app.post('/update-cobj', async (req, res) => { + try { + // Get form data + const { name, year, description } = req.body; + + // Prepare data for API call + const data = { + properties: { + name, + year, + description + } + }; + + // Make API call to create new guitar + await axios.post( + `${API_BASE_URL}/crm/v3/objects/${OBJECT_TYPE}`, + data, + { headers } + ); + + // Redirect to homepage after successful creation + res.redirect('/'); + } catch (error) { + console.error('Error creating guitar:', error.response ? error.response.data : error.message); + res.status(500).send('Error creating guitar. Check console for details.'); + } +}); -// * Localhost -app.listen(3000, () => console.log('Listening on http://localhost:3000')); \ No newline at end of file +// Start the server +const PORT = 3000; +app.listen(PORT, () => { + console.log(`Server running at http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index 85587bb4..8b839264 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,58 +1,67 @@ -@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); - -body, * { - font-family: 'Roboto', sans-serif; +body { + font-family: Arial, sans-serif; margin: 0; - padding: 0; -} - -h1 { - margin: 1rem; -} - -.cards { - display: flex; - align-items: center; - justify-content: space-evenly; - flex-wrap: wrap; -} - -.card { - flex-basis: 31%; - margin: 1rem 0.4rem; - padding: 1rem 0.4rem 2.5rem 0.4rem; - border: solid 1px lightgrey; - border-radius: 15px; - box-shadow: 3px 2px 6px lightgrey; -} - -.card__email { - font-size: 1rem; -} - -.form-wrapper { - font-size: 18px; - max-width: 768px; - margin: 2rem auto; - padding: 2rem; - border: solid 1px lightgrey; - border-radius: 15px; - box-shadow: 3px 2px 6px lightgrey; -} - -label, input { - margin-top: 5px; + padding: 20px; + line-height: 1.6; + } + + h1 { + color: #33475b; + } + + table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; + } + + th, td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #ddd; + } + + th { + background-color: #f8f9fa; + } + + a { + display: inline-block; + margin: 10px 0; + color: #ff7a59; + text-decoration: none; + font-weight: bold; + } + + a:hover { + text-decoration: underline; + } + + .form-group { + margin-bottom: 15px; + } + + label { display: block; - font-size: inherit; -} - -input[type="text"] { - padding: .25rem; -} - -input[type="submit"] { - background-color: lightgrey; + margin-bottom: 5px; + } + + input, textarea { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + } + + button { + background-color: #ff7a59; + color: white; border: none; - padding: .375rem 1rem; - margin-top: 10px; -} \ No newline at end of file + padding: 10px 15px; + border-radius: 4px; + cursor: pointer; + } + + button:hover { + background-color: #ff5a31; + } \ No newline at end of file diff --git a/test-env.js b/test-env.js new file mode 100644 index 00000000..806fd3a6 --- /dev/null +++ b/test-env.js @@ -0,0 +1,32 @@ +// Load the dotenv package +require('dotenv').config(); + +// Log what's in the environment +console.log('Environment variables loaded:'); +console.log('PRIVATE_APP_TOKEN exists:', !!process.env.PRIVATE_APP_TOKEN); +if (process.env.PRIVATE_APP_TOKEN) { + // Only show the first few characters for security + const firstFew = process.env.PRIVATE_APP_TOKEN.substring(0, 5); + console.log('First few characters of token:', firstFew + '...'); +} +console.log('OBJECT_TYPE:', process.env.OBJECT_TYPE || 'NOT FOUND'); + +// Check if dotenv is correctly configured +console.log('\nChecking dotenv configuration:'); +const path = require('path'); +const fs = require('fs'); +const envPath = path.resolve(process.cwd(), '.env'); +console.log('.env file should be at:', envPath); +console.log('.env file exists:', fs.existsSync(envPath)); + +// If it exists, show the first line (without revealing the full token) +if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, 'utf8'); + const firstLine = envContent.split('\n')[0]; + if (firstLine.startsWith('PRIVATE_APP_TOKEN=')) { + console.log('First line starts correctly with PRIVATE_APP_TOKEN='); + console.log('First few characters after equals:', firstLine.split('=')[1].substring(0, 5) + '...'); + } else { + console.log('First line of .env file:', firstLine); + } +} \ No newline at end of file diff --git a/views/homepage.pug b/views/homepage.pug new file mode 100644 index 00000000..30680f42 --- /dev/null +++ b/views/homepage.pug @@ -0,0 +1,23 @@ +html + head + title Guitars Collection | Integrating With HubSpot I Practicum + link(rel='stylesheet', href='css/style.css') + body + h1 My Guitar Collection + a(href="/update-cobj") Add to this table + + if guitars && guitars.length > 0 + table + thead + tr + th Name + th Year + th Description + tbody + each guitar in guitars + tr + td= guitar.properties.name + td= guitar.properties.year + td= guitar.properties.description + else + p No guitars found. Add one! \ No newline at end of file diff --git a/views/updates.pug b/views/updates.pug new file mode 100644 index 00000000..77ef572c --- /dev/null +++ b/views/updates.pug @@ -0,0 +1,23 @@ +html + head + title= title + link(rel='stylesheet', href='css/style.css') + body + h1 Add New Guitar + + form(action="/update-cobj", method="POST") + div.form-group + label(for="name") Guitar Name: + input#name(type="text", name="name", required) + + div.form-group + label(for="year") Year: + input#year(type="number", name="year", required) + + div.form-group + label(for="description") Description: + textarea#description(name="description", rows="4", required) + + button(type="submit") Add Guitar + + a(href="/") Return to the homepage \ No newline at end of file