From 955498090b6d8aaa7297112129c763bc5d58d370 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Jan 2026 22:15:17 +0000 Subject: [PATCH 1/3] feat: Add complete Camp Referral Builder project - HubSpot card UI extension (platformVersion 2025.2) - Deal sidebar card for referral management - React-based UI with company/program/session selection - Referral creation and status updates - Vercel Next.js API backend - HubSpot API integration layer - Endpoints for companies, programs, sessions, referrals - Association and batch read helpers - Comprehensive documentation - Full README with architecture and setup - Quick start guide for 15-min deployment - API endpoint documentation - Troubleshooting guide Project structure supports: - Custom object associations (Program, Session, Referral) - Deal-to-Company-to-Program-to-Session flow - Outreach status and client interest tracking - Configurable via environment variables --- referral-builder/.gitignore | 40 ++ referral-builder/QUICKSTART.md | 106 ++++ referral-builder/README.md | 581 ++++++++++++++++++ referral-builder/hubspot-card/.gitignore | 6 + referral-builder/hubspot-card/hsproject.json | 5 + .../hubspot-card/src/app/app-hsmeta.json | 35 ++ .../src/app/cards/ReferralBuilderCard.tsx | 400 ++++++++++++ .../src/app/cards/card-hsmeta.json | 13 + .../hubspot-card/src/app/cards/package.json | 8 + referral-builder/vercel-api/.env.example | 14 + referral-builder/vercel-api/.gitignore | 34 + referral-builder/vercel-api/next.config.js | 12 + referral-builder/vercel-api/package.json | 26 + .../companies/[companyId]/programs/route.ts | 30 + .../src/app/api/companies/search/route.ts | 30 + .../app/api/deals/[dealId]/referrals/route.ts | 98 +++ .../vercel-api/src/app/api/health/route.ts | 5 + .../programs/[programId]/sessions/route.ts | 35 ++ .../app/api/referrals/[referralId]/route.ts | 19 + .../vercel-api/src/app/api/referrals/route.ts | 57 ++ .../vercel-api/src/lib/associations.ts | 23 + referral-builder/vercel-api/src/lib/config.ts | 23 + .../vercel-api/src/lib/hubspot.ts | 57 ++ .../vercel-api/src/lib/objects.ts | 13 + referral-builder/vercel-api/tsconfig.json | 27 + referral-builder/vercel-api/vercel.json | 8 + 26 files changed, 1705 insertions(+) create mode 100644 referral-builder/.gitignore create mode 100644 referral-builder/QUICKSTART.md create mode 100644 referral-builder/README.md create mode 100644 referral-builder/hubspot-card/.gitignore create mode 100644 referral-builder/hubspot-card/hsproject.json create mode 100644 referral-builder/hubspot-card/src/app/app-hsmeta.json create mode 100644 referral-builder/hubspot-card/src/app/cards/ReferralBuilderCard.tsx create mode 100644 referral-builder/hubspot-card/src/app/cards/card-hsmeta.json create mode 100644 referral-builder/hubspot-card/src/app/cards/package.json create mode 100644 referral-builder/vercel-api/.env.example create mode 100644 referral-builder/vercel-api/.gitignore create mode 100644 referral-builder/vercel-api/next.config.js create mode 100644 referral-builder/vercel-api/package.json create mode 100644 referral-builder/vercel-api/src/app/api/companies/[companyId]/programs/route.ts create mode 100644 referral-builder/vercel-api/src/app/api/companies/search/route.ts create mode 100644 referral-builder/vercel-api/src/app/api/deals/[dealId]/referrals/route.ts create mode 100644 referral-builder/vercel-api/src/app/api/health/route.ts create mode 100644 referral-builder/vercel-api/src/app/api/programs/[programId]/sessions/route.ts create mode 100644 referral-builder/vercel-api/src/app/api/referrals/[referralId]/route.ts create mode 100644 referral-builder/vercel-api/src/app/api/referrals/route.ts create mode 100644 referral-builder/vercel-api/src/lib/associations.ts create mode 100644 referral-builder/vercel-api/src/lib/config.ts create mode 100644 referral-builder/vercel-api/src/lib/hubspot.ts create mode 100644 referral-builder/vercel-api/src/lib/objects.ts create mode 100644 referral-builder/vercel-api/tsconfig.json create mode 100644 referral-builder/vercel-api/vercel.json diff --git a/referral-builder/.gitignore b/referral-builder/.gitignore new file mode 100644 index 0000000..a725057 --- /dev/null +++ b/referral-builder/.gitignore @@ -0,0 +1,40 @@ +# Dependencies +node_modules/ + +# Environment variables +.env +.env*.local + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Build outputs +dist/ +build/ +.next/ +out/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# HubSpot +.hubspot/ + +# Vercel +.vercel/ diff --git a/referral-builder/QUICKSTART.md b/referral-builder/QUICKSTART.md new file mode 100644 index 0000000..fd98bfa --- /dev/null +++ b/referral-builder/QUICKSTART.md @@ -0,0 +1,106 @@ +# Quick Start Guide + +Get the Referral Builder running in 15 minutes. + +## Prerequisites Checklist + +- [ ] HubSpot developer account +- [ ] HubSpot private app access token with required scopes +- [ ] Custom objects created (Program, Session, Referral) +- [ ] Node.js 18+ installed +- [ ] HubSpot CLI: `npm install -g @hubspot/cli` +- [ ] Vercel account +- [ ] Git installed + +## 5-Step Setup + +### 1️⃣ Deploy Vercel API (5 min) + +```bash +cd referral-builder/vercel-api +npm install +cp .env.example .env +# Edit .env and add your HUBSPOT_ACCESS_TOKEN +npm run dev # Test locally (optional) +vercel # Deploy to production +``` + +πŸ“ **Save your Vercel URL**: `https://your-project.vercel.app` + +### 2️⃣ Configure Environment Variables in Vercel (2 min) + +Go to Vercel Dashboard β†’ Settings β†’ Environment Variables + +Add: +``` +HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxx +HS_PROGRAM_OBJECT_TYPE=p_program +HS_SESSION_OBJECT_TYPE=p_session +HS_REFERRAL_OBJECT_TYPE=p_referral +``` + +### 3️⃣ Update HubSpot Card Config (3 min) + +**A. Edit `hubspot-card/src/app/app-hsmeta.json`:** +```json +{ + "permittedUrls": { + "fetch": [ + "https://api.hubapi.com", + "https://your-project.vercel.app" ← YOUR VERCEL URL + ] + } +} +``` + +**B. Edit `hubspot-card/src/app/cards/ReferralBuilderCard.tsx` line 13:** +```typescript +const API_BASE = "https://your-project.vercel.app"; // ← YOUR VERCEL URL +``` + +### 4️⃣ Deploy HubSpot Card (3 min) + +```bash +cd ../hubspot-card +hs auth # Authenticate with HubSpot +hs project upload +``` + +### 5️⃣ Add Card to Deal Layout (2 min) + +1. HubSpot β†’ **Settings** β†’ **Objects** β†’ **Deals** β†’ **Record customization** +2. Edit your layout +3. Right sidebar β†’ **Add card** β†’ Find **"Referral Builder"** +4. Save & Publish + +## βœ… Test It + +1. Open any Deal in HubSpot +2. You should see the **Referral Builder** card in the sidebar +3. Try: + - Search for a company + - Select company β†’ see programs + - Select program β†’ see sessions + - Create a referral + - Update outreach status and client interest + +## 🚨 Common Issues + +| Problem | Solution | +|---------|----------| +| Card not visible | Add card to Deal layout in HubSpot settings | +| API errors | Check Vercel domain is correct in both config files | +| No companies found | Verify HUBSPOT_ACCESS_TOKEN has company read scope | +| No programs/sessions | Check associations in HubSpot custom objects | + +## πŸ“– Need More Help? + +See the full [README.md](./README.md) for: +- Detailed setup instructions +- API endpoint documentation +- Troubleshooting guide +- Architecture overview + +--- + +**That's it! You're ready to build referrals.** πŸŽ‰ diff --git a/referral-builder/README.md b/referral-builder/README.md new file mode 100644 index 0000000..963650f --- /dev/null +++ b/referral-builder/README.md @@ -0,0 +1,581 @@ +# Camp Referral Builder + +A complete HubSpot integration for managing camp referrals with a Deal sidebar card and external API backend. + +## πŸ“‹ Overview + +This project provides a **Referral Builder** for HubSpot that: + +- Displays a custom card on Deal records +- Allows searching for Companies (camps) +- Lists Programs associated with Companies +- Lists Sessions associated with Programs +- Creates Referral records linking Deal β†’ Company β†’ Program β†’ Session +- Updates referral properties (outreach status, client interest, notes) + +## πŸ—οΈ Architecture + +The project consists of two main parts: + +### 1. **HubSpot Card** (`hubspot-card/`) +- Uses HubSpot Developer Projects with `platformVersion: 2025.2` +- React-based UI card that appears in the Deal sidebar +- Deployed via `hs project upload` command + +### 2. **Vercel API** (`vercel-api/`) +- Next.js API routes deployed on Vercel +- Handles all HubSpot API interactions +- Manages custom object operations (Company, Program, Session, Referral) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Deal Record β”‚ +β”‚ (HubSpot UI) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ displays + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Referral Card │───────▢│ Vercel API β”‚ +β”‚ (React UI) β”‚ fetch β”‚ (Next.js) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ HubSpot API + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ HubSpot CRM β”‚ + β”‚ (Companies, β”‚ + β”‚ Programs, β”‚ + β”‚ Sessions, β”‚ + β”‚ Referrals) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸš€ Getting Started + +### Prerequisites + +1. **HubSpot Account** with: + - Developer account access + - Custom objects created: Program, Session, Referral + - Private app access token with scopes: + - `crm.objects.deals.read` + - `crm.objects.deals.write` + - `crm.objects.companies.read` + - `crm.objects.custom.read` + - `crm.objects.custom.write` + +2. **Development Tools**: + - Node.js 18+ installed + - HubSpot CLI installed: `npm install -g @hubspot/cli` + - Vercel account + - Git + +3. **HubSpot Custom Objects**: + You need to create these custom objects in HubSpot: + - **Program** (associated with Company) + - **Session** (associated with Program) + - **Referral** (associated with Deal, Company, Program, Session) + + Key properties for **Referral** object: + - `referral_key` (text) - Unique identifier + - `referral_outreach_status` (dropdown) - Draft, Ready to Send, Sent, etc. + - `referral_client_interest` (dropdown) - Active, Shortlist, Neutral, etc. + - `referral_note_to_company` (text area) - Notes + +--- + +## πŸ“¦ Installation + +### Step 1: Set Up HubSpot Custom Objects + +1. Go to **Settings** β†’ **Data Management** β†’ **Objects** +2. Create custom objects: + +#### A. Program Object +- **Name**: Program +- **Associations**: Company (many-to-one) +- **Properties**: name (text) + +#### B. Session Object +- **Name**: Session +- **Associations**: Program (many-to-one) +- **Properties**: + - `name` (text) + - `start_date` (date) + - `end_date` (date) + - `price` (number) + - `weeks` (number) + +#### C. Referral Object +- **Name**: Referral +- **Associations**: + - Deal (many-to-one) + - Company (many-to-one) + - Program (many-to-one, optional) + - Session (many-to-one, optional) +- **Properties**: + - `referral_key` (text, unique) + - `referral_outreach_status` (dropdown): Draft, Ready to Send, Sent, Resend, Don't send (already sent) + - `referral_client_interest` (dropdown): Active / considering, Shortlist, Neutral, Unlikely, Declined, Selected + - `referral_note_to_company` (text area) + +### Step 2: Deploy Vercel API + +1. **Navigate to the Vercel API directory**: + ```bash + cd referral-builder/vercel-api + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Set up environment variables**: + ```bash + cp .env.example .env + ``` + +4. **Edit `.env`** and add your HubSpot access token: + ```env + HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + # Adjust these if your custom object names differ + HS_PROGRAM_OBJECT_TYPE=p_program + HS_SESSION_OBJECT_TYPE=p_session + HS_REFERRAL_OBJECT_TYPE=p_referral + + # Adjust these if your property names differ + HS_REFERRAL_KEY_PROP=referral_key + HS_REFERRAL_OUTREACH_PROP=referral_outreach_status + HS_REFERRAL_INTEREST_PROP=referral_client_interest + HS_REFERRAL_NOTE_PROP=referral_note_to_company + ``` + +5. **Test locally** (optional): + ```bash + npm run dev + ``` + Visit http://localhost:3000/api/health to verify it works. + +6. **Deploy to Vercel**: + + **Option A: Deploy via Vercel CLI** + ```bash + npm install -g vercel + vercel + ``` + Follow the prompts to link to your Vercel account. + + **Option B: Deploy via Vercel Dashboard** + - Push code to GitHub + - Go to https://vercel.com/new + - Import your repository + - Add environment variables in Vercel dashboard + +7. **Configure Vercel Environment Variables**: + In Vercel dashboard β†’ Settings β†’ Environment Variables, add: + - `HUBSPOT_ACCESS_TOKEN` + - `HS_PROGRAM_OBJECT_TYPE` + - `HS_SESSION_OBJECT_TYPE` + - `HS_REFERRAL_OBJECT_TYPE` + - (and any other variables from `.env.example`) + +8. **Note your Vercel domain**: + After deployment, you'll get a URL like `https://your-project.vercel.app` + +### Step 3: Configure HubSpot Card + +1. **Navigate to the HubSpot card directory**: + ```bash + cd ../hubspot-card + ``` + +2. **Update the Vercel domain in TWO places**: + + **A. In `src/app/app-hsmeta.json`**: + ```json + { + "permittedUrls": { + "fetch": [ + "https://api.hubapi.com", + "https://your-project.vercel.app" ← CHANGE THIS + ] + } + } + ``` + + **B. In `src/app/cards/ReferralBuilderCard.tsx`** (line 13): + ```typescript + const API_BASE = "https://your-project.vercel.app"; // ← CHANGE THIS + ``` + +3. **Authenticate HubSpot CLI**: + ```bash + hs auth + ``` + Follow the prompts to authenticate with your HubSpot account. + +4. **Deploy to HubSpot**: + ```bash + hs project upload + ``` + + This will upload your card to HubSpot. + +### Step 4: Add Card to Deal Layout + +The card won't automatically appear on Deals. You need to add it: + +1. Go to **Settings** β†’ **Objects** β†’ **Deals** β†’ **Record customization** +2. Edit the layout you use +3. In the right sidebar, click **"Add card"** +4. Find **"Referral Builder"** under your app +5. Click **Save** and **Publish** + +### Step 5: Test the Integration + +1. Open any Deal record in HubSpot +2. The **Referral Builder** card should appear in the right sidebar +3. Test the workflow: + - Search for a company + - Select a company β†’ programs should load + - Select a program β†’ sessions should load + - Create a referral + - Update referral properties + - Reload to see persisted data + +--- + +## πŸ”§ Configuration + +### Custom Object Names + +If your custom objects use different names (e.g., `p_camp_program` instead of `p_program`): + +1. Update `.env` in `vercel-api/`: + ```env + HS_PROGRAM_OBJECT_TYPE=p_camp_program + HS_SESSION_OBJECT_TYPE=p_camp_session + HS_REFERRAL_OBJECT_TYPE=p_camp_referral + ``` + +2. Redeploy to Vercel + +### Property Names + +If your Referral object uses different property names: + +1. Update `.env` in `vercel-api/`: + ```env + HS_REFERRAL_KEY_PROP=custom_referral_key + HS_REFERRAL_OUTREACH_PROP=custom_outreach_status + # etc. + ``` + +2. Update the card UI in `ReferralBuilderCard.tsx` where it calls `updateReferral()` to use matching property names + +3. Redeploy both Vercel API and HubSpot card + +--- + +## πŸ“ Project Structure + +``` +referral-builder/ +β”œβ”€β”€ hubspot-card/ # HubSpot UI Extension +β”‚ β”œβ”€β”€ hsproject.json # Project config (platformVersion: 2025.2) +β”‚ β”œβ”€β”€ .gitignore +β”‚ └── src/ +β”‚ └── app/ +β”‚ β”œβ”€β”€ app-hsmeta.json # App metadata +β”‚ └── cards/ +β”‚ β”œβ”€β”€ card-hsmeta.json # Card metadata +β”‚ β”œβ”€β”€ ReferralBuilderCard.tsx # Card UI component +β”‚ └── package.json +β”‚ +└── vercel-api/ # Next.js API Backend + β”œβ”€β”€ package.json + β”œβ”€β”€ tsconfig.json + β”œβ”€β”€ next.config.js + β”œβ”€β”€ vercel.json + β”œβ”€β”€ .env.example + β”œβ”€β”€ .gitignore + └── src/ + β”œβ”€β”€ lib/ + β”‚ β”œβ”€β”€ hubspot.ts # HubSpot API client + β”‚ β”œβ”€β”€ config.ts # Configuration constants + β”‚ β”œβ”€β”€ associations.ts # Association helper + β”‚ └── objects.ts # Object helper + └── app/ + └── api/ + β”œβ”€β”€ health/ + β”‚ └── route.ts # Health check endpoint + β”œβ”€β”€ companies/ + β”‚ β”œβ”€β”€ search/ + β”‚ β”‚ └── route.ts # Search companies + β”‚ └── [companyId]/ + β”‚ └── programs/ + β”‚ └── route.ts # Get programs for company + β”œβ”€β”€ programs/ + β”‚ └── [programId]/ + β”‚ └── sessions/ + β”‚ └── route.ts # Get sessions for program + β”œβ”€β”€ deals/ + β”‚ └── [dealId]/ + β”‚ └── referrals/ + β”‚ └── route.ts # Get referrals for deal + └── referrals/ + β”œβ”€β”€ route.ts # Create referral + └── [referralId]/ + └── route.ts # Update referral +``` + +--- + +## πŸ”Œ API Endpoints + +All endpoints are prefixed with your Vercel domain (e.g., `https://your-project.vercel.app`). + +### `GET /api/health` +Health check endpoint. + +**Response:** +```json +{ + "ok": true, + "ts": "2025-01-11T12:00:00.000Z" +} +``` + +### `GET /api/companies/search?q={query}` +Search for companies by name. + +**Parameters:** +- `q` (required): Search query +- `limit` (optional): Max results (default: 20) + +**Response:** +```json +{ + "results": [ + { + "id": "12345", + "name": "Camp Adventure" + } + ] +} +``` + +### `GET /api/companies/{companyId}/programs` +Get programs associated with a company. + +**Response:** +```json +{ + "results": [ + { + "id": "67890", + "name": "Summer Adventure Program" + } + ] +} +``` + +### `GET /api/programs/{programId}/sessions` +Get sessions associated with a program. + +**Response:** +```json +{ + "results": [ + { + "id": "11111", + "name": "Session 1", + "startDate": "2025-06-01", + "endDate": "2025-06-15", + "price": "1200", + "weeks": "2" + } + ] +} +``` + +### `GET /api/deals/{dealId}/referrals` +Get all referrals associated with a deal. + +**Response:** +```json +{ + "results": [ + { + "id": "22222", + "referralKey": "12345-67890", + "outreachStatus": "Draft", + "clientInterest": "Active / considering", + "note": "Great fit for the family", + "company": { + "id": "67890", + "name": "Camp Adventure" + }, + "program": { + "id": "11111", + "name": "Summer Adventure Program" + }, + "session": { + "id": "33333", + "name": "Session 1", + "startDate": "2025-06-01", + "endDate": "2025-06-15", + "price": "1200" + } + } + ] +} +``` + +### `POST /api/referrals` +Create a new referral. + +**Request body:** +```json +{ + "dealId": "12345", + "companyId": "67890", + "programId": "11111", + "sessionId": "33333", + "note": "Great fit for the family" +} +``` + +**Response:** +```json +{ + "ok": true, + "referralId": "22222" +} +``` + +### `PATCH /api/referrals/{referralId}` +Update referral properties. + +**Request body:** +```json +{ + "properties": { + "referral_outreach_status": "Sent", + "referral_client_interest": "Shortlist", + "referral_note_to_company": "Updated note" + } +} +``` + +**Response:** +```json +{ + "ok": true +} +``` + +--- + +## πŸ› Troubleshooting + +### Card not appearing on Deals +- Verify you added the card to the Deal layout (Settings β†’ Objects β†’ Deals β†’ Record customization) +- Check that `objectTypes: ["deals"]` is set in `card-hsmeta.json` +- Re-run `hs project upload` + +### API requests failing +- Verify your Vercel domain is correctly set in both `app-hsmeta.json` and `ReferralBuilderCard.tsx` +- Check Vercel logs for errors +- Verify `HUBSPOT_ACCESS_TOKEN` is set in Vercel environment variables +- Test the health endpoint: `https://your-project.vercel.app/api/health` + +### Custom objects not found +- Verify object type IDs in `.env` match your HubSpot setup +- Check object API names in HubSpot Settings β†’ Objects +- Use `p_{object_name}` format or full `objectTypeId` + +### Permission errors +- Verify your HubSpot access token has all required scopes +- Check token hasn't expired +- Ensure token has access to custom objects + +### Associations not working +- Verify associations are set up between objects in HubSpot +- Check that Companies have associated Programs +- Check that Programs have associated Sessions +- Ensure association labels are set correctly + +--- + +## πŸ”„ Updating the Application + +### Update HubSpot Card UI + +1. Make changes to `ReferralBuilderCard.tsx` +2. Run: + ```bash + cd hubspot-card + hs project upload + ``` +3. Refresh the Deal page in HubSpot + +### Update Vercel API + +1. Make changes to API routes or helpers +2. Commit and push to GitHub (if using automatic deployments) +3. Or run: + ```bash + cd vercel-api + vercel --prod + ``` +4. Changes take effect immediately (no HubSpot refresh needed) + +--- + +## πŸ“š Additional Resources + +- [HubSpot Developer Projects Documentation](https://developers.hubspot.com/docs/platform/developer-projects) +- [HubSpot UI Extensions Documentation](https://developers.hubspot.com/docs/platform/ui-extensions-overview) +- [HubSpot CRM API Documentation](https://developers.hubspot.com/docs/api/crm/understanding-the-crm) +- [Vercel Documentation](https://vercel.com/docs) +- [Next.js API Routes](https://nextjs.org/docs/api-routes/introduction) + +--- + +## 🀝 Support + +If you encounter issues: + +1. Check the Troubleshooting section above +2. Review Vercel deployment logs +3. Check HubSpot developer console for errors +4. Verify all environment variables are set correctly + +--- + +## πŸ“ License + +This project is provided as-is for internal use. + +--- + +## βœ… Next Steps / Future Enhancements + +Consider implementing: + +1. **Session Multi-Select**: Allow selecting multiple session options per referral +2. **Copy Prior Year Referrals**: Clone referrals from previous Deals for the same household +3. **Auto-Update Deal**: When client interest becomes "Selected", update Deal amount and close date +4. **Enhanced Error Handling**: More robust error messages and loading states +5. **Bulk Operations**: Create multiple referrals at once +6. **Filtering & Sorting**: Filter referrals by status, sort by date/interest +7. **Email Integration**: Send referral emails directly from the card +8. **Analytics Dashboard**: Track referral conversion rates + +--- + +**Built with ❀️ using HubSpot 2025.2 Platform and Next.js** diff --git a/referral-builder/hubspot-card/.gitignore b/referral-builder/hubspot-card/.gitignore new file mode 100644 index 0000000..26f3f0a --- /dev/null +++ b/referral-builder/hubspot-card/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.env +.DS_Store +dist/ +*.log +.hubspot/ diff --git a/referral-builder/hubspot-card/hsproject.json b/referral-builder/hubspot-card/hsproject.json new file mode 100644 index 0000000..98479b8 --- /dev/null +++ b/referral-builder/hubspot-card/hsproject.json @@ -0,0 +1,5 @@ +{ + "name": "Camp Referral Builder", + "srcDir": "src", + "platformVersion": "2025.2" +} diff --git a/referral-builder/hubspot-card/src/app/app-hsmeta.json b/referral-builder/hubspot-card/src/app/app-hsmeta.json new file mode 100644 index 0000000..dd93be5 --- /dev/null +++ b/referral-builder/hubspot-card/src/app/app-hsmeta.json @@ -0,0 +1,35 @@ +{ + "uid": "camp_referral_builder_app", + "type": "app", + "config": { + "description": "Referral Builder (Deal β†’ Company β†’ Program β†’ Session β†’ Referral)", + "name": "Camp Referral Builder", + "distribution": "private", + "auth": { + "type": "static", + "requiredScopes": [ + "crm.objects.deals.read", + "crm.objects.deals.write", + "crm.objects.companies.read", + "crm.objects.custom.read", + "crm.objects.custom.write" + ], + "optionalScopes": [], + "conditionallyRequiredScopes": [] + }, + "permittedUrls": { + "fetch": [ + "https://api.hubapi.com", + "https://YOUR_VERCEL_DOMAIN" + ], + "iframe": [], + "img": [] + }, + "support": { + "supportEmail": "support@example.com", + "documentationUrl": "https://example.com/docs", + "supportUrl": "https://example.com/support", + "supportPhone": "+18005555555" + } + } +} diff --git a/referral-builder/hubspot-card/src/app/cards/ReferralBuilderCard.tsx b/referral-builder/hubspot-card/src/app/cards/ReferralBuilderCard.tsx new file mode 100644 index 0000000..ceaaa6e --- /dev/null +++ b/referral-builder/hubspot-card/src/app/cards/ReferralBuilderCard.tsx @@ -0,0 +1,400 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + hubspot, + Box, + Button, + Divider, + Flex, + Heading, + Input, + Select, + Text, + TextArea, +} from "@hubspot/ui-extensions"; + +const API_BASE = "https://YOUR_VERCEL_DOMAIN"; // <-- CHANGE THIS (no trailing slash) + +type Option = { label: string; value: string }; + +type ReferralRow = { + id: string; + referralKey?: string; + outreachStatus?: string; + clientInterest?: string; + note?: string; + company?: { id?: string; name?: string }; + program?: { id?: string; name?: string }; + session?: { id?: string; name?: string; startDate?: string; endDate?: string; price?: string }; +}; + +const OUTREACH_OPTIONS: Option[] = [ + { label: "Draft", value: "Draft" }, + { label: "Ready to Send", value: "Ready to Send" }, + { label: "Sent", value: "Sent" }, + { label: "Resend", value: "Resend" }, + { label: "Don't send (already sent)", value: "Don't send (already sent)" }, +]; + +const INTEREST_OPTIONS: Option[] = [ + { label: "Active / considering", value: "Active / considering" }, + { label: "Shortlist", value: "Shortlist" }, + { label: "Neutral", value: "Neutral" }, + { label: "Unlikely", value: "Unlikely" }, + { label: "Declined", value: "Declined" }, + { label: "Selected", value: "Selected" }, +]; + +hubspot.extend(({ context, actions }) => ( + +)); + +function ReferralBuilderCard({ context, actions }: any) { + const dealId = context?.crm?.objectId ? String(context.crm.objectId) : null; + + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const [referrals, setReferrals] = useState([]); + + const [companyQuery, setCompanyQuery] = useState(""); + const [companyOptions, setCompanyOptions] = useState([]); + const [selectedCompanyId, setSelectedCompanyId] = useState(""); + + const [programOptions, setProgramOptions] = useState([]); + const [selectedProgramId, setSelectedProgramId] = useState(""); + + const [sessionOptions, setSessionOptions] = useState([]); + const [selectedSessionId, setSelectedSessionId] = useState(""); + + const [note, setNote] = useState(""); + + const canCreate = useMemo(() => { + return Boolean(dealId && selectedCompanyId); + }, [dealId, selectedCompanyId]); + + async function apiRequest(path: string, init?: { method?: string; body?: any }) { + const url = `${API_BASE}${path}`; + const res = await hubspot.fetch(url, { + method: init?.method || "GET", + body: init?.body ? JSON.stringify(init.body) : undefined, + }); + + let data: any = null; + try { + data = await res.json(); + } catch (e) { + // ignore + } + + if (!res.ok) { + const msg = data?.error || data?.message || `Request failed (${res.status})`; + throw new Error(msg); + } + return data; + } + + async function loadReferrals() { + if (!dealId) return; + const data = await apiRequest(`/api/deals/${dealId}/referrals`); + setReferrals(data?.results || []); + } + + async function searchCompanies() { + if (!companyQuery.trim()) { + setCompanyOptions([]); + return; + } + const data = await apiRequest(`/api/companies/search?q=${encodeURIComponent(companyQuery.trim())}`); + const opts: Option[] = (data?.results || []).map((c: any) => ({ + label: c.name || `Company ${c.id}`, + value: String(c.id), + })); + setCompanyOptions(opts); + } + + async function loadPrograms(companyId: string) { + setProgramOptions([]); + setSelectedProgramId(""); + setSessionOptions([]); + setSelectedSessionId(""); + + if (!companyId) return; + const data = await apiRequest(`/api/companies/${companyId}/programs`); + const opts: Option[] = (data?.results || []).map((p: any) => ({ + label: p.name || `Program ${p.id}`, + value: String(p.id), + })); + setProgramOptions(opts); + } + + async function loadSessions(programId: string) { + setSessionOptions([]); + setSelectedSessionId(""); + + if (!programId) return; + const data = await apiRequest(`/api/programs/${programId}/sessions`); + + const opts: Option[] = (data?.results || []).map((s: any) => { + const labelParts = [s.name || `Session ${s.id}`]; + if (s.startDate) labelParts.push(`(${s.startDate})`); + if (s.price) labelParts.push(`$${s.price}`); + return { label: labelParts.join(" "), value: String(s.id) }; + }); + + setSessionOptions(opts); + } + + async function createReferral() { + if (!dealId) return; + if (!selectedCompanyId) return; + + const payload = { + dealId, + companyId: selectedCompanyId, + programId: selectedProgramId || undefined, + sessionId: selectedSessionId || undefined, + note: note || undefined, + }; + + const data = await apiRequest(`/api/referrals`, { method: "POST", body: payload }); + + actions?.addAlert?.({ + type: "success", + message: `Referral created (ID ${data?.referralId || "unknown"})`, + }); + + setNote(""); + await loadReferrals(); + } + + async function updateReferral(referralId: string, properties: Record) { + await apiRequest(`/api/referrals/${referralId}`, { + method: "PATCH", + body: { properties }, + }); + actions?.addAlert?.({ type: "success", message: "Referral updated" }); + await loadReferrals(); + } + + useEffect(() => { + (async () => { + setError(null); + if (!dealId) return; + try { + setBusy(true); + await loadReferrals(); + } catch (e: any) { + setError(e?.message || "Failed to load referrals"); + } finally { + setBusy(false); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dealId]); + + if (!dealId) { + return ( + + This card is meant to run on a Deal record. + + ); + } + + return ( + + Referral Builder + Deal ID: {dealId} + + {error ? {error} : null} + + + + Existing referrals + + + {busy ? Loading… : null} + + {referrals.length === 0 ? ( + No referrals yet. + ) : ( + + {referrals.map((r) => ( + + + {r.company?.name || "Company"} + {" β€” "} + {r.program?.name || "Program"} + {" β€” "} + {r.session?.name || "Session"} + + + + { + setReferrals((prev) => + prev.map((x) => + x.id === r.id ? { ...x, clientInterest: val } : x + ) + ); + }} + /> + + +