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