192 lines
6.4 KiB
JavaScript
192 lines
6.4 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 === 'INSPECT_LIVE_FEED') {
|
|
(async () => {
|
|
try {
|
|
const studentId = message.studentId || detectCurrentStudentId();
|
|
const apiListRes = await fetch(`https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities?page_size=25`, {
|
|
credentials: 'include',
|
|
headers: { 'Accept': 'application/json' }
|
|
}).then(r => r.json()).catch(e => ({ error: e.message }));
|
|
|
|
const rawActs = apiListRes?.activities || apiListRes?.items || [];
|
|
|
|
// Sample detail fetches for video, nap, checkin
|
|
const sampleDetails = {};
|
|
for (const act of rawActs.slice(0, 10)) {
|
|
const type = act.activity_type || act.action_type || act.type;
|
|
if (!sampleDetails[type] && act.id) {
|
|
const detail = await fetch(`https://schools.mybrightwheel.com/api/v1/students/${studentId}/activities/${act.id}`, {
|
|
credentials: 'include',
|
|
headers: { 'Accept': 'application/json' }
|
|
}).then(r => r.json()).catch(e => ({ error: e.message }));
|
|
sampleDetails[type] = detail;
|
|
}
|
|
}
|
|
|
|
const domCards = scrapeDomFeedCards();
|
|
|
|
sendResponse({
|
|
success: true,
|
|
studentId,
|
|
apiSampleActivities: rawActs.slice(0, 10),
|
|
sampleDetails,
|
|
domCards: domCards.slice(0, 20),
|
|
pageTitle: document.title,
|
|
currentUrl: window.location.href
|
|
});
|
|
} 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();
|
|
}
|
|
})();
|