Files
brightwheel-exporter/scripts/content_script.js
T

183 lines
5.9 KiB
JavaScript

/**
* 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 === 'SCRAPE_DOM_FEED') {
try {
const domActivities = scrapeDomFeedCards();
sendResponse({ success: true, activities: domActivities, htmlSample: document.body.innerHTML.substring(0, 5000) });
} catch (err) {
sendResponse({ success: false, error: err.message });
}
return true;
}
if (message.action === 'FETCH_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
}
});
// DOM Scraper for Brightwheel Web Feed
function scrapeDomFeedCards() {
const results = [];
// Broad selector covering common Brightwheel feed cards
const cardSelectors = [
'[data-testid*="activity"]',
'[data-testid*="feed"]',
'.activity-item',
'.feed-item',
'.timeline-item',
'article',
'[class*="ActivityCard"]',
'[class*="feed-card"]',
'[class*="FeedItem"]',
'[class*="TimelineItem"]'
];
let cards = document.querySelectorAll(cardSelectors.join(','));
if (!cards || cards.length === 0) {
// Fallback: look for cards by common structure
cards = document.querySelectorAll('div[class*="card"], div[class*="Card"]');
}
cards.forEach((card, index) => {
const text = card.innerText || '';
if (!text.trim()) return;
const cardId = card.getAttribute('data-id') || card.getAttribute('data-activity-id') || card.id || `dom_${index}`;
const img = card.querySelector('img[src*="cdn"], img[src*="brightwheel"], img[src*="cloudfront"], img[src*="s3"]');
const video = card.querySelector('video, source, a[href*=".mp4"], a[href*="video"]');
const videoUrl = video?.src || video?.getAttribute('src') || video?.getAttribute('href') || null;
const imageUrl = img?.src || null;
results.push({
dom_id: cardId,
raw_text: text,
image_url: imageUrl,
video_url: videoUrl,
class_name: card.className,
html_snippet: card.outerHTML.substring(0, 800)
});
});
return results;
}
// 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();
}
})();