Tailwind Upgrade

This commit is contained in:
2025-12-12 15:27:18 -06:00
parent e1695380fc
commit 835e29311b
4 changed files with 273 additions and 308 deletions
+250 -170
View File
@@ -4,170 +4,107 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" maximum-scale="1" /> <meta name="viewport" content="width=device-width,initial-scale=1" maximum-scale="1" />
<title>Job Info — Vanilla JS</title> <title>Job Info — Vanilla JS</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script> <script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
<script src="auth.js"></script> <script src="auth.js"></script>
<style>
/* Basic layout */
:root { --accent:#0d6efd; --muted:#6b7280; --card-bg:#ffffff; --card-border:#d1e7ff; }
body{font-family:Inter,Arial,Helvetica,sans-serif;background:#f3f4f6;margin:0;color:#111; overflow:hidden; touch-action: pan-y; height:100vh; display:flex; flex-direction:column;}
.wrap{max-width:800px;box-sizing:border-box;margin:0 auto;padding:16px;font-size:17px;display:flex;flex-direction:column;flex:1;overflow:hidden;}
h1{margin:0 0 12px;text-align:center;color:var(--accent);font-size:33px;font-weight:700;}
/* Progress */
.progress-wrap{width:100%;background:#e6edf7;height:6px;border-radius:4px;overflow:hidden;margin-bottom:12px;}
.progress-bar{height:100%;width:0;background:var(--accent);transition:width 0.3s ease-out;}
/* Search + filter toggle */
#searchBox{width:100%;padding:10px;border-radius:8px;border:1px solid #d1d5db;margin-bottom:8px;box-sizing:border-box;font-size: 18px;}
#searchBox::placeholder{color:#9ca3af;}
#filterToggle{background:var(--accent);color:#fff;padding:8px 10px;border-radius:8px;border:none;cursor:pointer;margin-bottom:8px;display:inline-block;max-width:110px;}
/* Filters panel */
#filtersPanel{display:none;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px;margin-bottom:10px;}
.filter-group{margin-bottom:8px;font-size:14px;}
.filter-group label{margin-right:8px;cursor:pointer;color:var(--muted);}
/* Results/cards */
#results{display:flex;flex-direction:column;gap:12px;flex:1;overflow:auto;padding:0;}
.card{background:var(--card-bg);border-radius:10px;padding:10px;border:1px solid #e6f0ff;box-shadow:0 6px 14px rgba(14,30,37,0.04);cursor:pointer;position:relative;display:flex;flex-direction:column;justify-content:center;box-sizing:border-box;gap:4px;}
.card-row{display:flex;align-items:center;gap:8px;font-size:18px;line-height:1.2;margin-bottom:2px;}
.card-label{font-weight:700;color:#374151;min-width:72px;white-space:nowrap;}
.card-info{color:#111;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;}
.active-dot{position:absolute;top:10px;right:10px;width:14px;height:14px;border-radius:999px;border:2px solid #fff;box-shadow:0 1px 2px rgba(0,0,0,0.12);}
/* Modal */
#detailModal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);align-items:flex-start;justify-content:center;padding-top:2vh;z-index:999;}
#detailBox{margin-block-start:0;margin-block-end:0;background:#fff;border-radius:10px;padding:2px 12px 6px 10px;max-width:380px;width:92%;max-height:96vh;overflow:auto;box-sizing:border-box;}
.h2{margin-top:0; margin-bottom:2px;margin-block-end:0;}
.detail-table{margin-top: 0;width:100%;border-collapse:collapse;margin-bottom:4px;}
.detail-table td{border:1px solid #eef2ff;padding:0px 6px 6px 6px;font-size:13px;vertical-align:top;}
.detail-table td.label{width:40%;background:#fbfdff;font-weight:700;color:#374151;}
#jobFolderBtn{width:100%;padding:10px 12px;border-radius:8px;border:none;color:#fff;cursor:pointer;font-size:17px;font-weight:700;margin-bottom:10px;transition:background 0.2s;}
#jobFolderBtn.active{background:var(--accent);}
#jobFolderBtn.active:hover{background:#0b5ed7;}
#jobFolderBtn:disabled{background:#4b5563;cursor:not-allowed;opacity:0.7;}
/* Notes */
#jobNotes{min-height:120px;max-height:200px;overflow:auto;padding:4px;border:1px solid #4b5563;border-radius:4px;font-size:11px;background:transparent;white-space:pre-wrap;word-wrap:break-word;color:#4b5563;}
#newNote{min-height:56px;max-height:120px;overflow:auto;padding:8px;border:1px solid #4b5563;border-radius:8px;font-size:16px;background:transparent;margin-top:6px;}
.toolbar{display:flex;flex-wrap:wrap;margin-bottom:3px;margin-top:5px} /* small space below toolbar */
.toolbar button{
height:28px;
width:32px;
font-size:12px;
padding:0 4px;
border-radius:6px;
border:1px solid #ccc;
background:#ccc;
color:#111;
cursor:pointer;
display:flex;
align-items:center;
justify-content:center;
margin: 1px;
}
.toolbar button#tbClear{
width:auto;
padding:4px 8px;
background:#9ca3af; /* slightly darker gray */
color:#fff;
}
.toolbar button.active{
background:#d1e7ff;
color:#111;
border-color:#0d6efd;
}
.modal-actions{display:flex;gap:8px;margin-top:6px;margin-bottom:50px;}
.btn{padding:8px 10px;border-radius:8px;border:none;color:#fff;cursor:pointer;}
.btn.green{background:#16a34a;} .btn.gray{background:#6b7280;} .btn.red{background:#dc2626;}
/* Footer */
.footer{text-align:right;font-size:12px;color:var(--muted);margin-top:20px;padding-top:12px;border-top:1px solid #e5e7eb;}
/* responsive small */
@media (max-width:800px){ .wrap{max-width:100%;} }
@media (max-width:380px){ .wrap{padding:12px;} .card{height:98px;} }
</style>
</head> </head>
<body> <body class="font-sans bg-gray-100 m-0 text-gray-900 overflow-hidden touch-pan-y h-screen flex flex-col">
<div class="wrap"> <div class="max-w-full md:max-w-2xl mx-auto p-4 sm:p-3 text-[17px] flex flex-col flex-1 overflow-hidden">
<h1>Job Info</h1> <h1 class="m-0 mb-3 text-center text-blue-600 text-[33px] font-bold">Job Info</h1>
<div class="progress-wrap"> <div class="w-full bg-blue-50 h-1.5 rounded overflow-hidden mb-3">
<div id="progressBar" class="progress-bar"></div> <div id="progressBar" class="h-full w-0 bg-blue-600 transition-all duration-300 ease-out"></div>
</div> </div>
<input id="searchBox" placeholder="Search jobs (just start typing)"> <input id="searchBox" placeholder="Search jobs (just start typing)" class="w-full p-2.5 rounded-lg border border-gray-300 mb-2 text-lg placeholder:text-gray-400">
<button id="filterToggle">Show Filters</button> <button id="filterToggle" class="bg-blue-600 text-white py-2 px-2.5 rounded-lg border-none cursor-pointer mb-2 inline-block min-w-[140px]">Show Filters</button>
<div id="filtersPanel"> <div id="filtersPanel" class="hidden bg-white border border-gray-200 rounded-lg p-2.5 mb-2.5">
<div class="filter-group"> <div class="mb-2 text-sm">
<strong>Division</strong><br/> <strong>Division</strong><br/>
<label><input type="checkbox" class="filter-division" value="C#"> Commercial</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="C#"> Commercial</label>
<label><input type="checkbox" class="filter-division" value="R#"> Residential</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="R#"> Residential</label>
<label><input type="checkbox" class="filter-division" value="S#"> Small Jobs</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="S#"> Small Jobs</label>
</div> </div>
<div class="filter-group"> <div class="mb-2 text-sm">
<strong>Active</strong><br/> <strong>Active</strong><br/>
<label><input type="checkbox" class="filter-active" value="Yes"> Yes</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-active" value="Yes"> Yes</label>
<label><input type="checkbox" class="filter-active" value="No"> No</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-active" value="No"> No</label>
</div> </div>
<div class="filter-group"> <div class="mb-2 text-sm">
<strong>Estimator</strong><br/> <strong>Estimator</strong><br/>
<label><input type="checkbox" class="filter-estimator" value="Steve Brewer"> Steve</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-estimator" value="Steve Brewer"> Steve</label>
<label><input type="checkbox" class="filter-estimator" value="Beth Cardoza"> Beth</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-estimator" value="Beth Cardoza"> Beth</label>
</div> </div>
<div class="filter-group"> <div class="mb-2 text-sm">
<strong>Status</strong><br/> <strong>Status</strong><br/>
<label><input type="checkbox" class="filter-status" value="Estimating"> Estimating</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Estimating"> Estimating</label>
<label><input type="checkbox" class="filter-status" value="In Progress"> In Progress</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="In Progress"> In Progress</label>
<label><input type="checkbox" class="filter-status" value="Awarded"> Awarded</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Awarded"> Awarded</label>
<label><input type="checkbox" class="filter-status" value="Billed (In Progress)"> Billed (In Progress)</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Billed (In Progress)"> Billed (In Progress)</label>
<label><input type="checkbox" class="filter-status" value="Not Bidding"> Not Bidding</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Not Bidding"> Not Bidding</label>
<label><input type="checkbox" class="filter-status" value="Est Sent"> Est Sent</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Est Sent"> Est Sent</label>
<label><input type="checkbox" class="filter-status" value="Not Awarded"> Not Awarded</label> <label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Not Awarded"> Not Awarded</label>
</div> </div>
<button id="clearFilters" style="background:#6b7280;color:white;padding:8px;border-radius:8px;border:none;">Clear Filters</button> <button id="clearFilters" class="bg-gray-500 text-white py-2 px-2 rounded-lg border-none">Clear Filters</button>
</div> </div>
<div id="results" aria-live="polite"></div> <div id="results" aria-live="polite" class="flex flex-col gap-3 flex-1 overflow-auto p-0"></div>
<div class="footer">v1.0.0-beta2</div> <div class="text-right text-xs text-gray-500 mt-5 pt-3 border-t border-gray-200">v1.0.0-beta2</div>
</div> </div>
<!-- Detail Modal --> <!-- Detail Modal -->
<div id="detailModal" role="dialog" aria-modal="true"> <div id="detailModal" role="dialog" aria-modal="true" class="hidden flex fixed inset-0 bg-black/45 items-start justify-center pt-[2vh] z-[999]">
<div id="detailBox"> <div id="detailBox" class="m-0 bg-white rounded-lg max-w-[380px] w-[92%] h-[90vh] max-h-[926px] overflow-hidden flex flex-col">
<h2 style="margin-top:0;color:var(--accent)">Job Detail</h2> <!-- INFO Tab Content -->
<div id="infoContent" class="flex-1 overflow-hidden flex flex-col min-h-0">
<table class="detail-table" id="detailTable"></table> <div id="infoHeading" class="bg-blue-600 text-white text-center py-2 text-sm font-semibold rounded-t-lg flex-shrink-0">INFO</div>
<div class="px-3 pt-2 pb-1 flex-1 overflow-auto">
<button id="jobFolderBtn" disabled>No Folder Available</button> <div class="mt-0 w-full mb-1 space-y-2" id="detailTable"></div>
<button id="jobFolderBtn" disabled class="w-full py-2.5 px-3 rounded-lg border-none text-white cursor-pointer text-[17px] font-bold mb-2.5 transition-colors duration-200 bg-gray-600 cursor-not-allowed opacity-70">No Folder Available</button>
<h3 style="margin:8px 0 6px 0;color:var(--accent)">Notes</h3> </div>
<div id="jobNotes"></div>
<div class="toolbar" id="toolbar">
<button data-cmd="bold" id="tbBold"><strong>B</strong></button>
<button data-cmd="italic" id="tbItalic"><em>I</em></button>
<button data-cmd="underline" id="tbUnderline"><u>U</u></button>
<button data-cmd="foreColor" data-val="#ff0000" id="tbRed" style="color:#ef4444">A</button>
<button data-cmd="foreColor" data-val="#0000ff" id="tbBlue" style="color:#2563eb">A</button>
<button data-cmd="foreColor" data-val="#000000" id="tbBlack" style="color:#000">A</button>
<button data-cmd="removeFormat" id="tbClear">Reset</button>
</div> </div>
<div id="newNote" contenteditable="true" aria-label="New note"></div> <!-- NOTES Tab Content -->
<div id="notesContent" class="hidden flex-1 overflow-hidden flex flex-col min-h-0">
<div id="notesHeading" class="bg-orange-600 text-white text-center py-2 text-sm font-semibold rounded-t-lg flex-shrink-0">NOTES</div>
<div class="px-3 pt-2 pb-1 flex-1 overflow-auto">
<div id="jobNotes" class="min-h-[120px] max-h-[200px] overflow-auto p-1 border border-gray-600 rounded text-[11px] bg-transparent whitespace-pre-wrap break-words text-gray-600"></div>
<div class="modal-actions"> <div class="flex flex-wrap mb-0.5 mt-1.5" id="toolbar">
<button class="btn green" id="btnSubmit">Submit</button> <button data-cmd="bold" id="tbBold" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px]"><strong>B</strong></button>
<button class="btn gray" id="btnClearNote">Clear</button> <button data-cmd="italic" id="tbItalic" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px]"><em>I</em></button>
<button class="btn red" id="btnClose">Close</button> <button data-cmd="underline" id="tbUnderline" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px]"><u>U</u></button>
<button data-cmd="foreColor" data-val="#ff0000" id="tbRed" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px] text-red-500">A</button>
<button data-cmd="foreColor" data-val="#0000ff" id="tbBlue" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px] text-blue-600">A</button>
<button data-cmd="foreColor" data-val="#000000" id="tbBlack" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px] text-black">A</button>
<button data-cmd="removeFormat" id="tbClear" class="h-7 w-auto py-1 px-2 text-xs rounded border border-gray-400 bg-gray-400 text-white cursor-pointer flex items-center justify-center m-[1px]">Reset</button>
</div>
<div id="newNote" contenteditable="true" aria-label="New note" class="min-h-14 max-h-[120px] overflow-auto p-2 border border-gray-600 rounded-lg text-base bg-transparent mt-1.5"></div>
<div class="flex gap-2 mt-1.5 mb-4">
<button class="py-2 px-2.5 rounded-lg border-none text-white cursor-pointer bg-green-600" id="btnSubmit">Submit</button>
<button class="py-2 px-2.5 rounded-lg border-none text-white cursor-pointer bg-gray-500" id="btnClearNote">Clear</button>
<button class="py-2 px-2.5 rounded-lg border-none text-white cursor-pointer bg-red-600" id="btnClose">Close</button>
</div>
</div>
</div>
<!-- Tab Bar at Bottom -->
<div class="flex items-center border-t border-gray-200 pt-2 px-3 pb-2 flex-shrink-0 bg-white rounded-b-lg">
<button id="backBtn" class="mr-6 pr-6 border-r border-gray-200 px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-800 flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Back
</button>
<button id="infoTab" class="px-4 py-2 text-sm font-medium text-blue-600 border-b-2 border-blue-600 -mb-px">INFO</button>
<button id="notesTab" class="px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700">NOTES</button>
</div> </div>
</div> </div>
</div> </div>
@@ -215,11 +152,11 @@
if(simulatedProg<65){ simulatedProg+=0.8; progressBar.style.width=Math.min(65,simulatedProg).toFixed(1)+'%'; } if(simulatedProg<65){ simulatedProg+=0.8; progressBar.style.width=Math.min(65,simulatedProg).toFixed(1)+'%'; }
},150); },150);
} }
function finishProgress(){ if(progTimer) clearInterval(progTimer); progressBar.style.width='100%'; setTimeout(()=>{progressBar.parentElement.style.display='none';},400); } function finishProgress(){ if(progTimer) clearInterval(progTimer); progressBar.style.width='100%'; setTimeout(()=>{progressBar.parentElement.classList.add('hidden');},400); }
// --- Fetch all jobs --- // --- Fetch all jobs ---
async function fetchAllJobs(){ async function fetchAllJobs(){
startProgress(); jobsCache=[]; visibleJobs=[]; resultsEl.innerHTML = '<div style="padding:8px;color:#475569;background:#fff;border-radius:8px;border:1px solid #e6eefc">Loading…</div>'; startProgress(); jobsCache=[]; visibleJobs=[]; resultsEl.innerHTML = '<div class="p-2 text-slate-600 bg-white rounded-lg border border-blue-100">Loading…</div>';
let page=1, totalItems=0; let firstBatchRendered=false; let page=1, totalItems=0; let firstBatchRendered=false;
try{ try{
while(true){ while(true){
@@ -251,13 +188,13 @@
function renderInitialBatch(jobs){ function renderInitialBatch(jobs){
resultsEl.innerHTML=''; resultsEl.innerHTML='';
for(const job of jobs){ for(const job of jobs){
const card=document.createElement('div'); card.className='card'; const card=document.createElement('div'); card.className='bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1';
card.innerHTML=` card.innerHTML=`
<div class="card-row"><div class="card-label">Job #:</div><div class="card-info">${escapeHtml(job.Job_Number||'')}</div></div> <div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job #:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Number||'')}</div></div>
<div class="card-row"><div class="card-label">Job Name:</div><div class="card-info">${escapeHtml(job.Job_Full_Name||'')}</div></div> <div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Full_Name||'')}</div></div>
<div class="card-row"><div class="card-label">Contact:</div><div class="card-info">${escapeHtml(job.Contact_Person||'')}</div></div> <div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Contact_Person||'')}</div></div>
`; `;
const dot=document.createElement('div'); dot.className='active-dot'; const dot=document.createElement('div'); dot.className='absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm';
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1'); dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
card.appendChild(dot); card.appendChild(dot);
card.addEventListener('click',()=>openDetail(job)); card.addEventListener('click',()=>openDetail(job));
@@ -310,19 +247,19 @@
function renderCards(list){ function renderCards(list){
resultsEl.innerHTML=''; resultsEl.innerHTML='';
if(!list.length){ resultsEl.innerHTML = `<div style="padding:8px;color:#475569;background:#fff;border-radius:8px;border:1px solid #e6eefc">No results</div>`; return; } if(!list.length){ resultsEl.innerHTML = `<div class="p-2 text-slate-600 bg-white rounded-lg border border-blue-100">No results</div>`; return; }
const firstChunk = list.slice(0,20); const firstChunk = list.slice(0,20);
const remaining = list.slice(20); const remaining = list.slice(20);
for(const job of firstChunk){ for(const job of firstChunk){
const card=document.createElement('div'); card.className='card'; const card=document.createElement('div'); card.className='bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1';
card.innerHTML=` card.innerHTML=`
<div class="card-row"><div class="card-label">Job #:</div><div class="card-info">${escapeHtml(job.Job_Number||'')}</div></div> <div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job #:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Number||'')}</div></div>
<div class="card-row"><div class="card-label">Job Name:</div><div class="card-info">${escapeHtml(job.Job_Full_Name||'')}</div></div> <div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Full_Name||'')}</div></div>
<div class="card-row"><div class="card-label">Contact:</div><div class="card-info">${escapeHtml(job.Contact_Person||'')}</div></div> <div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Contact_Person||'')}</div></div>
`; `;
const dot=document.createElement('div'); dot.className='active-dot'; const dot=document.createElement('div'); dot.className='absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm';
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1'); dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
card.appendChild(dot); card.appendChild(dot);
card.addEventListener('click',()=>openDetail(job)); card.addEventListener('click',()=>openDetail(job));
@@ -332,13 +269,13 @@
if(remaining.length){ if(remaining.length){
setTimeout(()=>{ setTimeout(()=>{
for(const job of remaining){ for(const job of remaining){
const card=document.createElement('div'); card.className='card'; const card=document.createElement('div'); card.className='bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1';
card.innerHTML=` card.innerHTML=`
<div class="card-row"><div class="card-label">Job #:</div><div class="card-info">${escapeHtml(job.Job_Number||'')}</div></div> <div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job #:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Number||'')}</div></div>
<div class="card-row"><div class="card-label">Job Name:</div><div class="card-info">${escapeHtml(job.Job_Full_Name||'')}</div></div> <div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Full_Name||'')}</div></div>
<div class="card-row"><div class="card-label">Contact:</div><div class="card-info">${escapeHtml(job.Contact_Person||'')}</div></div> <div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Contact_Person||'')}</div></div>
`; `;
const dot=document.createElement('div'); dot.className='active-dot'; const dot=document.createElement('div'); dot.className='absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm';
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1'); dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
card.appendChild(dot); card.appendChild(dot);
card.addEventListener('click',()=>openDetail(job)); card.addEventListener('click',()=>openDetail(job));
@@ -350,7 +287,7 @@
function escapeHtml(str){ return String(str).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;'); } function escapeHtml(str){ return String(str).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;'); }
// --- Filters --- // --- Filters ---
filterToggle.addEventListener('click',()=>{ filtersPanel.style.display = filtersPanel.style.display==='block'?'none':'block'; filterToggle.textContent = filtersPanel.style.display==='block'?'Hide Filters':'Show Filters'; }); filterToggle.addEventListener('click',()=>{ filtersPanel.classList.toggle('hidden'); filterToggle.textContent = filtersPanel.classList.contains('hidden')?'Show Filters':'Hide Filters'; });
searchBox.addEventListener('input',()=>applyFiltersAndRender()); searchBox.addEventListener('input',()=>applyFiltersAndRender());
document.querySelectorAll('.filter-division').forEach(cb=>cb.addEventListener('change',()=>applyFiltersAndRender())); document.querySelectorAll('.filter-division').forEach(cb=>cb.addEventListener('change',()=>applyFiltersAndRender()));
document.querySelectorAll('.filter-active').forEach(cb=>{ cb.addEventListener('change',()=>{ document.querySelectorAll('.filter-active').forEach(cb=>{ cb.addEventListener('change',()=>{
@@ -370,11 +307,121 @@
currentJob = job; currentJob = job;
detailTable.innerHTML = ''; detailTable.innerHTML = '';
const fields = ["Job_Full_Name","Job_Number","Company_Client","Manager","Contact_Person","Phone_Number","Job_Address","Start_Date","Tax_Exempt","Job_Status"]; const fields = ["Job_Full_Name","Job_Number","Company_Client","Manager","Contact_Person","Phone_Number","Job_Address","Start_Date","Tax_Exempt","Job_Status"];
for(const f of fields){ let i = 0;
const tr = document.createElement('tr'); while(i < fields.length){
const tdLabel = document.createElement('td'); tdLabel.className='label'; tdLabel.textContent=f.replace(/_/g,' '); const f = fields[i];
const tdVal = document.createElement('td'); tdVal.textContent=job[f]||'';
tr.appendChild(tdLabel); tr.appendChild(tdVal); detailTable.appendChild(tr); // Handle paired fields
if(f === 'Job_Number'){
const row = document.createElement('div'); row.className='pb-2 border-b border-gray-100 flex gap-4';
// Job Number
const col1 = document.createElement('div'); col1.className='flex-1';
const label1 = document.createElement('div'); label1.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label1.textContent='Job Number';
const val1 = document.createElement('div'); val1.className='text-sm text-gray-900'; val1.textContent = job['Job_Number'] || '—';
col1.appendChild(label1); col1.appendChild(val1);
// Company Client
const col2 = document.createElement('div'); col2.className='flex-1';
const label2 = document.createElement('div'); label2.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label2.textContent='Company Client';
const val2 = document.createElement('div'); val2.className='text-sm text-gray-900'; val2.textContent = job['Company_Client'] || '—';
col2.appendChild(label2); col2.appendChild(val2);
row.appendChild(col1); row.appendChild(col2); detailTable.appendChild(row);
i += 2; // Skip Company_Client in next iteration
continue;
}
if(f === 'Contact_Person'){
const row = document.createElement('div'); row.className='pb-2 border-b border-gray-100 flex gap-4';
// Contact Person
const col1 = document.createElement('div'); col1.className='flex-1';
const label1 = document.createElement('div'); label1.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label1.textContent='Contact Person';
const val1 = document.createElement('div'); val1.className='text-sm text-gray-900'; val1.textContent = job['Contact_Person'] || '—';
col1.appendChild(label1); col1.appendChild(val1);
// Phone Number
const col2 = document.createElement('div'); col2.className='flex-1';
const label2 = document.createElement('div'); label2.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label2.textContent='Phone Number';
const val2 = document.createElement('div'); val2.className='text-sm';
if(job['Phone_Number'] && job['Phone_Number'].trim() !== ''){
const phoneLink = document.createElement('a');
const phoneNumber = job['Phone_Number'].replace(/\D/g, ''); // Remove non-digits
phoneLink.href = `tel:${phoneNumber}`;
phoneLink.className = 'text-blue-600 hover:text-blue-800 underline';
phoneLink.textContent = job['Phone_Number'];
val2.appendChild(phoneLink);
} else {
val2.className = 'text-sm text-gray-900';
val2.textContent = '—';
}
col2.appendChild(label2); col2.appendChild(val2);
row.appendChild(col1); row.appendChild(col2); detailTable.appendChild(row);
i += 2; // Skip Phone_Number in next iteration
continue;
}
// Regular single field
const value = job[f] || '';
if(!value && f === 'Tax_Exempt') {
i++;
continue; // Skip empty Tax Exempt
}
const row = document.createElement('div'); row.className='pb-2 border-b border-gray-100 last:border-0';
const label = document.createElement('div'); label.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label.textContent=f.replace(/_/g,' ');
const val = document.createElement('div'); val.className='text-sm text-gray-900';
// Format date if it's Start_Date
if(f === 'Start_Date' && value){
try {
const date = new Date(value);
val.textContent = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
} catch(e) {
val.textContent = value;
}
} else if(f === 'Job_Address' && value && value.trim() !== ''){
// Create address container with text and map icons
val.className = 'text-sm flex items-center gap-2 flex-wrap';
// Address text
const addressText = document.createElement('span');
addressText.className = 'text-gray-900 flex-1 min-w-0';
addressText.textContent = value;
val.appendChild(addressText);
// Icon container
const iconContainer = document.createElement('div');
iconContainer.className = 'flex items-center gap-2';
// Apple Maps link
const appleLink = document.createElement('a');
const encodedAddress = encodeURIComponent(value);
appleLink.href = `https://maps.apple.com/?q=${encodedAddress}`;
appleLink.target = '_blank';
appleLink.className = 'inline-flex items-center justify-center w-6 h-6 hover:opacity-80 transition-opacity';
appleLink.title = 'Open in Apple Maps';
appleLink.innerHTML = '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" style="fill: #000000;"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>';
iconContainer.appendChild(appleLink);
// Google Maps link
const googleLink = document.createElement('a');
googleLink.href = `https://www.google.com/maps/search/?api=1&query=${encodedAddress}`;
googleLink.target = '_blank';
googleLink.className = 'inline-flex items-center justify-center w-6 h-6 hover:opacity-80 transition-opacity';
googleLink.title = 'Open in Google Maps';
googleLink.innerHTML = '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" style="fill: #4285F4;"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>';
iconContainer.appendChild(googleLink);
val.appendChild(iconContainer);
} else {
val.textContent = value || '—';
}
row.appendChild(label); row.appendChild(val); detailTable.appendChild(row);
i++;
} }
// Job folder button // Job folder button
@@ -383,28 +430,61 @@
const url=new URL(job.Job_Folder_Link); const url=new URL(job.Job_Folder_Link);
jobFolderBtn.textContent='Show Job Folder'; jobFolderBtn.textContent='Show Job Folder';
jobFolderBtn.disabled=false; jobFolderBtn.disabled=false;
jobFolderBtn.className='active'; jobFolderBtn.className='w-full py-2.5 px-3 rounded-lg border-none text-white cursor-pointer text-[17px] font-bold mb-2.5 transition-colors duration-200 bg-blue-600 hover:bg-blue-700';
jobFolderBtn.onclick=()=>window.open(url.href, '_blank', 'noopener,noreferrer'); jobFolderBtn.onclick=()=>window.open(url.href, '_blank', 'noopener,noreferrer');
}catch(e){ }catch(e){
jobFolderBtn.textContent='No Folder Available'; jobFolderBtn.textContent='No Folder Available';
jobFolderBtn.disabled=true; jobFolderBtn.disabled=true;
jobFolderBtn.className=''; jobFolderBtn.className='w-full py-2.5 px-3 rounded-lg border-none text-white cursor-pointer text-[17px] font-bold mb-2.5 transition-colors duration-200 bg-gray-600 cursor-not-allowed opacity-70';
jobFolderBtn.onclick=null; jobFolderBtn.onclick=null;
} }
} else { } else {
jobFolderBtn.textContent='No Folder Available'; jobFolderBtn.textContent='No Folder Available';
jobFolderBtn.disabled=true; jobFolderBtn.disabled=true;
jobFolderBtn.className=''; jobFolderBtn.className='w-full py-2.5 px-3 rounded-lg border-none text-white cursor-pointer text-[17px] font-bold mb-2.5 transition-colors duration-200 bg-gray-600 cursor-not-allowed opacity-70';
jobFolderBtn.onclick=null; jobFolderBtn.onclick=null;
} }
jobNotes.innerHTML = job.Notes || ''; jobNotes.innerHTML = job.Notes || '';
newNote.innerHTML = ''; newNote.innerHTML = '';
tbButtons.forEach(b=>b.classList.remove('active')); tbButtons.forEach(b=>b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600'));
detailModal.style.display='flex'; detailModal.classList.remove('hidden');
document.getElementById('detailBox').scrollTop = 0; document.getElementById('detailBox').scrollTop = 0;
// Reset to INFO tab when opening
switchTab('info');
} }
document.getElementById('btnClose').addEventListener('click',()=>{detailModal.style.display='none';}); document.getElementById('btnClose').addEventListener('click',()=>{detailModal.classList.add('hidden');});
document.getElementById('backBtn').addEventListener('click',()=>{detailModal.classList.add('hidden');});
// Tab switching
function switchTab(tab){
const infoTab = document.getElementById('infoTab');
const notesTab = document.getElementById('notesTab');
const infoContent = document.getElementById('infoContent');
const notesContent = document.getElementById('notesContent');
const infoHeading = document.getElementById('infoHeading');
const notesHeading = document.getElementById('notesHeading');
if(tab === 'info'){
infoTab.className = 'px-4 py-2 text-sm font-medium text-blue-600 border-b-2 border-blue-600 -mb-px';
notesTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
infoContent.classList.remove('hidden');
notesContent.classList.add('hidden');
if(infoHeading) infoHeading.classList.remove('hidden');
if(notesHeading) notesHeading.classList.add('hidden');
} else {
infoTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
notesTab.className = 'px-4 py-2 text-sm font-medium text-orange-600 border-b-2 border-orange-600 -mb-px';
infoContent.classList.add('hidden');
notesContent.classList.remove('hidden');
if(infoHeading) infoHeading.classList.add('hidden');
if(notesHeading) notesHeading.classList.remove('hidden');
}
}
document.getElementById('infoTab').addEventListener('click',()=>switchTab('info'));
document.getElementById('notesTab').addEventListener('click',()=>switchTab('notes'));
// --- Toolbar --- // --- Toolbar ---
let savedSelection = null; let savedSelection = null;
@@ -417,9 +497,9 @@
restoreSelection(); restoreSelection();
const cmd = btn.dataset.cmd; const cmd = btn.dataset.cmd;
if(!cmd) return; if(!cmd) return;
if(cmd==='foreColor'){ const val=btn.dataset.val; document.execCommand(cmd,false,val); tbButtons.forEach(b=>{if(b.dataset.cmd==='foreColor'&&b!==btn)b.classList.remove('active');}); btn.classList.toggle('active'); } if(cmd==='foreColor'){ const val=btn.dataset.val; document.execCommand(cmd,false,val); tbButtons.forEach(b=>{if(b.dataset.cmd==='foreColor'&&b!==btn)b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600');}); btn.classList.toggle('bg-blue-100'); btn.classList.toggle('text-gray-900'); btn.classList.toggle('border-blue-600'); }
else if(cmd==='removeFormat'){ restoreSelection(); const sel=window.getSelection(); if(sel.rangeCount>0){ const range=sel.getRangeAt(0); let content=range.toString(); range.deleteContents(); range.insertNode(document.createTextNode(content)); range.setStartAfter(range.startContainer); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } tbButtons.forEach(b=>b.classList.remove('active')); } else if(cmd==='removeFormat'){ restoreSelection(); const sel=window.getSelection(); if(sel.rangeCount>0){ const range=sel.getRangeAt(0); let content=range.toString(); range.deleteContents(); range.insertNode(document.createTextNode(content)); range.setStartAfter(range.startContainer); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } tbButtons.forEach(b=>b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600')); }
else{ document.execCommand(cmd,false,null); btn.classList.toggle('active'); } else{ document.execCommand(cmd,false,null); btn.classList.toggle('bg-blue-100'); btn.classList.toggle('text-gray-900'); btn.classList.toggle('border-blue-600'); }
newNote.focus(); saveSelection(); newNote.focus(); saveSelection();
}); });
}); });
+21 -138
View File
@@ -4,143 +4,26 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign In</title> <title>Sign In</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script> <script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
<script src="auth.js"></script> <script src="auth.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
padding: 3rem;
border-radius: 1rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
text-align: center;
max-width: 400px;
width: 90%;
}
h1 {
color: #333;
margin-bottom: 0.5rem;
font-size: 1.75rem;
}
p {
color: #666;
margin-bottom: 2rem;
}
button {
background: #0078d4;
color: white;
border: none;
padding: 0.875rem 2rem;
font-size: 1rem;
border-radius: 0.375rem;
cursor: pointer;
transition: background 0.2s;
font-weight: 500;
}
button:hover:not(:disabled) {
background: #005a9e;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.user-info {
display: none;
margin-top: 2rem;
text-align: left;
background: #f8f9fa;
padding: 1.5rem;
border-radius: 0.5rem;
}
.user-info h2 {
font-size: 1.125rem;
margin-bottom: 1rem;
color: #333;
}
.user-info p {
margin-bottom: 0.75rem;
color: #555;
word-break: break-all;
}
.user-info strong {
color: #333;
}
.error {
color: #d32f2f;
margin-top: 1rem;
display: none;
}
.logout-btn {
background: #6c757d;
margin-top: 1rem;
}
.logout-btn:hover {
background: #5a6268;
}
.notes-section {
display: none;
margin-top: 2rem;
text-align: left;
}
.notes-section h3 {
font-size: 1rem;
margin-bottom: 1rem;
color: #333;
}
textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 0.375rem;
font-family: inherit;
font-size: 0.9rem;
resize: vertical;
min-height: 80px;
}
textarea:focus {
outline: none;
border-color: #0078d4;
}
.submit-btn {
margin-top: 0.75rem;
width: 100%;
}
.success {
color: #2e7d32;
margin-top: 0.75rem;
display: none;
font-size: 0.9rem;
}
</style>
</head> </head>
<body> <body class="font-sans bg-gradient-to-br from-indigo-500 to-purple-600 min-h-screen flex items-center justify-center">
<div class="container"> <div class="bg-white p-12 rounded-2xl shadow-2xl text-center max-w-md w-[90%]">
<h1>Welcome</h1> <h1 class="text-gray-800 mb-2 text-3xl">Welcome</h1>
<p>Sign in with your Microsoft account</p> <p class="text-gray-600 mb-8">Sign in with your Microsoft account</p>
<button id="loginBtn" onclick="login()">Sign in with Microsoft</button> <button id="loginBtn" onclick="login()" class="bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed">Sign in with Microsoft</button>
<div class="error" id="error"></div> <div class="text-red-600 mt-4 hidden" id="error"></div>
<div class="user-info" id="userInfo"> <div class="hidden mt-8 text-left bg-gray-50 p-6 rounded-lg" id="userInfo">
<h2>Signed in</h2> <h2 class="text-lg mb-4 text-gray-800">Signed in</h2>
<p><strong>Name:</strong> <span id="displayName"></span></p> <p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Name:</strong> <span id="displayName"></span></p>
<p><strong>Email:</strong> <span id="email"></span></p> <p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Email:</strong> <span id="email"></span></p>
<button id="continueBtn" style="display:none;margin-top:1rem;" onclick="goToIndex()">Continue to Job Info</button> <button id="continueBtn" onclick="goToIndex()" class="hidden mt-4 bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed w-full">Continue to Job Info</button>
<button class="logout-btn" onclick="logout()">Sign out</button> <button onclick="logout()" class="bg-gray-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-gray-700 mt-4">Sign out</button>
</div> </div>
</div> </div>
@@ -157,11 +40,11 @@
} }
function showAuthedUI(displayName, email) { function showAuthedUI(displayName, email) {
loginBtn.style.display = 'none'; loginBtn.classList.add('hidden');
loginBtn.disabled = false; loginBtn.disabled = false;
loginBtn.textContent = 'Sign in with Microsoft'; loginBtn.textContent = 'Sign in with Microsoft';
userInfo.style.display = 'block'; userInfo.classList.remove('hidden');
continueBtn.style.display = 'block'; continueBtn.classList.remove('hidden');
document.getElementById('displayName').textContent = displayName || 'Unknown'; document.getElementById('displayName').textContent = displayName || 'Unknown';
document.getElementById('email').textContent = email || 'Not available'; document.getElementById('email').textContent = email || 'Not available';
} }
@@ -169,7 +52,7 @@
async function login() { async function login() {
loginBtn.disabled = true; loginBtn.disabled = true;
loginBtn.textContent = 'Signing in...'; loginBtn.textContent = 'Signing in...';
errorEl.style.display = 'none'; errorEl.classList.add('hidden');
try { try {
const authData = await pb.collection('users').authWithOAuth2({ const authData = await pb.collection('users').authWithOAuth2({
@@ -179,7 +62,7 @@
} catch (error) { } catch (error) {
console.error('Login failed:', error); console.error('Login failed:', error);
errorEl.textContent = error.message || 'Authentication failed. Please try again.'; errorEl.textContent = error.message || 'Authentication failed. Please try again.';
errorEl.style.display = 'block'; errorEl.classList.remove('hidden');
loginBtn.disabled = false; loginBtn.disabled = false;
loginBtn.textContent = 'Sign in with Microsoft'; loginBtn.textContent = 'Sign in with Microsoft';
} }
@@ -187,9 +70,9 @@
function logout() { function logout() {
pb.authStore.clear(); pb.authStore.clear();
loginBtn.style.display = 'block'; loginBtn.classList.remove('hidden');
userInfo.style.display = 'none'; userInfo.classList.add('hidden');
continueBtn.style.display = 'none'; continueBtn.classList.add('hidden');
loginBtn.disabled = false; loginBtn.disabled = false;
loginBtn.textContent = 'Sign in with Microsoft'; loginBtn.textContent = 'Sign in with Microsoft';
} }
+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>

After

Width:  |  Height:  |  Size: 650 B

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>

After

Width:  |  Height:  |  Size: 457 B