This commit is contained in:
2025-12-15 16:33:30 -06:00
parent 7797c0b719
commit 04f6da76e7
7 changed files with 904 additions and 1 deletions
+30
View File
@@ -0,0 +1,30 @@
# Dependencies
node_modules/
# Environment variables
.env
.env.local
.env.*.local
# Build outputs
dist/
build/
*.log
# OS files
.DS_Store
Thumbs.db
# Editor directories
.vscode/
.idea/
# Backup files
*.backup
*.bak
*.old
# Lock files (optional - include if using different package managers)
# package-lock.json
# yarn.lock
# bun.lockb
+134 -1
View File
@@ -1,2 +1,135 @@
# Idea-Feedback-Form # Job Creation Form with Excel Sync
**Unified system** that combines job creation form with immediate Excel synchronization.
## Architecture
This system merges:
- **JobSubmissionFlow**: User form with auto-incrementing Job_Number
- **SyncToExcelFromPb**: Microsoft Graph API Excel sync logic
### Flow
1. User submits job form
2. Server authenticates with PocketBase (agent credentials)
3. Auto-calculates `Job_Number = max(existing) + 1`
4. Creates record in PocketBase `Job_Info_Prod`
5. Captures `record.id` from creation response
6. Acquires Microsoft Graph API token
7. Gets Excel table columns
8. Maps PocketBase fields to Excel columns (case-insensitive, only existing columns)
9. Adds `pb_id = record.id` to Excel row
10. POSTs new row to Excel `Test_Table`
## Configuration
All credentials are in `.env` (copied from existing systems):
```bash
# Azure AD
CLIENT_ID=3c846e71-9609-40e1-b458-0eb805e21b9f
CLIENT_SECRET=7aD8Q~d5K~_PzQv6KqDdrEnmyXHE60eVDpbcnaK_
TENANT_ID=3fd97ea7-b124-41f1-855f-52d8ac3b16c7
# PocketBase Agent
PB_DB=https://pocketbase.ccllc.pro
PB_AGENT_EMAIL=pbserviceupdate@cardoza.construction
PB_AGENT_PASSWORD=gJFjYx5y0_BSMgX
PB_COLLECTION=Job_Info_Prod
PB_AUTH_COLLECTION=Users
# Excel
DRIVE_ID=b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx
ITEM_ID=01SPNXLDQRICHB63BFUNGKZ3GE6RL7LGBT
EXCEL_TABLE=/drives/.../tables/Test_Table
# Server
PORT=4000
```
## Installation
**Use npm install** (not bun install) to avoid Windows/OneDrive symlink issues:
```bash
npm install
```
## Usage
Start the server:
```bash
bun run dev
```
Server runs at: http://localhost:4000
## Key Features
### Auto-Increment Job Numbers
- Queries all existing `Job_Number` values
- Calculates `max + 1` for new job
- No manual input required
### Excel Column Mapping
- **Case-insensitive**: Matches `Job_Number` to `job_number`, `Company_Client` to `company_client`, etc.
- **Only existing columns**: Ignores PocketBase fields not in Excel table
- **Special mapping**: `record.id` → Excel column `pb_id`
### Error Handling
- If Excel sync fails, PocketBase record is still created
- Returns warning with partial success
- Logs Excel error details
## Endpoints
- `POST /api/submit` - Submit job form (creates in PB + Excel)
- `GET /api/health` - Health check
## Technical Details
### Dependencies
- **Hono**: Web framework
- **PocketBase SDK**: Database client
- **@microsoft/microsoft-graph-client**: Excel API
- **dotenv**: Environment config
### Authentication
- **PocketBase**: Service account (`pbserviceupdate@cardoza.construction`)
- **Microsoft Graph**: OAuth2 client credentials flow
### Excel Operations
- Table path: `EXCEL_TABLE` from .env
- Operation: `POST /rows` with `{ values: [[...]] }`
- Row format: 2D array matching column order
## Differences from Other Systems
### vs. SyncToExcelFromPb
- **No batch sync**: Creates one record at a time
- **No duplicate detection**: Always adds new row
- **No change comparison**: Simple create operation
- **No collision warnings**: Assumes Job_Number is unique
### vs. JobSubmissionFlow
- **Excel sync included**: Adds to Excel immediately after PB creation
- **pb_id captured**: Stores PocketBase record.id in Excel
- **Graph token acquisition**: Handles Microsoft authentication
- **Column-aware**: Only maps fields that exist in Excel table
## Troubleshooting
### Bun Package Resolution Error
**Symptom**: `Cannot find package 'hono'`
**Solution**: Use `npm install` instead of `bun install`
### Excel Sync Fails
- Check `EXCEL_TABLE` path in .env
- Verify Azure app has Files.ReadWrite.All permission
- Check Excel table name matches exactly (case-sensitive in Graph API)
### PocketBase Auth Fails
- Verify `PB_AGENT_EMAIL` and `PB_AGENT_PASSWORD` are correct
- Check agent account exists in `Users` collection
- Ensure `PB_DB` URL is accessible
+237
View File
@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Idea & Feedback Form</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4c51bf',
secondary: '#5b21b6',
}
}
}
}
</script>
</head>
<body class="min-h-screen bg-gradient-to-br from-primary to-secondary p-4 sm:p-8 flex justify-center items-center font-sans">
<!-- Login Container -->
<div id="loginContainer" class="bg-white rounded-2xl shadow-2xl max-w-md w-full p-8">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-800 mb-2">Cardoza Construction</h1>
<p class="text-gray-600">Idea & Feedback Form</p>
</div>
<div class="space-y-6">
<p class="text-gray-700 text-center">Sign in with your Microsoft account to access the Idea & Feedback Form</p>
<button type="button" id="loginBtn" class="w-full py-3 px-4 bg-gradient-to-br from-primary to-secondary text-white font-semibold rounded-lg hover:opacity-95 transition-opacity shadow-lg hover:shadow-xl">
Login with Microsoft
</button>
</div>
<div id="loginError" class="hidden mt-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm"></div>
</div>
<!-- Form Container (hidden until logged in) -->
<div id="formContainer" class="hidden bg-white rounded-2xl shadow-2xl max-w-2xl w-full p-6 sm:p-12">
<div class="flex items-center justify-between mb-2">
<div>
<h1 class="text-3xl sm:text-4xl font-bold text-gray-800">Idea & Feedback Form</h1>
<p class="text-gray-600 text-sm sm:text-base">Please share your ideas or feedback for improvement</p>
</div>
<div class="text-right">
<p id="userEmailDisplay" class="text-sm text-gray-600 mb-2"></p>
<button type="button" id="logoutBtn" class="px-3 py-1 bg-gray-200 text-gray-800 rounded-lg text-sm hover:bg-gray-300 transition-colors">
Logout
</button>
</div>
</div>
<form id="feedbackForm" class="space-y-6" autocomplete="off">
<!-- Form Type -->
<div class="bg-gray-50 rounded-xl p-4 sm:p-6">
<div class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b-2 border-primary">Form Type</div>
<div class="flex flex-col">
<label class="block text-sm font-medium text-gray-700 mb-4">
Select Type <span class="text-red-600">*</span>
</label>
<div class="flex flex-wrap gap-6">
<label class="flex items-center gap-3 cursor-pointer">
<input type="radio" id="formTypeFeedback" name="Form_Type" value="Feedback" required class="w-4 h-4 text-primary focus:ring-primary" autocomplete="off">
<span class="text-sm font-medium">Feedback</span>
</label>
<label class="flex items-center gap-3 cursor-pointer">
<input type="radio" id="formTypeSuggestion" name="Form_Type" value="Suggestion" required class="w-4 h-4 text-primary focus:ring-primary" autocomplete="off">
<span class="text-sm font-medium">Suggestion</span>
</label>
</div>
</div>
</div>
<!-- Description -->
<div class="bg-gray-50 rounded-xl p-4 sm:p-6">
<div class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b-2 border-primary">Description</div>
<div class="flex flex-col">
<label for="Description" class="block text-sm font-medium text-gray-700 mb-2">
Details <span class="text-red-600">*</span>
</label>
<textarea id="Description" name="Description" required placeholder="Please describe your idea or feedback and be as detailed as possible" rows="6"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all resize-none"
></textarea>
</div>
</div>
<button type="submit" id="submitBtn" disabled
class="w-full py-3 bg-gradient-to-br from-primary to-secondary text-white font-semibold rounded-lg hover:opacity-95 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed shadow-lg hover:shadow-xl">
Submit Feedback
</button>
<div id="message" class="hidden p-4 rounded-lg font-medium"></div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/pocketbase/dist/pocketbase.umd.js"></script>
<script>
// Initialize PocketBase client
const pb = new PocketBase('https://pocketbase.ccllc.pro');
// Update auth UI based on login state
function updateAuthUI() {
const loginContainer = document.getElementById('loginContainer');
const formContainer = document.getElementById('formContainer');
const userEmailDisplay = document.getElementById('userEmailDisplay');
if (pb.authStore.isValid) {
// Show form, hide login
loginContainer.classList.add('hidden');
formContainer.classList.remove('hidden');
const userName = pb.authStore.record?.name || pb.authStore.record?.email || 'Unknown User';
userEmailDisplay.textContent = userName;
} else {
// Show login, hide form
loginContainer.classList.remove('hidden');
formContainer.classList.add('hidden');
document.getElementById('loginError').classList.add('hidden');
}
}
// Handle login button click
document.getElementById('loginBtn').addEventListener('click', async (e) => {
e.preventDefault();
const loginBtn = document.getElementById('loginBtn');
const loginError = document.getElementById('loginError');
loginBtn.disabled = true;
loginBtn.textContent = 'Logging in...';
loginError.classList.add('hidden');
try {
const authData = await pb.collection('Users').authWithOAuth2({
provider: 'microsoft',
});
console.log('✓ Logged in with Microsoft');
updateAuthUI();
} catch (error) {
console.error(error);
loginError.textContent = `Login failed: ${error.message}`;
loginError.classList.remove('hidden');
} finally {
loginBtn.disabled = false;
loginBtn.textContent = 'Login with Microsoft';
}
});
// Handle logout button click
document.getElementById('logoutBtn').addEventListener('click', () => {
pb.authStore.clear();
updateAuthUI();
});
// Initial auth UI update
updateAuthUI();
// Function to check if required fields are filled
function checkRequiredFields() {
const formType = document.querySelector('input[name="Form_Type"]:checked');
const description = document.getElementById('Description').value.trim();
const submitBtn = document.getElementById('submitBtn');
if (formType && description) {
submitBtn.disabled = false;
} else {
submitBtn.disabled = true;
}
}
// Add event listeners to required fields
document.querySelectorAll('input[name="Form_Type"]').forEach(radio => {
radio.addEventListener('change', checkRequiredFields);
});
document.getElementById('Description').addEventListener('input', checkRequiredFields);
// Initial check
checkRequiredFields();
// Form submission
document.getElementById('feedbackForm').addEventListener('submit', async (e) => {
e.preventDefault();
// Check if user is authenticated
if (!pb.authStore.isValid) {
alert('Please log in with your Microsoft account first');
return;
}
const submitBtn = e.target.querySelector('#submitBtn');
const message = document.getElementById('message');
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
message.classList.add('hidden');
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
// Add user's PocketBase token
data.pbToken = pb.authStore.token;
try {
const response = await fetch('/idea-feedback/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (result.success) {
message.className = 'p-4 rounded-lg font-medium bg-green-50 text-green-700 border border-green-200';
message.textContent = result.message;
message.classList.remove('hidden');
e.target.reset();
checkRequiredFields();
} else {
throw new Error(result.message);
}
} catch (error) {
message.className = 'p-4 rounded-lg font-medium bg-red-50 text-red-700 border border-red-200';
message.textContent = error.message || 'Failed to submit form. Please try again.';
message.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Submit Feedback';
}
});
</script>
</body>
</html>
+384
View File
@@ -0,0 +1,384 @@
{
"name": "job-creation-with-excel-sync",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "job-creation-with-excel-sync",
"version": "1.0.0",
"dependencies": {
"@microsoft/microsoft-graph-client": "^3.0.7",
"dotenv": "^17.2.3",
"hono": "^4.10.8",
"pocketbase": "^0.26.5"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "latest",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@microsoft/microsoft-graph-client": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-client/-/microsoft-graph-client-3.0.7.tgz",
"integrity": "sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependenciesMeta": {
"@azure/identity": {
"optional": true
},
"@azure/msal-browser": {
"optional": true
},
"buffer": {
"optional": true
},
"stream-browserify": {
"optional": true
}
}
},
"node_modules/@types/bun": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.4.tgz",
"integrity": "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bun-types": "1.3.4"
}
},
"node_modules/@types/node": {
"version": "25.0.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.1.tgz",
"integrity": "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/autoprefixer": {
"version": "10.4.22",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
"integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.27.0",
"caniuse-lite": "^1.0.30001754",
"fraction.js": "^5.3.4",
"normalize-range": "^0.1.2",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.7",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz",
"integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/bun-types": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.4.tgz",
"integrity": "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001760",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
"integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"dev": true,
"license": "ISC"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/hono": {
"version": "4.10.8",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.10.8.tgz",
"integrity": "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/pocketbase": {
"version": "0.26.5",
"resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.5.tgz",
"integrity": "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ==",
"license": "MIT"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
"integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "idea-feedback-form",
"version": "1.0.0",
"description": "Idea and Feedback Form with PocketBase",
"type": "module",
"scripts": {
"dev": "bun run server.ts",
"start": "bun run server.ts"
},
"dependencies": {
"@microsoft/microsoft-graph-client": "^3.0.7",
"dotenv": "^17.2.3",
"hono": "^4.10.8",
"pocketbase": "^0.26.5"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "latest"
}
}
+77
View File
@@ -0,0 +1,77 @@
import { Hono } from 'hono';
import { serveStatic } from 'hono/bun';
import { cors } from 'hono/cors';
import PocketBase from 'pocketbase';
import { config } from 'dotenv';
config();
const app = new Hono();
// Enable CORS
app.use('/*', cors());
// Serve static files from frontend directory
app.use('/*', serveStatic({ root: './frontend' }));
// PocketBase client
const pb = new PocketBase(process.env.PB_DB || 'https://pocketbase.ccllc.pro');
// Use user's PocketBase token (passed from authenticated frontend)
function setUserPocketBaseAuth(token: string) {
pb.authStore.save(token);
}
// API endpoint to submit feedback
app.post('/api/submit', async (c) => {
try {
const data = await c.req.json();
const userToken = data.pbToken;
if (!userToken) {
return c.json({
success: false,
message: 'User authentication required. Please log in first.'
}, 401);
}
// Use user's PocketBase token
setUserPocketBaseAuth(userToken);
// Remove pbToken from data before creating record
delete data.pbToken;
// Create the record in PocketBase as the authenticated user
const record = await pb.collection(process.env.PB_COLLECTION!).create(data);
console.log(`✓ Created PocketBase record with ID: ${record.id}`);
return c.json({
success: true,
message: 'Thank you! Your feedback has been submitted successfully.',
recordId: record.id
});
} catch (error: any) {
console.error('✗ Error submitting feedback:', error);
return c.json({
success: false,
message: error.message || 'Failed to submit feedback'
}, 400);
}
});
// Health check endpoint
app.get('/api/health', (c) => {
return c.json({
status: 'ok',
service: 'idea-feedback-form',
timestamp: new Date().toISOString()
});
});
const PORT = Number(process.env.PORT || 7000);
console.log(`Idea & Feedback Form server running at http://localhost:${PORT}`);
export default {
port: PORT,
fetch: app.fetch,
};
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["bun-types", "node", "bun"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}