# Bloom Bug Screens — Full Project
This document contains the full project for the Bloom Bug Screens Custom Order Builder with:
– Frontend: `index.html` (single-page app using Tailwind + plain JS)
– Backend: `server.js` (Node.js + Express)
– `package.json` for dependencies
– `.env.example` showing required environment variables
– README with install & deploy instructions
—
## 1) README / Quick Setup
**Prerequisites**
– Node.js 18+ (or recent LTS)
– A Square account with a **Square Access Token** and **Location ID** (sandbox or production)
– A SendGrid account and **API Key**
**Environment variables** (create a `.env` file based on `.env.example` below)
**Install & Run (development)**
“`bash
# install
npm install
# start server (development)
npm run dev
“`
The server runs by default on `http://localhost:3000`. Open `index.html` in your browser (or serve it from the server – README shows both ways).
**Notes**
– The frontend calls the backend endpoints `/create-checkout` and `/send-quote`. Those are implemented in `server.js`.
– For production, enable HTTPS and secure your env variables.
—
## 2) index.html (frontend)
Save this file as `index.html` in your project root or in a `public/` folder. The file contains the product builder UI and calls the backend endpoints.
“`html
Bloom Bug Screens — Custom Order Builder
Measurements (per item)
Cart
```
---
## 3) server.js (Node.js + Express backend)
Save this file as `server.js` in your project root.
```js
/* server.js
- Express server
- /create-checkout -> creates Square Checkout link and returns { checkoutUrl }
- /send-quote -> sends an email using SendGrid with the cart & attached files (optional)
*/
require('dotenv').config();
const express = require('express');
const path = require('path');
const app = express();
const port = process.env.PORT || 3000;
// parse JSON bodies
app.use(express.json());
// serve static frontend (if you place index.html in /public)
app.use(express.static(path.join(__dirname, 'public')));
// --- Square SDK ---
const { Client, Environment } = require('@square/square');
const squareClient = new Client({
environment: process.env.SQUARE_ENV === 'production' ? Environment.Production : Environment.Sandbox,
accessToken: process.env.SQUARE_ACCESS_TOKEN,
});
// --- SendGrid ---
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
// --- multer for file uploads ---
const multer = require('multer');
const storage = multer.memoryStorage();
const upload = multer({ storage });
// helper: convert cart items to Square line items
function buildOrderAndRedirect(items, orderId) {
// This function just builds Order objects for a Checkout request.
// You'll want to customize currency, taxes, etc.
return items.map((it, idx) => {
const name = `${it.category} - ${it.type}`;
// price in cents
const amount = Math.round((Number(it.price) || 0) * 100);
return {
name,
quantity: '1',
base_price_money: {
amount,
currency: 'CAD'
}
};
});
}
// Create Checkout link (Square Checkout API)
app.post('/create-checkout', async (req, res) => {
try {
const { items, additionalInfo, finalComments } = req.body;
if(!items || !Array.isArray(items) || items.length === 0) return res.status(400).json({ error: 'No items provided' });
const locationId = process.env.SQUARE_LOCATION_ID;
if(!locationId) return res.status(500).json({ error: 'Missing SQUARE_LOCATION_ID' });
// Build line items for Square
const lineItems = buildOrderAndRedirect(items);
const checkoutApi = squareClient.checkoutApi;
const requestBody = {
idempotencyKey: `bb_${Date.now()}`,
order: {
locationId,
lineItems
},
askForShippingAddress: false,
merchantSupportEmail: process.env.QUOTE_EMAIL || 'orders@bloombug.ca',
note: `Order from Bloom Bug Screens - ${new Date().toISOString()}`,
prePopulateBuyerEmail: req.body.customerEmail || undefined,
};
const result = await checkoutApi.createCheckout(locationId, requestBody);
// createCheckout returns a .checkout object containing .checkoutPageUrl
const checkoutUrl = result.result.checkout.checkoutPageUrl;
res.json({ checkoutUrl });
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message || 'Checkout creation failed' });
}
});
// Send quote via SendGrid (accepts multipart/form-data with files)
app.post('/send-quote', upload.array('files'), async (req, res) => {
try {
const payloadStr = req.body.payload;
if(!payloadStr) return res.status(400).json({ error: 'Missing payload' });
const payload = JSON.parse(payloadStr);
const items = payload.items || [];
const lines = items.map((it, i) => `Item ${i+1}: ${it.category} • ${it.type} • ${it.widthIn}\" x ${it.heightIn}\" • Color: ${it.color || '-'} • Price est: ${it.price ? '$' + it.price : 'TBD'}`).join('
');
const emailBody = `New quote request:
${lines}
Subtotal estimate: ${items.reduce((s,it)=> s + (it.price||0),0).toFixed(2)}
Additional Info: ${payload.additionalInfo || '(none)'}
Final Comments: ${payload.finalComments || '(none)'}
Please follow up with customer.`;
const msg = {
to: process.env.QUOTE_EMAIL || 'hello@bloombug.ca',
from: process.env.SEND_FROM_EMAIL || 'no-reply@bloombug.ca',
subject: 'Bloom Bug Screens - Quote Request',
text: emailBody,
};
// Attach files if provided
if(req.files && req.files.length){
msg.attachments = req.files.map(f => ({
content: f.buffer.toString('base64'),
filename: f.originalname,
type: f.mimetype,
disposition: 'attachment'
}));
}
await sgMail.send(msg);
res.json({ ok: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message || 'Send quote failed' });
}
});
// fallback route
app.get('/ping', (req,res) => res.json({ ok: true }));
app.listen(port, () => console.log(`Server listening on http://localhost:${port}`));
```
---
## 4) package.json
```json
{
"name": "bloom-bug-screens",
"version": "1.0.0",
"description": "Custom order builder with Square Checkout and SendGrid integration",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "NODE_ENV=development nodemon server.js"
},
"dependencies": {
"@sendgrid/mail": "^7.9.0",
"@square/square": "^20.0.0",
"dotenv": "^16.0.0",
"express": "^4.18.2",
"multer": "^1.4.5"
},
"devDependencies": {
"nodemon": "^2.0.22"
}
}
```
> NOTE: Version numbers are examples — run `npm install` and the lockfile will pin exact versions.
---
## 5) .env.example
```
PORT=3000
SQUARE_ENV=sandbox # or 'production'
SQUARE_ACCESS_TOKEN=REPLACE_WITH_YOUR_SQUARE_ACCESS_TOKEN
SQUARE_LOCATION_ID=REPLACE_WITH_YOUR_SQUARE_LOCATION_ID
SENDGRID_API_KEY=REPLACE_WITH_YOUR_SENDGRID_KEY
SEND_FROM_EMAIL=no-reply@bloombug.ca
QUOTE_EMAIL=hello@bloombug.ca
```
---
## 6) How the flow works
- **Checkout**: Frontend sends `POST /create-checkout` with `{ items, additionalInfo, finalComments }`. Server creates a Square Checkout using the Square SDK and returns `checkoutUrl`. Frontend redirects the user to `checkoutUrl`.
- **Request Quote**: Frontend sends `POST /send-quote` (multipart/form-data) with payload and optional files. Server sends an email to `QUOTE_EMAIL` with the details and attachments.
---
## 7) Security & production notes
- Keep your `.env` out of version control. Add `.env` to `.gitignore`.
- Use Square **production** tokens only after testing in sandbox.
- Use HTTPS in production and consider additional validation on the server for item prices to avoid price tampering (compute prices server-side as the source of truth).
- Consider adding authentication for admin pages that can view quote requests.
---
If you'd like, I can now:
1. Put the **final frontend** (complete JS UI) into the `public/index.html` file and update the server to serve it.
2. Produce the actual `server.js`, `package.json` and `.env.example` files separately in the canvas for download.
3. Or generate a GitHub-ready repository structure and a ZIP file you can download.
Tell me which you prefer and I will drop the exact files into the canvas for you to copy/download.