feat: initial release of Brightwheel Memories & Reports Exporter Chrome extension
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Brightwheel Exporter - Service Worker (Background Script)
|
||||
* Manifest V3
|
||||
*/
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
console.log('[Brightwheel Exporter] Extension installed.');
|
||||
});
|
||||
|
||||
// Handle incoming messages from popup, dashboard, or content scripts
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'OPEN_DASHBOARD') {
|
||||
const dashboardUrl = chrome.runtime.getURL('dashboard/dashboard.html');
|
||||
|
||||
// Check if dashboard is already open
|
||||
chrome.tabs.query({ url: dashboardUrl }, (tabs) => {
|
||||
if (tabs && tabs.length > 0) {
|
||||
chrome.tabs.update(tabs[0].id, { active: true });
|
||||
if (tabs[0].windowId) {
|
||||
chrome.windows.update(tabs[0].windowId, { focused: true });
|
||||
}
|
||||
} else {
|
||||
chrome.tabs.create({ url: dashboardUrl });
|
||||
}
|
||||
sendResponse({ success: true });
|
||||
});
|
||||
return true; // Keep message channel open for async response
|
||||
}
|
||||
|
||||
if (message.action === 'DOWNLOAD_BLOB_URL') {
|
||||
chrome.downloads.download({
|
||||
url: message.url,
|
||||
filename: message.filename,
|
||||
saveAs: true
|
||||
}, (downloadId) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
sendResponse({ success: false, error: chrome.runtime.lastError.message });
|
||||
} else {
|
||||
sendResponse({ success: true, downloadId });
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'GET_BRIGHTWHEEL_TABS') {
|
||||
chrome.tabs.query({ url: 'https://schools.mybrightwheel.com/*' }, (tabs) => {
|
||||
sendResponse({ tabs: tabs || [] });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Brightwheel Exporter - Content Script
|
||||
* Injected on https://schools.mybrightwheel.com/*
|
||||
*/
|
||||
|
||||
(function () {
|
||||
console.log('[Brightwheel Exporter] Content script initialized.');
|
||||
|
||||
// Extract student ID from URL if present
|
||||
function detectCurrentStudentId() {
|
||||
const match = window.location.pathname.match(/\/students\/([a-zA-Z0-9-]+)/);
|
||||
if (match && match[1]) {
|
||||
chrome.storage.local.set({ lastActiveStudentId: match[1] });
|
||||
return match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
detectCurrentStudentId();
|
||||
|
||||
// Watch for SPA URL changes
|
||||
let lastUrl = location.href;
|
||||
new MutationObserver(() => {
|
||||
const url = location.href;
|
||||
if (url !== lastUrl) {
|
||||
lastUrl = url;
|
||||
detectCurrentStudentId();
|
||||
}
|
||||
}).observe(document, { subtree: true, childList: true });
|
||||
|
||||
// Listen for messages from extension popup or dashboard
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.action === 'GET_PAGE_CONTEXT') {
|
||||
const studentId = detectCurrentStudentId();
|
||||
sendResponse({
|
||||
url: window.location.href,
|
||||
studentId: studentId,
|
||||
cookiesPresent: document.cookie.includes('_brightwheel_v2')
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.action === 'FETCH_WITH_CREDENTIALS') {
|
||||
// Execute fetch inside page context with credentials
|
||||
fetch(message.url, {
|
||||
method: message.method || 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
...(message.headers || {})
|
||||
},
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => '');
|
||||
return sendResponse({ success: false, status: res.status, statusText: res.statusText, body: errorText });
|
||||
}
|
||||
const json = await res.json();
|
||||
sendResponse({ success: true, status: res.status, data: json });
|
||||
})
|
||||
.catch((err) => {
|
||||
sendResponse({ success: false, error: err.message });
|
||||
});
|
||||
return true; // async
|
||||
}
|
||||
});
|
||||
|
||||
// Inject a discreet floating button on Brightwheel page
|
||||
function injectExporterButton() {
|
||||
if (document.getElementById('bw-exporter-floating-btn')) return;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'bw-exporter-floating-btn';
|
||||
btn.innerHTML = `
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
Export Brightwheel Data
|
||||
`;
|
||||
|
||||
Object.assign(btn.style, {
|
||||
position: 'fixed',
|
||||
bottom: '24px',
|
||||
right: '24px',
|
||||
zIndex: '999999',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
padding: '10px 18px',
|
||||
background: 'linear-gradient(135deg, #FF6B35 0%, #FF8E53 100%)',
|
||||
color: '#FFFFFF',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
border: 'none',
|
||||
borderRadius: '30px',
|
||||
boxShadow: '0 4px 16px rgba(255, 107, 53, 0.4), 0 2px 6px rgba(0, 0, 0, 0.1)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.25s ease',
|
||||
});
|
||||
|
||||
btn.addEventListener('mouseenter', () => {
|
||||
btn.style.transform = 'translateY(-2px) scale(1.03)';
|
||||
btn.style.boxShadow = '0 6px 20px rgba(255, 107, 53, 0.5), 0 3px 8px rgba(0, 0, 0, 0.15)';
|
||||
});
|
||||
|
||||
btn.addEventListener('mouseleave', () => {
|
||||
btn.style.transform = 'translateY(0) scale(1)';
|
||||
btn.style.boxShadow = '0 4px 16px rgba(255, 107, 53, 0.4), 0 2px 6px rgba(0, 0, 0, 0.1)';
|
||||
});
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
chrome.runtime.sendMessage({ action: 'OPEN_DASHBOARD' });
|
||||
});
|
||||
|
||||
document.body.appendChild(btn);
|
||||
}
|
||||
|
||||
// Inject when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', injectExporterButton);
|
||||
} else {
|
||||
injectExporterButton();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user