INIT
This commit is contained in:
+30
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
Generated
+384
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user