Compare commits
4 Commits
2b037f6583
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 241b8a0f76 | |||
| c783836bca | |||
| 9eb8be207d | |||
| 076027e8b4 |
+42
@@ -0,0 +1,42 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
bun.lockb
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
|
||||
@@ -1,2 +1,48 @@
|
||||
# PRISM
|
||||
|
||||
PRISM is a construction job management system built with Bun, Hono, and PocketBase.
|
||||
|
||||
## Features
|
||||
|
||||
- **Authentication**: Login system powered by PocketBase
|
||||
- **Dashboard**: Post-login dashboard with project overview
|
||||
- **User Management**: Standard PocketBase users collection
|
||||
- **API Integration**: Direct connection to PocketBase backend
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Bun](https://bun.sh/) runtime
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
bun run start
|
||||
```
|
||||
|
||||
## Accessing the Application
|
||||
|
||||
- **Login Page**: Visit `http://localhost:3000` in your browser
|
||||
- **Dashboard**: Available at `/dashboard.html` after authentication
|
||||
- **Backend API**: `GET /health` - Health check endpoint
|
||||
|
||||
## PocketBase Configuration
|
||||
|
||||
The application connects to your PocketBase instance at `https://pocketbase.ccllc.pro` with:
|
||||
- Users collection (standard PocketBase authentication)
|
||||
- API collection for additional data
|
||||
- Authentication rule: `id = @request.auth.id`
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "prism",
|
||||
"dependencies": {
|
||||
"hono": "^4.0.0",
|
||||
"pocketbase": "^0.21.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"bun-types": "^1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||
|
||||
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
|
||||
|
||||
"pocketbase": ["pocketbase@0.21.5", "", {}, "sha512-bnI/uinnQps+ElSlzxkc4yvwuSFfKcoszDtXH/4QT2FhGq2mJVUvDlxn+rjRXVntUjPfmMG5LEPZ1eGqV6ssog=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "prism",
|
||||
"version": "1.0.0",
|
||||
"description": "PRISM - Construction job management system",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"dev": "bun run --hot src/index.ts",
|
||||
"start": "bun run src/index.ts",
|
||||
"build": "bun build src/index.ts",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.0.0",
|
||||
"pocketbase": "^0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"bun-types": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// Shared components for PRISM application
|
||||
|
||||
export class MenuBar {
|
||||
constructor(currentPage = 'dashboard') {
|
||||
this.currentPage = currentPage;
|
||||
this.injectStyles();
|
||||
}
|
||||
|
||||
injectStyles() {
|
||||
// Only inject styles once
|
||||
if (document.getElementById('menubar-styles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'menubar-styles';
|
||||
style.textContent = `
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--gruvbox-bg2) 0%, var(--gruvbox-bg1) 100%);
|
||||
color: var(--gruvbox-fg0);
|
||||
padding: 1.25rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
border-bottom: 1px solid var(--gruvbox-bg3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--gruvbox-orange);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--gruvbox-fg1);
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: var(--gruvbox-bg3);
|
||||
color: var(--gruvbox-fg0);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: var(--gruvbox-blue);
|
||||
color: var(--gruvbox-bg0);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.user-info span {
|
||||
color: var(--gruvbox-fg1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: var(--gruvbox-red);
|
||||
color: var(--gruvbox-fg0);
|
||||
border: 1px solid var(--gruvbox-bg3);
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: var(--gruvbox-orange);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(251, 73, 52, 0.3);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
render(userName = 'Loading...') {
|
||||
return `
|
||||
<header class="header">
|
||||
<h1>🏗️ PRISM</h1>
|
||||
<nav class="nav-links">
|
||||
<a href="/dashboard.html" class="nav-link ${this.currentPage === 'dashboard' ? 'active' : ''}">Dashboard</a>
|
||||
<a href="/jobs.html" class="nav-link ${this.currentPage === 'jobs' ? 'active' : ''}">Jobs</a>
|
||||
</nav>
|
||||
<div class="user-info">
|
||||
<span id="user-name">${userName}</span>
|
||||
<button class="logout-btn" onclick="logout()">Logout</button>
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
|
||||
updateUserName(name) {
|
||||
const userNameElement = document.getElementById('user-name');
|
||||
if (userNameElement) {
|
||||
userNameElement.textContent = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to initialize the menu bar
|
||||
export function initMenuBar(currentPage, userName = 'Loading...') {
|
||||
const menuBar = new MenuBar(currentPage);
|
||||
|
||||
// Insert the menu bar at the beginning of the body
|
||||
const body = document.body;
|
||||
const firstChild = body.firstElementChild;
|
||||
|
||||
// Create a temporary container for the menu HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = menuBar.render(userName).trim();
|
||||
|
||||
// Insert the menu bar before the first child
|
||||
if (firstChild) {
|
||||
body.insertBefore(tempDiv.firstElementChild, firstChild);
|
||||
} else {
|
||||
body.appendChild(tempDiv.firstElementChild);
|
||||
}
|
||||
|
||||
return menuBar;
|
||||
}
|
||||
|
||||
// Export a function to get the current page from the URL
|
||||
export function getCurrentPage() {
|
||||
const path = window.location.pathname;
|
||||
if (path.includes('jobs.html')) {
|
||||
return 'jobs';
|
||||
}
|
||||
return 'dashboard';
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PRISM Dashboard - Construction Job Management</title>
|
||||
<style>
|
||||
:root {
|
||||
--gruvbox-bg0: #282828;
|
||||
--gruvbox-bg1: #3c3836;
|
||||
--gruvbox-bg2: #504945;
|
||||
--gruvbox-bg3: #665c54;
|
||||
--gruvbox-fg0: #fbf1c7;
|
||||
--gruvbox-fg1: #ebdbb2;
|
||||
--gruvbox-fg2: #d5c4a1;
|
||||
--gruvbox-orange: #fe8019;
|
||||
--gruvbox-green: #b8bb26;
|
||||
--gruvbox-blue: #83a598;
|
||||
--gruvbox-purple: #d3869b;
|
||||
--gruvbox-red: #fb4934;
|
||||
--gruvbox-yellow: #fabd2f;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'SF Pro Display', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(135deg, var(--gruvbox-bg0) 0%, var(--gruvbox-bg2) 50%, var(--gruvbox-bg1) 100%);
|
||||
min-height: 100vh;
|
||||
color: var(--gruvbox-fg1);
|
||||
}
|
||||
|
||||
|
||||
.main-content {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
background: var(--gruvbox-bg1);
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--gruvbox-bg3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.welcome-section h2 {
|
||||
color: var(--gruvbox-orange);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.welcome-section p {
|
||||
color: var(--gruvbox-fg2);
|
||||
line-height: 1.6;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: var(--gruvbox-bg1);
|
||||
padding: 1.75rem;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid var(--gruvbox-bg3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
||||
border-color: var(--gruvbox-blue);
|
||||
}
|
||||
|
||||
.dashboard-card h3 {
|
||||
color: var(--gruvbox-fg0);
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-card .icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--gruvbox-green);
|
||||
}
|
||||
|
||||
.dashboard-card p {
|
||||
color: var(--gruvbox-fg2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
color: var(--gruvbox-orange);
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.recent-activity {
|
||||
background: var(--gruvbox-bg1);
|
||||
padding: 1.75rem;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--gruvbox-bg3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.recent-activity h3 {
|
||||
color: var(--gruvbox-fg0);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem 0;
|
||||
border-bottom: 1px solid var(--gruvbox-bg3);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-item:hover {
|
||||
background: rgba(131, 165, 152, 0.05);
|
||||
border-radius: 8px;
|
||||
padding-left: 0.5rem;
|
||||
margin-left: -0.5rem;
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
background: linear-gradient(135deg, var(--gruvbox-blue) 0%, var(--gruvbox-purple) 100%);
|
||||
color: var(--gruvbox-bg0);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 12px rgba(131, 165, 152, 0.3);
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-content h4 {
|
||||
color: var(--gruvbox-fg0);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity-content p {
|
||||
color: var(--gruvbox-fg2);
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid var(--gruvbox-bg3);
|
||||
border-top: 4px solid var(--gruvbox-orange);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: rgba(251, 73, 52, 0.1);
|
||||
color: var(--gruvbox-red);
|
||||
padding: 1.25rem;
|
||||
border-radius: 8px;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid rgba(251, 73, 52, 0.3);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
.main-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.welcome-section {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.recent-activity {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main class="main-content">
|
||||
<div id="loading" class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading dashboard...</p>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-content" style="display: none;">
|
||||
<div class="welcome-section">
|
||||
<h2>Welcome back!</h2>
|
||||
<p>Here's your construction job management overview. Stay organized and efficient with PRISM.</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="dashboard-card">
|
||||
<h3><span class="icon">📋</span> Active Jobs</h3>
|
||||
<div class="stat-number" id="active-jobs">-</div>
|
||||
<p>Currently active construction projects</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h3><span class="icon">👷</span> Team Members</h3>
|
||||
<div class="stat-number" id="team-members">-</div>
|
||||
<p>People working on your projects</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h3><span class="icon">✅</span> Completed This Month</h3>
|
||||
<div class="stat-number" id="completed-month">-</div>
|
||||
<p>Jobs finished in the last 30 days</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h3><span class="icon">📊</span> Revenue</h3>
|
||||
<div class="stat-number" id="revenue">$0</div>
|
||||
<p>Total revenue this quarter</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recent-activity">
|
||||
<h3>Recent Activity</h3>
|
||||
<div id="activity-list">
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon">🔄</div>
|
||||
<div class="activity-content">
|
||||
<h4>System Initialized</h4>
|
||||
<p>Welcome to PRISM dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="error-content" class="error-message" style="display: none;">
|
||||
Unable to load dashboard. Please try refreshing the page.
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script type="module">
|
||||
import PocketBase from '/pocketbase.es.mjs';
|
||||
import { initMenuBar, getCurrentPage } from '/components.js';
|
||||
|
||||
// Initialize PocketBase
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||
|
||||
// Check authentication and load dashboard
|
||||
async function initDashboard() {
|
||||
try {
|
||||
// Check if user is authenticated
|
||||
if (!pb.authStore.isValid) {
|
||||
// Redirect to login if not authenticated
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize menu bar
|
||||
const menuBar = initMenuBar(getCurrentPage());
|
||||
|
||||
const user = pb.authStore.model;
|
||||
menuBar.updateUserName(user.name || user.email);
|
||||
|
||||
// Hide loading and show dashboard
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('dashboard-content').style.display = 'block';
|
||||
|
||||
// Load dashboard data
|
||||
await loadDashboardData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Dashboard initialization failed:', error.message);
|
||||
showError('Failed to initialize dashboard. Please try logging in again.');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
// This is a placeholder for actual API calls to your collections
|
||||
// For now, we'll show some mock data
|
||||
|
||||
// Mock data - replace with actual API calls to your collections
|
||||
document.getElementById('active-jobs').textContent = '12';
|
||||
document.getElementById('team-members').textContent = '8';
|
||||
document.getElementById('completed-month').textContent = '5';
|
||||
document.getElementById('revenue').textContent = '$125,000';
|
||||
|
||||
// Example: Load data from your API collection
|
||||
// const records = await pb.collection('api').getList(1, 50);
|
||||
// Process your data here...
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error.message);
|
||||
// Show error but don't redirect - user is authenticated
|
||||
document.getElementById('error-content').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
pb.authStore.clear();
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// Make logout function globally available
|
||||
window.logout = logout;
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('dashboard-content').style.display = 'none';
|
||||
document.getElementById('error-content').textContent = message;
|
||||
document.getElementById('error-content').style.display = 'block';
|
||||
}
|
||||
|
||||
// Initialize dashboard when page loads
|
||||
document.addEventListener('DOMContentLoaded', initDashboard);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
import PocketBase from '/pocketbase.es.mjs';
|
||||
|
||||
// Initialize PocketBase
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||
|
||||
// API key for admin operations (fetched from 'api' collection)
|
||||
let API_KEY = null;
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAuthenticated() {
|
||||
return pb.authStore.isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current authenticated user
|
||||
* @returns {object|null}
|
||||
*/
|
||||
export function getCurrentUser() {
|
||||
return pb.authStore.model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout current user
|
||||
*/
|
||||
export function logout() {
|
||||
pb.authStore.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch API key from the 'api' collection
|
||||
* The key is stored in a field called 'API_KEY'
|
||||
*/
|
||||
export async function loadApiKey() {
|
||||
// Try different collection name variations
|
||||
const collectionNames = ['api', 'API', 'Api'];
|
||||
|
||||
for (const collectionName of collectionNames) {
|
||||
try {
|
||||
console.log(`Trying to load API key from collection: ${collectionName}`);
|
||||
|
||||
// Fetch the API key from the collection
|
||||
const records = await pb.collection(collectionName).getFullList({
|
||||
sort: '-created'
|
||||
});
|
||||
|
||||
console.log(`Found ${records?.length || 0} record(s) in ${collectionName} collection`);
|
||||
|
||||
if (records && records.length > 0) {
|
||||
const firstRecord = records[0];
|
||||
console.log(`First record fields:`, Object.keys(firstRecord));
|
||||
console.log(`First record data:`, firstRecord);
|
||||
|
||||
// Try different field name variations
|
||||
// Priority: API_KEY (as shown in PocketBase admin), then other variations
|
||||
API_KEY = firstRecord.API_KEY ||
|
||||
firstRecord.Key ||
|
||||
firstRecord.key ||
|
||||
firstRecord.KEY ||
|
||||
firstRecord.api_key ||
|
||||
firstRecord.apiKey ||
|
||||
firstRecord.ApiKey ||
|
||||
null;
|
||||
|
||||
if (API_KEY) {
|
||||
console.log(`✅ API key loaded successfully from ${collectionName} collection`);
|
||||
return API_KEY; // Success, exit function
|
||||
} else {
|
||||
console.warn(`API key field not found in ${collectionName} collection record. Available fields:`, Object.keys(firstRecord));
|
||||
}
|
||||
} else {
|
||||
console.warn(`No records found in ${collectionName} collection`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Could not load from ${collectionName} collection:`, error.message);
|
||||
// Try next collection name
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, no API key was found
|
||||
console.warn('⚠️ API key not found in any api collection. Operations will use user authentication.');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key (returns null if not loaded)
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function getApiKey() {
|
||||
return API_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load collection schema from PocketBase
|
||||
* @param {string} collectionName - Name of the collection
|
||||
* @returns {Promise<Array>} Collection schema
|
||||
*/
|
||||
export async function loadCollectionSchema(collectionName = 'Jobs') {
|
||||
// Try to fetch real schema from PocketBase
|
||||
try {
|
||||
// Use API key for schema access if available (bypasses collection rules)
|
||||
let collection;
|
||||
if (API_KEY) {
|
||||
// Make request with API key in header
|
||||
const response = await fetch(`${pb.baseUrl}/api/collections/${collectionName}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
collection = await response.json();
|
||||
} else {
|
||||
// Use standard PocketBase client (may fail with 403 if no permissions)
|
||||
collection = await pb.collections.getOne(collectionName);
|
||||
}
|
||||
|
||||
console.log('Fetched collection:', collection);
|
||||
console.log('Collection schema:', collection.schema);
|
||||
|
||||
if (collection.schema && collection.schema.length > 0) {
|
||||
// Map PocketBase schema to our format
|
||||
const schema = collection.schema.map(field => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
required: field.required || false,
|
||||
options: field.options || {}
|
||||
}));
|
||||
console.log('Real collection schema loaded with', schema.length, 'fields');
|
||||
console.log('Schema details:', schema.map(f => ({
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
required: f.required
|
||||
})));
|
||||
return schema;
|
||||
} else {
|
||||
throw new Error('Collection schema is empty');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch collection schema, using defaults:', error.message);
|
||||
// Fallback to predefined schema
|
||||
const defaultSchema = [
|
||||
{ name: 'Job_Number', type: 'number' },
|
||||
{ name: 'Job_Name', type: 'text' },
|
||||
{ name: 'Job_Address', type: 'text' },
|
||||
{ name: 'updated', type: 'date' }
|
||||
];
|
||||
console.log('Using predefined collection schema with', defaultSchema.length, 'fields');
|
||||
return defaultSchema;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collection info (for debugging)
|
||||
* @param {string} collectionName - Name of the collection
|
||||
* @returns {Promise<object>} Collection info
|
||||
*/
|
||||
export async function getCollectionInfo(collectionName = 'Jobs') {
|
||||
try {
|
||||
let collection;
|
||||
if (API_KEY) {
|
||||
// Use API key for schema access
|
||||
const response = await fetch(`${pb.baseUrl}/api/collections/${collectionName}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
collection = await response.json();
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
} else {
|
||||
collection = await pb.collections.getOne(collectionName);
|
||||
}
|
||||
return collection;
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch collection info:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all jobs from the Jobs collection
|
||||
* @returns {Promise<Array>} Array of job records
|
||||
*/
|
||||
export async function loadJobs() {
|
||||
try {
|
||||
console.log('Attempting to load jobs from collection...');
|
||||
|
||||
const collectionUsed = 'Jobs';
|
||||
|
||||
// Use getFullList to fetch all records at once (better for client-side filtering)
|
||||
const records = await pb.collection(collectionUsed).getFullList({
|
||||
sort: '-updated'
|
||||
});
|
||||
|
||||
console.log(`Jobs response from collection "${collectionUsed}":`, records);
|
||||
console.log('Number of jobs found:', records?.length || 0);
|
||||
|
||||
// Debug: Log each job's structure
|
||||
if (records && records.length > 0) {
|
||||
console.log('Sample job structure:', records[0]);
|
||||
console.log('Job fields:', Object.keys(records[0]));
|
||||
}
|
||||
|
||||
return records || [];
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading jobs:', error);
|
||||
console.error('Error details:', {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
data: error.response?.data,
|
||||
url: error.config?.url
|
||||
});
|
||||
|
||||
// Handle specific error cases
|
||||
if (error.response?.status === 403) {
|
||||
throw new Error('Access denied to jobs collection. Please check your permissions in PocketBase.');
|
||||
} else if (error.response?.status === 404) {
|
||||
throw new Error('Jobs collection not found. Please create the "jobs" collection in PocketBase.');
|
||||
} else {
|
||||
throw new Error(`Failed to load jobs: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available job number
|
||||
* @returns {Promise<number>} Next job number (1000-9999)
|
||||
*/
|
||||
export async function getNextJobNumber() {
|
||||
try {
|
||||
// Use getFirstListItem to get only the highest job number (most efficient)
|
||||
// Sort by Job_Number descending to get the highest first
|
||||
const highestJob = await pb.collection('Jobs').getFirstListItem('', {
|
||||
sort: '-Job_Number'
|
||||
});
|
||||
|
||||
const highestJobNumber = highestJob.Job_Number || 0;
|
||||
const nextJobNumber = highestJobNumber + 1;
|
||||
|
||||
// Ensure it's within 1000-9999 range
|
||||
if (nextJobNumber < 1000) {
|
||||
return 1000;
|
||||
} else if (nextJobNumber > 9999) {
|
||||
throw new Error('Maximum job number (9999) reached');
|
||||
}
|
||||
|
||||
return nextJobNumber;
|
||||
|
||||
} catch (error) {
|
||||
// If no records exist or any other error, start with 1000
|
||||
if (error.status === 404 || error.message?.includes('not found')) {
|
||||
// No jobs exist, start with 1000
|
||||
return 1000;
|
||||
}
|
||||
console.error('Error getting next job number:', error);
|
||||
// Fallback: start with 1000 if we can't determine the next number
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new job
|
||||
* @param {object} jobData - Job data object
|
||||
* @returns {Promise<object>} Created job record
|
||||
*/
|
||||
export async function createJob(jobData) {
|
||||
try {
|
||||
// Check if job number already exists (optional validation)
|
||||
try {
|
||||
const existingJobs = await pb.collection('Jobs').getList(1, 1, {
|
||||
filter: `Job_Number = ${jobData.Job_Number}`
|
||||
});
|
||||
|
||||
if (existingJobs.items.length > 0) {
|
||||
throw new Error(`Job number ${jobData.Job_Number} already exists. Please refresh to get the next available number.`);
|
||||
}
|
||||
} catch (checkError) {
|
||||
if (checkError.message.includes('already exists')) {
|
||||
throw checkError;
|
||||
}
|
||||
// Ignore other errors from the check - continue with creation
|
||||
console.warn('Could not check for existing job number:', checkError.message);
|
||||
}
|
||||
|
||||
// Create job in PocketBase
|
||||
const createdJob = await pb.collection('Jobs').create(jobData);
|
||||
console.log('Job created successfully:', createdJob);
|
||||
return createdJob;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating job:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a job
|
||||
* @param {string} jobId - Job record ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteJob(jobId) {
|
||||
try {
|
||||
await pb.collection('Jobs').delete(jobId);
|
||||
} catch (error) {
|
||||
console.error('Error deleting job:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug function to test PocketBase connection and permissions
|
||||
*/
|
||||
export async function debugPocketBase() {
|
||||
console.log('=== POCKETBASE DEBUG ===');
|
||||
console.log('Base URL:', pb.baseUrl);
|
||||
console.log('Auth valid:', pb.authStore.isValid);
|
||||
console.log('Auth model:', pb.authStore.model);
|
||||
|
||||
try {
|
||||
// Try to get collections
|
||||
const collections = await pb.collections.getFullList();
|
||||
console.log('Available collections:', collections.map(c => c.name));
|
||||
} catch (error) {
|
||||
console.error('Cannot list collections:', error.message);
|
||||
}
|
||||
|
||||
// Test the Jobs collection specifically
|
||||
console.log('Testing Jobs collection...');
|
||||
|
||||
// Test read access
|
||||
try {
|
||||
const result = await pb.collection('Jobs').getList(1, 10);
|
||||
console.log(`✅ Read access: ${result.totalItems} items found`);
|
||||
if (result.items.length > 0) {
|
||||
console.log('Sample item:', result.items[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Read access failed:', error.message);
|
||||
}
|
||||
|
||||
// Test write access with dummy data
|
||||
try {
|
||||
const testData = { Job_Number: 9999, Job_Name: 'TEST_JOB_PLEASE_DELETE' };
|
||||
const testResult = await pb.collection('Jobs').create(testData);
|
||||
console.log('✅ Write access successful, created test record:', testResult);
|
||||
|
||||
// Clean up test record
|
||||
await pb.collection('Jobs').delete(testResult.id);
|
||||
console.log('✅ Test record cleaned up');
|
||||
} catch (error) {
|
||||
console.log('❌ Write access failed:', error.message);
|
||||
if (error.response) {
|
||||
console.log('Error details:', error.response.data);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=== END DEBUG ===');
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug collections
|
||||
*/
|
||||
export async function debugCollections() {
|
||||
try {
|
||||
console.log('=== DEBUGGING COLLECTIONS ===');
|
||||
|
||||
// Try different collection names (only valid ones)
|
||||
const collectionNames = ['jobs', 'Jobs'];
|
||||
|
||||
for (const collectionName of collectionNames) {
|
||||
try {
|
||||
console.log(`Trying collection: ${collectionName}`);
|
||||
const records = await pb.collection(collectionName).getList(1, 10);
|
||||
console.log(`${collectionName} found with ${records.totalItems} items`);
|
||||
if (records.totalItems > 0) {
|
||||
console.log(`${collectionName} sample item:`, records.items[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`${collectionName} error:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=== END DEBUG ===');
|
||||
} catch (error) {
|
||||
console.warn('Debug collections failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Export PocketBase instance for advanced usage if needed
|
||||
export { pb };
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PRISM - Construction Job Management</title>
|
||||
<style>
|
||||
:root {
|
||||
--gruvbox-bg0: #282828;
|
||||
--gruvbox-bg1: #3c3836;
|
||||
--gruvbox-bg2: #504945;
|
||||
--gruvbox-bg3: #665c54;
|
||||
--gruvbox-fg0: #fbf1c7;
|
||||
--gruvbox-fg1: #ebdbb2;
|
||||
--gruvbox-fg2: #d5c4a1;
|
||||
--gruvbox-orange: #fe8019;
|
||||
--gruvbox-green: #b8bb26;
|
||||
--gruvbox-blue: #83a598;
|
||||
--gruvbox-purple: #d3869b;
|
||||
--gruvbox-red: #fb4934;
|
||||
--gruvbox-yellow: #fabd2f;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'SF Pro Display', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: linear-gradient(135deg, var(--gruvbox-bg0) 0%, var(--gruvbox-bg2) 50%, var(--gruvbox-bg1) 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--gruvbox-fg1);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: var(--gruvbox-bg1);
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid var(--gruvbox-bg3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: var(--gruvbox-orange);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: var(--gruvbox-fg2);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--gruvbox-fg1);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.875rem;
|
||||
border: 2px solid var(--gruvbox-bg3);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
background: var(--gruvbox-bg2);
|
||||
color: var(--gruvbox-fg1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--gruvbox-blue);
|
||||
box-shadow: 0 0 0 3px rgba(131, 165, 152, 0.1);
|
||||
background: var(--gruvbox-bg0);
|
||||
}
|
||||
|
||||
.form-group input::placeholder {
|
||||
color: var(--gruvbox-fg2);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 0.875rem;
|
||||
background: linear-gradient(135deg, var(--gruvbox-orange) 0%, var(--gruvbox-red) 100%);
|
||||
color: var(--gruvbox-bg0);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(254, 128, 25, 0.3);
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.login-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: rgba(251, 73, 52, 0.1);
|
||||
color: var(--gruvbox-red);
|
||||
padding: 0.875rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid rgba(251, 73, 52, 0.3);
|
||||
display: none;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: rgba(184, 187, 38, 0.1);
|
||||
color: var(--gruvbox-green);
|
||||
padding: 0.875rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid rgba(184, 187, 38, 0.3);
|
||||
display: none;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.user-info {
|
||||
background: var(--gruvbox-bg2);
|
||||
padding: 1.25rem;
|
||||
border-radius: 8px;
|
||||
margin-top: 1rem;
|
||||
display: none;
|
||||
border: 1px solid var(--gruvbox-bg3);
|
||||
}
|
||||
|
||||
.user-info h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--gruvbox-green);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.user-info p {
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--gruvbox-fg2);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: var(--gruvbox-red);
|
||||
color: var(--gruvbox-fg0);
|
||||
border: none;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: var(--gruvbox-orange);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(251, 73, 52, 0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1>PRISM</h1>
|
||||
<p>Construction Job Management System</p>
|
||||
</div>
|
||||
|
||||
<div id="error-message" class="error-message"></div>
|
||||
<div id="success-message" class="success-message"></div>
|
||||
|
||||
<div id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" placeholder="Enter your email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" placeholder="Enter your password" required>
|
||||
</div>
|
||||
|
||||
<button id="login-btn" class="login-btn">
|
||||
<span id="login-text">Login</span>
|
||||
<span id="loading" class="loading" style="display: none;"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="user-info" class="user-info" style="display: none;">
|
||||
<h3>Welcome!</h3>
|
||||
<p id="user-details"></p>
|
||||
<button class="logout-btn" onclick="logout()">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import PocketBase from '/pocketbase.es.mjs';
|
||||
|
||||
// Initialize PocketBase
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||
|
||||
// Optional: Set API key for admin operations (if needed)
|
||||
// pb.authStore.save('itlbbkeok6lxtvxvic8dgpugj2962h', null);
|
||||
|
||||
// Check if user is already authenticated
|
||||
if (pb.authStore.isValid) {
|
||||
showUserInfo(pb.authStore.model);
|
||||
}
|
||||
|
||||
// Test connection on page load
|
||||
testConnection();
|
||||
|
||||
async function testConnection() {
|
||||
try {
|
||||
// Test basic connectivity (silent)
|
||||
await pb.health.check();
|
||||
} catch (error) {
|
||||
// Only show user-facing error for real connection issues
|
||||
if (error.message && !error.message.includes('autocancelled') && !error.message.includes('aborted')) {
|
||||
showError('Unable to connect to server. Please check your internet connection.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const email = document.getElementById('email').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const loginText = document.getElementById('login-text');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
if (!email || !password) {
|
||||
showError('Please fill in all fields');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
loginBtn.disabled = true;
|
||||
loginText.style.display = 'none';
|
||||
loading.style.display = 'inline-block';
|
||||
|
||||
try {
|
||||
const authData = await pb.collection('users').authWithPassword(email, password);
|
||||
|
||||
showSuccess('Login successful! Redirecting to dashboard...');
|
||||
|
||||
// Clear form
|
||||
document.getElementById('email').value = '';
|
||||
document.getElementById('password').value = '';
|
||||
|
||||
// Redirect to dashboard after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/dashboard.html';
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
// Log error for debugging (can be removed in production)
|
||||
console.error('Login failed:', error.message);
|
||||
|
||||
// Provide more specific error messages
|
||||
let errorMessage = 'Login failed. Please try again.';
|
||||
|
||||
if (error.response) {
|
||||
// Server responded with error
|
||||
const status = error.response.status;
|
||||
const data = error.response.data;
|
||||
|
||||
if (status === 400) {
|
||||
errorMessage = 'Invalid email or password. Please check your credentials.';
|
||||
} else if (status === 401) {
|
||||
errorMessage = 'Authentication failed. Please check your email and password.';
|
||||
} else if (status === 403) {
|
||||
errorMessage = 'Access denied. Your account may be disabled.';
|
||||
} else if (status === 404) {
|
||||
errorMessage = 'User not found. Please check your email address.';
|
||||
} else if (status >= 500) {
|
||||
errorMessage = 'Server error. Please try again later.';
|
||||
} else if (data && data.message) {
|
||||
errorMessage = data.message;
|
||||
}
|
||||
} else if (error.code === 'NETWORK_ERROR' || error.message?.includes('fetch')) {
|
||||
errorMessage = 'Network error. Please check your internet connection.';
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
showError(errorMessage);
|
||||
} finally {
|
||||
// Reset loading state
|
||||
loginBtn.disabled = false;
|
||||
loginText.style.display = 'inline';
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
pb.authStore.clear();
|
||||
document.getElementById('user-info').style.display = 'none';
|
||||
document.getElementById('login-form').style.display = 'block';
|
||||
showSuccess('Logged out successfully');
|
||||
}
|
||||
|
||||
// Make logout function globally available
|
||||
window.logout = logout;
|
||||
|
||||
function showUserInfo(user) {
|
||||
document.getElementById('login-form').style.display = 'none';
|
||||
document.getElementById('user-info').style.display = 'block';
|
||||
|
||||
const userDetails = document.getElementById('user-details');
|
||||
userDetails.innerHTML = `
|
||||
<strong>Name:</strong> ${user.name || 'N/A'}<br>
|
||||
<strong>Email:</strong> ${user.email}<br>
|
||||
<strong>ID:</strong> ${user.id}<br>
|
||||
<strong>Created:</strong> ${new Date(user.created).toLocaleDateString()}
|
||||
`;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorDiv = document.getElementById('error-message');
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
document.getElementById('success-message').style.display = 'none';
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const successDiv = document.getElementById('success-message');
|
||||
successDiv.textContent = message;
|
||||
successDiv.style.display = 'block';
|
||||
document.getElementById('error-message').style.display = 'none';
|
||||
}
|
||||
|
||||
// Allow Enter key to submit form
|
||||
document.getElementById('password').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
login();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle login button click
|
||||
document.getElementById('login-btn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
login();
|
||||
});
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
testConnection();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+1122
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,45 @@
|
||||
import { Hono } from 'hono'
|
||||
import { serveStatic } from 'hono/bun'
|
||||
import PocketBase from 'pocketbase/cjs'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// Initialize PocketBase for server-side testing
|
||||
const pb = new PocketBase('https://pocketbase.ccllc.pro')
|
||||
|
||||
// Serve static files from public directory
|
||||
app.use('/public/*', serveStatic({ root: './public' }))
|
||||
app.use('/dashboard.html', serveStatic({ path: './public/dashboard.html' }))
|
||||
app.use('/jobs.html', serveStatic({ path: './public/jobs.html' }))
|
||||
app.use('/components.js', serveStatic({ path: './public/components.js' }))
|
||||
app.use('/db.js', serveStatic({ path: './public/db.js' }))
|
||||
app.use('/pocketbase.es.mjs', serveStatic({ path: './public/pocketbase.es.mjs' }))
|
||||
app.use('/pocketbase.es.mjs.map', serveStatic({ path: './public/pocketbase.es.mjs.map' }))
|
||||
|
||||
// Serve the main login page
|
||||
app.get('/', serveStatic({ path: './public/index.html' }))
|
||||
|
||||
app.get('/health', (c) => {
|
||||
return c.json({ status: 'ok', service: 'PRISM API' })
|
||||
})
|
||||
|
||||
app.get('/test-pocketbase', async (c) => {
|
||||
try {
|
||||
const health = await pb.health.check()
|
||||
return c.json({
|
||||
status: 'ok',
|
||||
pocketbase: health,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} catch (error) {
|
||||
return c.json({
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
|
||||
export default app
|
||||
|
||||
|
||||
Reference in New Issue
Block a user