Skip to content

Instantly share code, notes, and snippets.

@SagesOfRPG
Created May 2, 2026 02:42
Show Gist options
  • Select an option

  • Save SagesOfRPG/afa991f250e3b0698da7eac6359484cb to your computer and use it in GitHub Desktop.

Select an option

Save SagesOfRPG/afa991f250e3b0698da7eac6359484cb to your computer and use it in GitHub Desktop.
Mission Control v3 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sages Entertainment Mission Control</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet">
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#1e1640">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="assets/images/sages-logo.png">
<link rel="stylesheet" href="css/mobile.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: 'Nunito', Helvetica, sans-serif;
background: #0f0c1e;
color: #ede9f7;
min-height: 100vh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
#root { display: flex; min-height: 100vh; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0a0815; }
::-webkit-scrollbar-thumb { background: #2d2550; border-radius: 3px; }
/* Mobile Responsive Styles */
@media (max-width: 768px) {
#root { flex-direction: column !important; }
/* Sidebar becomes top header on mobile */
nav {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
position: relative !important;
height: auto !important;
min-height: auto !important;
flex-direction: column !important;
padding: 10px !important;
border-right: none !important;
border-bottom: 1px solid rgba(255,255,255,0.06) !important;
}
/* When collapsed, hide nav items */
nav.mobile-collapsed > div:nth-child(2),
nav.mobile-collapsed > div:last-of-type {
display: none !important;
}
/* Logo header adjustments */
nav > div:first-child {
padding: 10px !important;
border-bottom: none !important;
}
nav > div:first-child img {
width: 60px !important;
height: auto !important;
}
/* Navigation items - horizontal scroll */
nav > div:nth-child(2) {
display: flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
overflow-x: auto !important;
padding: 5px !important;
gap: 5px !important;
-webkit-overflow-scrolling: touch;
}
nav > div:nth-child(2)::-webkit-scrollbar {
display: none;
}
nav > div:nth-child(2) button {
flex: 0 0 auto !important;
min-width: auto !important;
padding: 8px 12px !important;
font-size: 14px !important;
white-space: nowrap !important;
}
/* Quick access hidden on mobile */
nav > div:last-of-type {
display: none !important;
}
/* Main content - full width, no margin */
main, .main-content {
margin-left: 0 !important;
width: 100% !important;
max-width: 100% !important;
padding: 15px !important;
}
/* Stats row - stack vertically */
.stats-row,
div[class*="stats"] {
flex-direction: column !important;
}
.stats-row > div,
div[class*="stats"] > div {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
}
/* Team grid - single column */
.team-grid,
div[style*="gridTemplateColumns"] {
grid-template-columns: 1fr !important;
}
/* Mission banner - smaller on mobile */
.mission-banner,
div[class*="mission"] {
padding: 15px !important;
}
.mission-banner img,
div[class*="mission"] img {
width: 80px !important;
}
/* Mobile menu button */
.mobile-menu-btn {
display: block !important;
position: fixed !important;
top: 10px !important;
right: 10px !important;
z-index: 1000 !important;
background: #1e1640 !important;
border: 1px solid #2d2550 !important;
color: #ede9f7 !important;
padding: 10px 15px !important;
border-radius: 8px !important;
font-size: 20px !important;
cursor: pointer !important;
}
}
/* Desktop - hide mobile menu button */
@media (min-width: 769px) {
.mobile-menu-btn {
display: none !important;
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"accentRed": "#E8294A",
"accentPurple": "#7B3FA8",
"accentTeal": "#26BFD9",
"accentOrange": "#F7921E",
"bgBody": "#22203a",
"bgSidebar": "#1e1640",
"bgCard": "#2e2e48",
"fontScale": 1
} /*EDITMODE-END*/;
// ─── SAMPLE DATA ────────────────────────────────────────────────────────────
const SCHEDULE = [
{ time: "05:30 AM", title: "Wayne Wake Up!!!!", tag: "Sages", done: false },
{ time: "06:00 AM", title: "Pack the Van!!", tag: "Sages", done: false },
{ time: "06:30 AM", title: "Wake up Alice and get her Ready.", tag: "Sages", done: false },
{ time: "07:00 AM", title: "Leave for Store and get food.", tag: "Sages", done: false },
{ time: "08:00 AM", title: "Travel Chalk the Walk - Rock Row", tag: "Sages", done: false },
{ time: "09:00 AM", title: "Setup Chalk the Walk - Rock Row", tag: "Sages", done: false },
{ time: "10:00 AM", title: "Chalk the Walk - Rock Row (Chalk Art, Mural, Balloons)", tag: "Sages", done: true },
{ time: "01:00 PM", title: "Chalk the Walk - Cleanup", tag: "Sages", done: false }];
const OPEN_LOOPS = {
urgent: [],
invoices: [
{ title: "Rock Row — May 9th Summer Event", sub: "Send invoice for May 9th event" },
{ title: "Chalk the Walk — Rock Row", sub: "Send invoice for today's Chalk the Walk" },
{ title: "Dover Library Summer Magic Show", sub: "Send invoice" },
{ title: "Seacoast Science Center Magic Show", sub: "Send invoice" },
{ title: "Seacoast Science Center Bubbles at World Ocean Day", sub: "Send invoice" }],
active: [
{ title: "Van Kit Setup", sub: "Create organized van kit with: trash bags, pens, scissors, etc." },
{ title: "3D Print — Abbey Rock Row Stencils", sub: "Print Abbey stencils for Rock Row" },
{ title: "Chalk the Walk — Anna Payment", sub: "Pay Anna for the Chalk the Walk mural work" },
{ title: "Alicia MacDonald — Kittery Community Center", sub: "Send invoice for birthday party" },
{ title: "Larry — Follow Up", sub: "Send follow-up email." },
{ title: "Dover Public Library", sub: "Write description" }]
};
const PARKING_LOT = [
{ title: "Dino robot skin — eye decision & purchase", when: "Later", sub: "Choose eyes, order" },
{ title: "Lilac City Fun Fest Proposal", when: "Later", sub: "Send quote to Nicole Lee" },
{ title: "Alicia MacDonald party", when: "Oct 3", sub: "Kittery Community Center" }];
const THREE_THINGS = [
{ title: "Deposit checks / send 2 invoices", meta: "Today · 💰 Cash Flow" },
{ title: "Friends in Action video", meta: "In Progress · Due Apr 18" },
{ title: "Caitlin Rochester grant invoice", meta: "Due TODAY · $2,800" }];
const TEAM = [
{ name: "Wayne", role: "Founder & Chief of Staff", emoji: "🎩", photo: "assets/wayne.png", color: "#E8294A", items: ["Mission Control v3 development", "Caitlin Rochester invoice", "Cash flow management"], email: "hello@sagesentertainment.com", status: "active" },
{ name: "Kali", role: "Co-Founder & Magic", emoji: "✨", photo: "assets/kali.png", color: "#7B3FA8", items: ["Managing health and recovery", "Planning upcoming performances", "Supporting operations"], email: "kali@sagesentertainment.com", status: "limited" },
{ name: "Shine", role: "Virtual Assistant", emoji: "🌟", photo: "assets/shine.png", color: "#26BFD9", items: ["Social media graphics", "Asana task management", "Content creation support"], email: null, status: "active" },
{ name: "Joey", role: "Operations Support", emoji: "🤝", photo: "assets/joey.png", color: "#F7921E", items: ["Invoice processing", "Body doubling sessions", "Administrative support"], email: "jpantelakos86@gmail.com", status: "active" },
{ name: "Colby", role: "Event Assistant", emoji: "🎪", photo: "assets/colby.png", color: "#26BFD9", items: ["Available for upcoming events", "Helping with performances"], email: "c.breen.writes@gmail.com", status: "active" },
{ name: "Caleigh", role: "Business Coach", emoji: "💼", photo: "assets/caleigh.png", color: "#F7921E", items: ["Strategic roadmapping", "Event planning assistance", "Accountability"], email: "caleigh@tartantactics.com", status: "active" },
{ name: "Alice", role: "Smile Maker", emoji: "🌸", photo: "assets/alice.png", color: "#E8294A", items: ["Bringing joy to events", "Being adorable"], email: null, status: "active" },
{ name: "Snickers", role: "Sidekick 🦭", emoji: "🐾", photo: "assets/snickers.png", color: "#7B3FA8", items: ["Moral support", "Cuddle breaks"], email: null, status: "active" }];
// ─── CONTEXT-AWARE RACCOON WISDOM SYSTEM ─────────────────────────────────────
// Context detection helpers
function getTimeContext() {
const hour = new Date().getHours();
if (hour < 6) return { timeOfDay: "night", greeting: "Night owl" };
if (hour < 9) return { timeOfDay: "morning", greeting: "Good morning" };
if (hour < 12) return { timeOfDay: "pre-lunch", greeting: "Morning" };
if (hour < 14) return { timeOfDay: "lunch", greeting: "Lunch time" };
if (hour < 17) return { timeOfDay: "afternoon", greeting: "Afternoon" };
if (hour < 20) return { timeOfDay: "evening", greeting: "Evening" };
return { timeOfDay: "night", greeting: "Good evening" };
}
function getDayContext() {
const day = new Date().getDay();
const isWeekend = day === 0 || day === 6;
const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
return { isWeekend, dayName: dayNames[day], day };
}
function getOpenLoopsContext(openLoopsCount) {
if (openLoopsCount === 0) return { level: "empty", message: "All caught up!" };
if (openLoopsCount <= 3) return { level: "low", message: "Just a few things" };
if (openLoopsCount <= 8) return { level: "medium", message: "Steady flow" };
if (openLoopsCount <= 15) return { level: "high", message: "Busy day" };
return { level: "overwhelming", message: "Heavy load" };
}
function getThreeThingsContext(things) {
const done = things.filter(t => t.status === "DONE" || t.status === "done").length;
const inProgress = things.filter(t => t.status === "In Progress" || t.status === "in-progress").length;
const notStarted = things.filter(t => t.status === "Not Started" || t.status === "not-started").length;
return { done, inProgress, notStarted, total: things.length };
}
// Generate context-aware wisdom
// This file contains the updated generateContextualWisdom function with Wayne's raccoon voice
// Copy this into index.html to replace the existing function
function generateContextualWisdom(context) {
const { timeOfDay, isWeekend, openLoops, threeThings, hasUpcomingEvent, eventType } = context;
const wisdoms = [];
// Time-based wisdom - raccoon morning/evening vibes
if (timeOfDay === "morning") {
wisdoms.push(
{ mood: "wise", text: "Coffee's brewing, raccoon's prowling. What's the ONE shiny thing that'll make today a win?", emoji: "☕", label: "Morning Focus" },
{ mood: "excited", text: "Fresh trash day! 🦝 The hoard is empty and ready. Pick your first shiny before the squirrels wake up!", emoji: "🌅", label: "Morning Hype" },
{ mood: "curious", text: "Meds taken? ☕ Coffee ready? Good. Now... which of those 3 things feels LEAST like a monster?", emoji: "🦝", label: "Morning Check" }
);
} else if (timeOfDay === "lunch") {
wisdoms.push(
{ mood: "wise", text: "Midday trash check: Have you touched your 3 shinies yet? No shame, just awareness! 🦝", emoji: "🍜", label: "Lunch Check" },
{ mood: "cozy", text: "Lunch break = perfect time for a tiny trash dash. One email, one invoice, one win!", emoji: "🥢", label: "Lunch Break" }
);
} else if (timeOfDay === "afternoon") {
wisdoms.push(
{ mood: "alert", text: "Afternoon energy dip? Classic squirrel trap. 🐿️ Perfect time for a 5-minute trash dash!", emoji: "⚡", label: "Afternoon Boost" },
{ mood: "wise", text: "Day's not over yet. What's one shiny you could grab before dinner? Just one!", emoji: "🎯", label: "Afternoon Focus" }
);
} else if (timeOfDay === "evening") {
wisdoms.push(
{ mood: "cozy", text: "Evening mode: Time to guard the hoard. Family > everything. The open loops can wait. 💙", emoji: "🌙", label: "Evening Wind-down" },
{ mood: "wise", text: "Put down the trash. Pick up Alice. Be present. That's the real magic trick. ✨", emoji: "🏠", label: "Family First" }
);
}
// Weekend vs weekday - trash party vibes
if (isWeekend) {
wisdoms.push(
{ mood: "excited", text: "WEEKEND! 🎉 Trash party time! Or... rest mode. Your call, chief raccoon. 🦝", emoji: "🎊", label: "Weekend Vibes" },
{ mood: "cozy", text: "Weekends are for family adventures and recharging the magic batteries. The hoard can wait.", emoji: "✨", label: "Weekend Rest" },
{ mood: "wise", text: "Saturday shinies: Board games with Alice? Rubik's cube? Family time IS productive time.", emoji: "🎲", label: "Weekend Joy" }
);
} else {
wisdoms.push(
{ mood: "wise", text: "Weekday hustle: Small trash piles add up. One invoice, one email, one shiny at a time.", emoji: "📈", label: "Weekday Grind" },
{ mood: "alert", text: "Monday through Friday = showtime. 🎭 You've got this, magician. Make 'em disappear!", emoji: "🎪", label: "Showtime" }
);
}
// Open loops based - the trash hoard metaphor
if (openLoops?.level === "empty") {
wisdoms.push(
{ mood: "excited", text: "ZERO open loops?! Wayne, you're a TRASH LEGEND! 🏆 Time to celebrate or... find new shinies?", emoji: "🏆", label: "Victory Lap" },
{ mood: "wise", text: "Hoard cleared! Empty bin! This is what victory smells like. (Like coffee, probably.)", emoji: "☕", label: "Empty Hoard" }
);
} else if (openLoops?.level === "low") {
wisdoms.push(
{ mood: "wise", text: `Just ${openLoops.count} things in the bin? You could clear this before snack time! 🦝`, emoji: "✅", label: "Almost Clear" },
{ mood: "excited", text: "Light hoard today! These are the good days — quick wins, fast clears, happy raccoon!", emoji: "🎯", label: "Light Load" }
);
} else if (openLoops?.level === "high" || openLoops?.level === "overwhelming") {
wisdoms.push(
{ mood: "alert", text: `${openLoops.count} open loops? That's a hefty hoard. 🗑️ Let's pick the 3 shinies that matter MOST.`, emoji: "🚨", label: "Priority Alert" },
{ mood: "wise", text: "Heavy trash load detected. Remember: you don't clear the whole bin at once. One shiny. Then another.", emoji: "🗑️", label: "Gentle Reminder" },
{ mood: "cozy", text: "Big hoard energy today. Deep breath. You've juggled worse. Start with the smallest shiny.", emoji: "🌙", label: "Overwhelm Mode" }
);
}
// 3 Things progress - specific encouragement
if (threeThings) {
if (threeThings.done === threeThings.total && threeThings.total > 0) {
wisdoms.push(
{ mood: "excited", text: "ALL 3 THINGS DONE! 🔥 Wayne, you absolute magician! The hoard is CLEAR! 🎉", emoji: "🔥", label: "3/3 Complete" },
{ mood: "excited", text: "3 for 3! 🏆 That's not luck — that's SKILL. You turned a to-do list into a DONE list!", emoji: "🪄", label: "Perfect Score" }
);
} else if (threeThings.done > 0) {
wisdoms.push(
{ mood: "wise", text: `${threeThings.done} of ${threeThings.total} shinies collected! Momentum is building. Keep that streak! 🦝`, emoji: "💪", label: "Progress Mode" },
{ mood: "excited", text: "Halfway through the hoard! The trash pile is shrinking. You're basically a productivity wizard!", emoji: "🧙", label: "Halfway There" }
);
} else if (threeThings.inProgress > 0) {
wisdoms.push(
{ mood: "alert", text: "Something in progress... don't let it become a 'later monster'! 👹 Finish the loop?", emoji: "🔄", label: "Finish It" },
{ mood: "wise", text: "Started but not done? The 'almost finished' pile is where shinies go to hide. Grab it!", emoji: "🎯", label: "Almost Done" }
);
} else if (threeThings.notStarted === threeThings.total && threeThings.total > 0) {
wisdoms.push(
{ mood: "alert", text: "3 shinies waiting in the bin... which one feels LEAST scary? Start there! 🦝", emoji: "🎯", label: "Start Small" },
{ mood: "curious", text: "Haven't touched the hoard yet? No judgment. Just open ONE. That's it. The rest can wait.", emoji: "📂", label: "First Step" }
);
}
}
// Calendar event based - performance and family awareness
if (hasUpcomingEvent) {
if (eventType === "meeting") {
wisdoms.push(
{ mood: "alert", text: "Meeting coming up! 🎩 Prep mode: review notes, grab water, you've got this!", emoji: "📅", label: "Meeting Prep" }
);
} else if (eventType === "performance") {
wisdoms.push(
{ mood: "excited", text: "Show time approaching! 🎪 The magic is ready. Break a leg out there! ✨", emoji: "🎪", label: "Performance Prep" }
);
} else if (eventType === "family") {
wisdoms.push(
{ mood: "cozy", text: "Family time ahead! 💙 The best kind of busy. Be present and make memories.", emoji: "👨‍👩‍👧", label: "Family Time" }
);
} else if (eventType === "dinner") {
wisdoms.push(
{ mood: "cozy", text: "Dinner plans! Hope you bring back treats... for the raccoon. 🍜", emoji: "🍽️", label: "Dinner Time" }
);
}
}
// Wayne-specific wisdoms - magic, family, ADHD, business
wisdoms.push(
{ mood: "curious", text: "Remember: You turned a passion for magic into a BUSINESS. That's not luck — that's SKILL. 🪄", emoji: "🎩", label: "Pride Moment" },
{ mood: "wise", text: "Cash flow, creativity flow, family flow — you're juggling it all. That's the real magic trick.", emoji: "🎩", label: "Juggling Act" },
{ mood: "cozy", text: "Even on slow days, you're building something amazing. One show, one smile, one family moment.", emoji: "🌟", label: "Gentle Encouragement" },
{ mood: "alert", text: "Squirrel! 🐿️ Wait, no — stay focused. What's the ONE shiny thing right now?", emoji: "🦝", label: "Squirrel Alert" },
{ mood: "wise", text: "Procrastination is just your brain saying 'this feels too big.' Shrink it. One tiny paw-step.", emoji: "🐾", label: "ADHD Wisdom" },
{ mood: "excited", text: "Wayne + Kali + Alice = unstoppable magic-making family. Never forget that! 💙", emoji: "💙", label: "Family Power" },
{ mood: "curious", text: "Did you know? Raccoons have excellent memory for where they left shiny things. You do too!", emoji: "🧠", label: "Fun Fact" },
{ mood: "wise", text: "You built a whole business while showing up for your family. That's the real magic trick. ✨", emoji: "🌙", label: "Cozy Mode" },
{ mood: "alert", text: "Step right up — that urgent deadline won't close itself. Let's make it disappear! ✨", emoji: "🚨", label: "Alert Mode" },
{ mood: "wise", text: "Yesterday's trash is today's treasure. Those invoices? Pure gold waiting in the bin.", emoji: "🎩", label: "Wise Mode" },
{ mood: "excited", text: "SHINY ALERT!! Things done today. Wayne, you absolute magician. 🪄", emoji: "🎉", label: "Hype Mode" },
{ mood: "curious", text: '"Shop for Chalk" — what\'s one tiny step toward this shiny?', emoji: "🦝", label: "Curious Mode" }
);
return wisdoms;
}
// Get a random wisdom from the context-aware set
function getRandomContextualWisdom(context) {
const wisdoms = generateContextualWisdom(context);
return wisdoms[Math.floor(Math.random() * wisdoms.length)];
}
// Parse open-loops.md markdown file
function parseOpenLoopsMarkdown(md) {
const sections = { urgent: [], invoices: [], active: [] };
const lines = md.split('\n');
let currentSection = null;
let currentItem = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.includes('🚨 URGENT')) currentSection = 'urgent';
else if (line.includes('💰 INVOICES')) currentSection = 'invoices';
else if (line.includes('🟣 ACTIVE')) currentSection = 'active';
else if (line.startsWith('###') && currentSection) {
// Start a new item
if (currentItem) sections[currentSection].push(currentItem);
// Extract title - remove ### and optional Job:/Invoice:/Invoice #N: prefix
let title = line.replace(/^###\s*/, '').trim();
const type = /^Invoice(\s*#\d+)?:/.test(title) ? 'Invoice' : 'Job';
title = title.replace(/^(Job:|Invoice\s*(?:#\d+)?:)\s*/, '').trim();
currentItem = {
title: title,
due: '',
sub: '',
done: false,
type: type,
tasks: [],
amount: '',
date: '',
notes: ''
};
}
else if (line.startsWith('- [ ]') && currentItem) {
currentItem.tasks.push(line.replace('- [ ]', '').trim());
// Use first task as subtitle if not set
if (!currentItem.sub) currentItem.sub = line.replace('- [ ]', '').trim();
}
else if (line.startsWith('- **Amount:**') && currentItem) {
currentItem.amount = line.replace('- **Amount:**', '').trim();
}
else if (line.startsWith('- **Date:**') && currentItem) {
currentItem.date = line.replace('- **Date:**', '').trim();
currentItem.due = 'Due: ' + currentItem.date;
}
else if (line.startsWith('- **Notes:**') && currentItem) {
currentItem.notes = line.replace('- **Notes:**', '').trim();
}
else if (line.startsWith('- **Event:**') && currentItem) {
currentItem.event = line.replace('- **Event:**', '').trim();
}
else if (line.startsWith('---') && currentItem && currentSection) {
if (currentItem) sections[currentSection].push(currentItem);
currentItem = null;
}
}
// Push final item if file doesn't end with ---
if (currentItem && currentSection) sections[currentSection].push(currentItem);
return sections;
}
// Parse parking-lot.md markdown file
function parseParkingLotMarkdown(md) {
const items = [];
const lines = md.split('\n');
let inParkedTable = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Check for parked table start
if (line.includes('## 🟢 PARKED')) {
inParkedTable = true;
continue;
}
// Check for table end (next ## or empty line after table)
if (inParkedTable && line.startsWith('## ')) {
inParkedTable = false;
continue;
}
// Parse table rows (skip header and separator)
if (inParkedTable && line.startsWith('|') && !line.includes('Item') && !line.includes('---')) {
const cells = line.split('|').map(c => c.trim()).filter(c => c);
if (cells.length >= 2) {
items.push({
title: cells[0],
when: cells[1] || 'Later',
sub: cells[2] || ''
});
}
}
}
return items;
}
// Parse team-current-working-on.md markdown file
function parseTeamMarkdown(md) {
const members = [];
const lines = md.split('\n');
let currentMember = null;
let inWorkingOn = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Parse member header (## Name) - match emoji after ##
// Emoji can be multi-byte, so we match non-space chars until space
const memberMatch = line.match(/^##\s+(.+?)\s+(.+)/);
if (memberMatch) {
// Save previous member
if (currentMember) {
members.push(currentMember);
}
const emoji = memberMatch[1].trim();
const name = memberMatch[2].trim();
currentMember = {
name: name || 'Unknown',
emoji: emoji || '👤',
role: '',
items: [],
status: 'active'
};
inWorkingOn = false;
}
// Parse role
else if (line.startsWith('**Role:**') && currentMember) {
currentMember.role = line.replace('**Role:**', '').trim();
}
// Parse working on section
else if (line.includes('Currently Working On:') && currentMember) {
inWorkingOn = true;
}
// Parse working on items
else if (inWorkingOn && line.startsWith('- ') && currentMember) {
currentMember.items.push(line.replace('- ', '').trim());
}
// Parse status
else if (line.startsWith('**Status:**') && currentMember) {
const statusText = line.replace('**Status:**', '').trim();
if (statusText.includes('🟡') || statusText.includes('Limited')) {
currentMember.status = 'limited';
} else if (statusText.includes('🔴')) {
currentMember.status = 'unavailable';
} else {
currentMember.status = 'active';
}
inWorkingOn = false;
}
// Section break - end working on
else if (line.startsWith('---') && currentMember) {
if (currentMember) {
members.push(currentMember);
}
currentMember = null;
inWorkingOn = false;
}
}
// Push final member
if (currentMember) {
members.push(currentMember);
}
return members;
}
// Parse 3-things.md markdown file
function parseThreeThingsMarkdown(md) {
const things = [];
const lines = md.split('\n');
let currentThing = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Match ## 1. ## 2. or ## 3. headers
const match = line.match(/^##?\s*(\d+)\.\s*(.+)/);
if (match) {
// Save previous thing
if (currentThing) {
things.push(currentThing);
}
currentThing = {
title: match[2].trim(),
priority: '',
category: '',
status: 'Not Started',
meta: ''
};
}
// Parse priority
else if (line.startsWith('- **Priority:**') && currentThing) {
currentThing.priority = line.replace('- **Priority:**', '').trim();
if (currentThing.priority === 'High') currentThing.meta = 'High Priority';
else if (currentThing.priority === 'Medium') currentThing.meta = 'Medium Priority';
}
// Parse category
else if (line.startsWith('- **Category:**') && currentThing) {
currentThing.category = line.replace('- **Category:**', '').trim();
}
// Parse status
else if (line.startsWith('- **Status:**') && currentThing) {
const statusText = line.replace('- **Status:**', '').trim();
if (statusText.includes('✅') || statusText.includes('COMPLETE')) {
currentThing.status = 'DONE';
} else if (statusText.includes('⏳')) {
currentThing.status = 'Not Started';
} else if (statusText.includes('🔄')) {
currentThing.status = 'In Progress';
}
}
// Parse notes (extract emoji and short version)
else if (line.startsWith('- **Notes:**') && currentThing) {
const notes = line.replace('- **Notes:**', '').trim();
currentThing.meta = notes.substring(0, 40) + (notes.length > 40 ? '...' : '');
}
// Parse notes without label
else if (line.startsWith('- ') && currentThing && !line.includes('**')) {
currentThing.meta = line.replace('- ', '').trim().substring(0, 40);
}
}
// Push final thing
if (currentThing) {
things.push(currentThing);
}
return things;
}
// Legacy static wisdoms as fallback
const STATIC_WISDOMS = [
{ mood: "curious", text: '"Shop for Chalk" — what\'s one tiny step toward this shiny?', emoji: "🦝", label: "Curious Mode" },
{ mood: "wise", text: "Yesterday's trash is today's treasure. Those 5 invoices? Pure gold waiting in the bin.", emoji: "🎩", label: "Wise Mode" },
{ mood: "excited", text: "SHINY ALERT!! Things done today. Wayne, you absolute magician. 🪄", emoji: "🎉", label: "Hype Mode" },
{ mood: "cozy", text: "You built a whole business while showing up for your family. That's the real magic trick.", emoji: "🌙", label: "Cozy Mode" },
{ mood: "alert", text: "Step right up — that urgent deadline won't close itself. Let's make it disappear! ✨", emoji: "🚨", label: "Alert Mode" }];
const NAV_ITEMS = [
{ id: "overview", label: "Overview", icon: "◉", color: "#E8294A", href: "./" },
{ id: "calendar", label: "Calendar", icon: "▦", color: "#26BFD9", href: "./html/calendar.html" },
{ id: "open-loops", label: "Open Loops", icon: "↺", color: "#F7921E", href: "./html/open-loops.html" },
{ id: "parking-lot", label: "Parking Lot", icon: "◼", color: "#7B3FA8", href: "./html/parking-lot.html" },
{ id: "team", label: "Team", icon: "✦", color: "#26BFD9", href: "./html/team.html" },
{ id: "activity", label: "Activity", icon: "◆", color: "#F9C01C", href: "./html/activity.html" },
{ id: "settings", label: "Settings", icon: "◎", color: "#c0b4c0", href: "./html/settings.html" }];
const QUICK_LINKS = [
{ label: "Launch HoneyBook", icon: "🚀", href: "https://honeybook.com" },
{ label: "Email", icon: "📧", href: "https://gmail.com" },
{ label: "Sages Events Sheet", icon: "📊", href: "#" },
{ label: "Connecteam", icon: "📅", href: "https://app.connecteam.com" }];
// ─── SPARKLE DECORATION ────────────────────────────────────────────────────
function Sparkles({ count = 12, colors = ["#E8294A", "#26BFD9", "#F7921E", "#7B3FA8", "#F9C01C"] }) {
const shapes = React.useMemo(() => Array.from({ length: count }, (_, i) => ({
left: Math.random() * 100,
top: Math.random() * 100,
color: colors[i % colors.length],
size: 4 + Math.random() * 8,
shape: ["✦", "◆", "▲", "●"][i % 4],
delay: Math.random() * 3,
dur: 2 + Math.random() * 2
})), []);
return (
<div style={{ position: "absolute", inset: 0, pointerEvents: "none", overflow: "hidden" }}>
{shapes.map((s, i) =>
<span key={i} style={{
position: "absolute",
left: s.left + "%",
top: s.top + "%",
color: s.color,
fontSize: s.size,
opacity: 0.6,
animation: `twinkle ${s.dur}s ${s.delay}s ease-in-out infinite alternate`
}}>{s.shape}</span>
)}
<style>{`@keyframes twinkle { from { opacity: 0.2; transform: scale(0.8) rotate(-10deg); } to { opacity: 0.8; transform: scale(1.2) rotate(10deg); } }`}</style>
</div>);
}
// ─── SIDEBAR ────────────────────────────────────────────────────────────────
function Sidebar({ activePage, onNav, tweak, mobileCollapsed, isMobile, openLoopsCount }) {
// Mobile sidebar styles
const sidebarStyles = isMobile
? {
width: "100%",
minWidth: "100%",
background: tweak.bgSidebar,
borderBottom: "2px solid #2d2550",
display: "flex",
flexDirection: "column",
position: "relative",
height: mobileCollapsed ? "auto" : "auto",
overflow: "visible",
zIndex: 50,
padding: "15px 10px 10px"
}
: {
width: 240,
minWidth: 240,
background: tweak.bgSidebar,
borderRight: "1px solid rgba(255,255,255,0.06)",
display: "flex",
flexDirection: "column",
position: "fixed",
height: "100vh",
overflowY: "auto",
zIndex: 50
};
return (
<nav style={sidebarStyles}>
{/* Logo header */}
<div style={{
padding: isMobile ? "0" : "24px 16px 16px",
borderBottom: isMobile ? "none" : "1px solid rgba(255,255,255,0.08)",
background: "linear-gradient(180deg, #14103000 0%, transparent 100%)",
position: "relative",
overflow: "hidden",
display: "flex",
flexDirection: "column",
alignItems: "center"
}}>
{/* Tiny marquee dots decoration - hidden on mobile */}
{!isMobile && (
<div style={{ position: "absolute", top: 8, left: 0, right: 0, display: "flex", justifyContent: "center", gap: 7, opacity: 0.3 }}>
{["#E8294A", "#F7921E", "#F9C01C", "#26BFD9", "#7B3FA8", "#E8294A", "#F7921E"].map((c, i) =>
<div key={i} style={{ width: 4, height: 4, borderRadius: "50%", background: c }} />
)}
</div>
)}
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
<img src="assets/images/sages-logo.png" alt="Sages Entertainment" style={{
width: isMobile ? 70 : 110, height: "auto",
filter: "drop-shadow(0 4px 20px rgba(232,41,74,0.35)) drop-shadow(0 2px 8px rgba(123,63,168,0.4))",
animation: "float 5s ease-in-out infinite"
}} />
<div style={{
fontFamily: "'Fredoka', sans-serif", fontSize: 14, fontWeight: 700,
color: "#e0d8ff", letterSpacing: "0.12em", textTransform: "uppercase",
textShadow: "0 0 12px rgba(196,181,253,0.4)"
}}>Mission Control</div>
<div style={{ display: "flex", alignItems: "center", gap: 5 }}>
<span style={{ fontSize: 12, color: "#9080c0", fontFamily: "Nunito, sans-serif", fontWeight: 600 }}>🦝 Raccoon OS-Powered™</span>
</div>
</div>
</div>
{/* Navigation */}
{!mobileCollapsed && (
<div style={{
padding: isMobile ? "10px 5px" : "14px 10px",
flex: 1,
display: isMobile ? "flex" : "block",
flexDirection: isMobile ? "row" : "column",
flexWrap: isMobile ? "wrap" : "nowrap",
justifyContent: isMobile ? "center" : "flex-start",
gap: isMobile ? "8px" : "0"
}}>
{NAV_ITEMS.map((item) => {
const isActive = activePage === item.id;
const baseStyle = {
display: "flex", alignItems: "center", gap: 10,
width: isMobile ? "auto" : "100%",
padding: isMobile ? "10px 16px" : "11px 14px",
borderRadius: 10,
border: "none", cursor: "pointer",
marginBottom: isMobile ? 0 : 3,
marginRight: isMobile ? 0 : 0,
background: isActive ? item.color + "25" : "transparent",
color: isActive ? item.color : "#b0a0cc",
fontFamily: "'Fredoka', sans-serif",
fontSize: isMobile ? 14 : 19,
fontWeight: isActive ? 700 : 500,
transition: "all 0.18s",
textAlign: "left",
position: "relative",
flex: isMobile ? "1 1 45%" : "none",
minWidth: isMobile ? "140px" : "auto",
maxWidth: isMobile ? "200px" : "none",
textDecoration: "none"
};
const content = (
<React.Fragment>
{isActive &&
<div style={{ position: "absolute", left: 0, top: "18%", bottom: "18%", width: 3, background: item.color, borderRadius: "0 3px 3px 0", boxShadow: `0 0 8px ${item.color}` }} />
}
<span style={{
fontSize: 16, width: 22, height: 22, textAlign: "center", flexShrink: 0,
display: "flex", alignItems: "center", justifyContent: "center",
color: isActive ? item.color : "#9080c0",
fontWeight: 700
}}>{item.icon}</span>
<span style={{ flex: 1 }}>{item.label}</span>
{(item.badge || (item.id === 'open-loops' && openLoopsCount > 0)) &&
<span style={{
background: tweak.accentOrange, color: "#ffffff",
fontSize: 11, fontWeight: 800, padding: "2px 8px",
borderRadius: 10, fontFamily: "Nunito, sans-serif",
boxShadow: `0 0 8px ${tweak.accentOrange}88`
}}>{item.badge || openLoopsCount}</span>
}
</React.Fragment>
);
if (item.href && item.id !== 'overview') {
return (
<a key={item.id} href={item.href} style={baseStyle}
onMouseEnter={(e) => {if (!isActive) {e.currentTarget.style.background = "rgba(255,255,255,0.06)";e.currentTarget.style.color = "#fff";}}}
onMouseLeave={(e) => {if (!isActive) {e.currentTarget.style.background = "transparent";e.currentTarget.style.color = "#b0a0cc";}}}>
{content}
</a>
);
}
return (
<button key={item.id} onClick={() => onNav(item.id)} style={baseStyle}
onMouseEnter={(e) => {if (!isActive) {e.currentTarget.style.background = "rgba(255,255,255,0.06)";e.currentTarget.style.color = "#fff";}}}
onMouseLeave={(e) => {if (!isActive) {e.currentTarget.style.background = "transparent";e.currentTarget.style.color = "#b0a0cc";}}}>
{content}
</button>
);
})}
</div>
)}
{/* Quick Access */}
<div style={{ padding: "12px 10px 20px", borderTop: "1px solid rgba(255,255,255,0.08)" }}>
<div style={{
fontFamily: "'Fredoka', sans-serif", fontSize: 14, fontWeight: 700,
color: "#a090c8", textTransform: "uppercase", letterSpacing: "0.08em",
paddingLeft: 12, marginBottom: 8
}}>Quick Access</div>
{QUICK_LINKS.map((link) =>
<a key={link.label} href={link.href} target="_blank" rel="noopener noreferrer" style={{
display: "flex", alignItems: "center", gap: 10,
padding: "10px 12px", borderRadius: 8, color: "#c0b0e0",
textDecoration: "none", fontSize: 15, fontFamily: "Nunito, sans-serif",
fontWeight: 600, transition: "all 0.15s"
}}
onMouseEnter={(e) => {e.currentTarget.style.background = "rgba(255,255,255,0.06)";e.currentTarget.style.color = "#fff";}}
onMouseLeave={(e) => {e.currentTarget.style.background = "transparent";e.currentTarget.style.color = "#c0b0e0";}}>
<span style={{ fontSize: 15 }}>{link.icon}</span>
<span>{link.label}</span>
</a>
)}
{/* Confetti strip - hidden on mobile */}
{!isMobile && (
<div style={{ display: "flex", gap: 4, justifyContent: "center", marginTop: 20, opacity: 0.4 }}>
{["#E8294A", "◆", "#26BFD9", "◆", "#F7921E", "◆", "#7B3FA8", "◆", "#F9C01C"].map((c, i) =>
c === "◆" ?
<span key={i} style={{ color: "#2d2050", fontSize: 8 }}>◆</span> :
<div key={i} style={{ width: 6, height: 6, borderRadius: "50%", background: c, boxShadow: `0 0 4px ${c}` }} />
)}
</div>
)}
</div>
<style>{`@keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-5px)} }`}</style>
</nav>);
}
// ─── MISSION BANNER ─────────────────────────────────────────────────────────
function MarqueeDots({ color, count = 10 }) {
return (
<div style={{ display: "flex", gap: 10, alignItems: "center" }}>
{Array.from({ length: count }).map((_, i) =>
<div key={i} style={{
width: 7, height: 7, borderRadius: "50%",
background: color,
boxShadow: `0 0 6px ${color}, 0 0 12px ${color}55`,
animation: `bulb ${1.2 + i % 3 * 0.4}s ${i * 0.12}s ease-in-out infinite alternate`
}} />
)}
<style>{`@keyframes bulb { from{opacity:0.3;transform:scale(0.85)} to{opacity:1;transform:scale(1.1)} }`}</style>
</div>);
}
function MissionBanner({ tweak, isMobile }) {
return (
<div className="mission-banner" style={{
position: "relative",
background: "linear-gradient(135deg, #1e0f3f 0%, #110d28 50%, #0c1528 100%)",
borderRadius: 20,
padding: "0 0 32px",
overflow: "hidden",
border: "1px solid #2d2060",
marginBottom: 28,
boxShadow: "0 8px 40px rgba(123,63,168,0.2)"
}}>
{/* Marquee dot rows top & bottom */}
{!isMobile && (
<div style={{
padding: isMobile ? "8px 12px" : "10px 24px",
borderBottom: "1px solid #2d2060",
display: "flex",
justifyContent: "space-between"
}}>
<MarqueeDots color={tweak.accentRed} count={isMobile ? 5 : 8} />
<MarqueeDots color={tweak.accentTeal} count={isMobile ? 5 : 8} />
</div>
)}
<Sparkles count={isMobile ? 10 : 20} />
<div style={{
position: "relative",
zIndex: 1,
display: "flex",
flexDirection: isMobile ? "column" : "row",
alignItems: isMobile ? "center" : "center",
gap: isMobile ? 16 : 32,
padding: isMobile ? "20px 16px 0" : "28px 36px 0",
textAlign: isMobile ? "center" : "left"
}}>
<img src="assets/images/sages-logo.png" alt="Sages Entertainment" style={{
width: isMobile ? 80 : 140,
height: "auto",
flexShrink: 0,
filter: "drop-shadow(0 4px 28px rgba(232,41,74,0.5)) drop-shadow(0 0 16px rgba(123,63,168,0.4))",
animation: "floatBanner 5s ease-in-out infinite"
}} />
<div style={{ flex: 1 }}>
<div style={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
alignItems: "center",
gap: 8,
marginBottom: 12,
justifyContent: isMobile ? "center" : "flex-start"
}}>
{!isMobile && <div style={{ height: 2, width: 24, background: tweak.accentRed, borderRadius: 1 }} />}
<div style={{ fontFamily: "Nunito, sans-serif", fontSize: 10, fontWeight: 800, color: tweak.accentRed, textTransform: "uppercase", letterSpacing: "0.2em" }}>Mission Statement</div>
{!isMobile && <div style={{ height: 2, width: 24, background: tweak.accentRed, borderRadius: 1 }} />}
</div>
<h1 style={{
fontFamily: "'Fredoka', sans-serif",
fontSize: isMobile ? 18 : 28,
fontWeight: 700,
color: "#ffffff",
lineHeight: 1.35,
marginBottom: 14,
textShadow: "0 2px 12px rgba(0,0,0,0.4)",
textWrap: "pretty"
}}>
Build a thriving business that lets us work smarter, reduce daily stress, and be present with our family.
</h1>
<p style={{
fontFamily: "Nunito, sans-serif",
fontSize: isMobile ? 12 : 14,
color: "#b8b0d8",
lineHeight: 1.7,
fontStyle: "italic"
}}>
Then go out and fill the world with joy, wonder, and laughter for those who need it most. ✦
</p>
</div>
</div>
{/* Bottom marquee dots */}
{!isMobile && (
<div style={{
padding: isMobile ? "8px 12px 0" : "10px 24px 0",
marginTop: 20,
borderTop: "1px solid #2d2060",
display: "flex",
justifyContent: "space-between"
}}>
<MarqueeDots color={tweak.accentOrange} count={isMobile ? 5 : 8} />
<MarqueeDots color={tweak.accentPurple} count={isMobile ? 5 : 8} />
</div>
)}
<style>{`@keyframes floatBanner { 0%,100%{transform:translateY(0) rotate(-1deg)} 50%{transform:translateY(-7px) rotate(1deg)} }`}</style>
</div>);
}
// ─── STAT CARD ───────────────────────────────────────────────────────────────
function StatCard({ value, label, trend, trendColor, color, onClick, isMobile }) {
const [hov, setHov] = React.useState(false);
return (
<div onClick={onClick} onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)} style={{
flex: 1,
minWidth: isMobile ? "100%" : 160,
width: isMobile ? "100%" : "auto",
background: hov ? "#302d50" : "#252240",
borderLeft: `1px solid rgba(255,255,255,0.1)`,
borderRight: `1px solid rgba(255,255,255,0.1)`,
borderBottom: `1px solid rgba(255,255,255,0.1)`,
borderTop: `3px solid ${color}`,
borderRadius: 14,
padding: isMobile ? "16px 18px" : "20px 22px",
cursor: onClick ? "pointer" : "default",
transition: "all 0.2s",
transform: hov ? "translateY(-2px)" : "none"
}}>
<div style={{ fontFamily: "'Fredoka', sans-serif", fontSize: isMobile ? 36 : 44, fontWeight: 700, color, lineHeight: 1.1 }}>{value}</div>
<div style={{ fontFamily: "Nunito, sans-serif", fontSize: 14, color: "#d0c8e8", marginTop: 4, fontWeight: 600 }}>{label}</div>
{trend && <div style={{ fontFamily: "Nunito, sans-serif", fontSize: 13, color: trendColor || color, marginTop: 8, fontWeight: 700 }}>{trend}</div>}
</div>);
}
// ─── RACCOON WISDOM CARD ─────────────────────────────────────────────────────
function RaccoonWisdomCard({ tweak, isMobile, openLoopsCount = 0, threeThings = [] }) {
// Build context for wisdom generation
const timeContext = getTimeContext();
const dayContext = getDayContext();
const openLoopsContext = getOpenLoopsContext(openLoopsCount);
const threeThingsContext = getThreeThingsContext(threeThings);
// Generate context-aware wisdoms and pick random one
const contextWisdoms = React.useMemo(() => {
return generateContextualWisdom({
timeOfDay: timeContext.timeOfDay,
isWeekend: dayContext.isWeekend,
openLoops: { ...openLoopsContext, count: openLoopsCount },
threeThings: threeThingsContext,
hasUpcomingEvent: false, // Could be enhanced with real calendar data
eventType: null
});
}, [timeContext.timeOfDay, dayContext.isWeekend, openLoopsCount, threeThings]);
// Pick a random wisdom on initial load
const [wisdomIdx, setWisdomIdx] = React.useState(() => Math.floor(Math.random() * contextWisdoms.length));
const [animKey, setAnimKey] = React.useState(0);
// Get current wisdom (from context-aware list or fallback)
const w = contextWisdoms[wisdomIdx] || STATIC_WISDOMS[0];
const next = () => {
setWisdomIdx((i) => (i + 1) % contextWisdoms.length);
setAnimKey((k) => k + 1);
};
const moodColors = {
curious: "#7B3FA8", wise: "#26BFD9", excited: "#E8294A",
cozy: "#7B3FA8", alert: "#F7921E"
};
const mc = moodColors[w.mood] || tweak.accentPurple;
return (
<div style={{
flex: 1,
minWidth: isMobile ? "100%" : 240,
width: isMobile ? "100%" : "auto",
background: `linear-gradient(135deg, ${mc}22 0%, #1a1030 60%, #12091e 100%)`,
border: `1px solid ${mc}44`,
borderTop: `3px solid ${mc}`,
borderRadius: 14,
padding: isMobile ? "14px 16px" : "18px 20px",
position: "relative", overflow: "hidden"
}}>
{/* Mood label */}
<div style={{ marginBottom: 14 }}>
<div style={{ fontFamily: "'Fredoka', sans-serif", fontSize: isMobile ? 18 : 20, fontWeight: 700, color: mc }}>Raccoon Wisdom</div>
</div>
{/* Character - Raccoon + Mood Emoji */}
<div style={{ display: "flex", gap: 14, alignItems: "flex-start" }}>
<div key={animKey} style={{
fontSize: isMobile ? 36 : 42,
lineHeight: 1,
flexShrink: 0,
animation: "raccoonPop 0.4s cubic-bezier(0.175,0.885,0.32,1.275) both",
filter: `drop-shadow(0 2px 8px ${mc}66)`
}}>
🦝 {w.emoji}
</div>
{/* Speech bubble */}
<div style={{
position: "relative",
background: "#1e1535",
border: `1px solid ${mc}44`,
borderRadius: "12px 12px 12px 4px",
padding: isMobile ? "8px 10px" : "10px 12px",
flex: 1
}}>
<div style={{
position: "absolute", left: -6, top: 12,
width: 0, height: 0,
borderTop: "5px solid transparent",
borderBottom: "5px solid transparent",
borderRight: `6px solid ${mc}44`
}} />
<p style={{ fontFamily: "Nunito, sans-serif", fontSize: isMobile ? 12 : 13, color: "#f0e8ff", lineHeight: 1.6, fontStyle: "italic", margin: 0 }}>{w.text}</p>
</div>
</div>
<button onClick={next} style={{
marginTop: 14, width: "100%",
background: mc + "20", border: `1px solid ${mc}44`,
borderRadius: 8, color: mc, fontSize: 12, padding: "6px 12px",
cursor: "pointer", fontFamily: "'Fredoka', sans-serif", fontWeight: 600,
transition: "all 0.15s", letterSpacing: "0.03em"
}}
onMouseEnter={(e) => {e.currentTarget.style.background = mc + "40";}}
onMouseLeave={(e) => {e.currentTarget.style.background = mc + "20";}}>
✦ another shiny</button>
<style>{`
@keyframes raccoonPop {
from { transform: scale(0.5) rotate(-20deg); opacity: 0; }
to { transform: scale(1) rotate(0deg); opacity: 1; }
}
`}</style>
</div>);
}
// ─── SECTION HEADER ──────────────────────────────────────────────────────────
function SectionHeader({ title, color, action, onAction }) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16 }}>
<h2 style={{
fontFamily: "'Fredoka', sans-serif", fontSize: 22, fontWeight: 600,
color: "#ffffff", display: "flex", alignItems: "center", gap: 10
}}>
<span style={{ display: "inline-block", width: 4, height: 20, background: color, borderRadius: 2 }} />
{title}
</h2>
{action &&
<button onClick={onAction} style={{
background: "none", border: "none", color, fontSize: 13, fontFamily: "Nunito, sans-serif",
fontWeight: 700, cursor: "pointer", textDecoration: "none", padding: "4px 8px",
borderRadius: 6, transition: "background 0.15s"
}}
onMouseEnter={(e) => e.currentTarget.style.background = color + "22"}
onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
{action} →</button>
}
</div>);
}
// ─── CARD (base) ─────────────────────────────────────────────────────────────
function Card({ children, style = {}, color, topBorder }) {
const [hov, setHov] = React.useState(false);
const borderColor = hov && color ? color + "55" : "rgba(255,255,255,0.08)";
return (
<div onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)} style={{
background: hov ? "#302d50" : "#252240",
borderLeft: `1px solid rgba(255,255,255,0.1)`,
borderRight: `1px solid rgba(255,255,255,0.1)`,
borderBottom: `1px solid rgba(255,255,255,0.1)`,
borderTop: topBorder ? `3px solid ${topBorder}` : `1px solid rgba(255,255,255,0.1)`,
borderRadius: 12,
padding: "16px 18px",
transition: "all 0.18s",
...style
}}>{children}</div>);
}
// ─── SCHEDULE SECTION ────────────────────────────────────────────────────────
function ScheduleSection({ tweak, onNav, isMobile, calendarData }) {
const events = calendarData?.events || [];
const dateLabel = calendarData?.date || new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
return (
<div style={{ marginBottom: 32 }}>
<SectionHeader title="Today's Schedule" color={tweak.accentTeal} action="View Calendar" onAction={() => onNav("calendar")} />
<Card>
<div style={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
alignItems: isMobile ? "flex-start" : "center",
justifyContent: "space-between",
marginBottom: 14,
paddingBottom: 12,
borderBottom: "1px solid rgba(255,255,255,0.1)",
gap: isMobile ? "10px" : "0"
}}>
<div style={{ fontFamily: "'Fredoka', sans-serif", fontSize: isMobile ? 16 : 18, color: "#ffffff" }}>{dateLabel}</div>
</div>
{events.length === 0 ? (
<div style={{ color: "#9d94c0", padding: 16, textAlign: "center" }}>
No events scheduled today! Free day for the raccoon. 🦝
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
{events.map((ev, i) =>
<div key={i} style={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
alignItems: isMobile ? "flex-start" : "center",
gap: isMobile ? 6 : 14,
padding: "10px 10px", borderRadius: 8,
background: ev.done ? tweak.accentTeal + "0a" : "transparent",
opacity: ev.done ? 0.55 : 1
}}>
<span style={{ fontSize: 13, fontFamily: "Nunito, sans-serif", fontWeight: 800, color: tweak.accentTeal, minWidth: isMobile ? "auto" : 72, flexShrink: 0 }}>{ev.time}</span>
<span style={{ flex: 1, fontFamily: "Nunito, sans-serif", fontSize: isMobile ? 14 : 15, color: ev.done ? "#b8b0d8" : "#f0ecff", textDecoration: ev.done ? "line-through" : "none" }}>{ev.icon || '📅'} {ev.title}</span>
<span style={{
fontFamily: "Nunito, sans-serif", fontSize: 12, fontWeight: 700,
color: tweak.accentTeal, background: tweak.accentTeal + "18",
padding: "3px 9px", borderRadius: 6
}}>{ev.calendar || 'Personal'}</span>
</div>
)}
</div>
)}
</Card>
</div>);
}
// ─── THREE THINGS ────────────────────────────────────────────────────────────
function ThreeThingsSection({ tweak, isMobile, threeThings }) {
const things = threeThings || [];
if (things.length === 0) {
return (
<div style={{ marginBottom: 32 }}>
<SectionHeader title="3 Important Things" color="#22c55e" />
<Card>
<div style={{ color: "#9d94c0", padding: 16, textAlign: "center" }}>
Loading your 3 things... 🦝
</div>
</Card>
</div>
);
}
return (
<div style={{ marginBottom: 32 }}>
<SectionHeader title="3 Important Things" color="#22c55e" />
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{things.map((t, i) =>
<Card key={i} color="#22c55e" style={{ display: "flex", alignItems: "center", gap: 14, opacity: t.status === 'DONE' ? 0.6 : 1 }}>
<div style={{
width: isMobile ? 28 : 32,
height: isMobile ? 28 : 32,
borderRadius: "50%",
flexShrink: 0,
background: t.status === 'DONE' ? "#22c55e" : "#22c55e33",
border: `2px solid #22c55e`,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "'Fredoka', sans-serif",
fontSize: isMobile ? 14 : 16,
fontWeight: 700,
color: t.status === 'DONE' ? "#fff" : "#22c55e"
}}>{t.status === 'DONE' ? "✓" : i + 1}</div>
<div style={{ flex: 1 }}>
<div style={{
fontFamily: "Nunito, sans-serif",
fontSize: isMobile ? 13 : 14,
fontWeight: 700,
color: "#ffffff",
textDecoration: t.status === 'DONE' ? "line-through" : "none"
}}>{t.title}</div>
<div style={{ fontFamily: "Nunito, sans-serif", fontSize: 12, color: "#c0b4e0", marginTop: 2 }}>
{t.priority && <span style={{ color: t.priority === 'High' ? '#fca5a5' : '#86efac' }}>{t.priority} Priority · </span>}
{t.meta || t.category}
</div>
</div>
</Card>
)}
</div>
</div>);
}
// ─── OPEN LOOPS ──────────────────────────────────────────────────────────────
function OpenLoopsSection({ tweak, onNav, isMobile, loops }) {
const loopData = loops || { urgent: [], invoices: [], active: [] };
return (
<div style={{ marginBottom: 32 }}>
<SectionHeader title="Open Loops" color={tweak.accentOrange} action="View All" onAction={() => onNav("open-loops")} />
{/* Summary tiles */}
<div style={{ display: "flex", flexDirection: isMobile ? "column" : "row", gap: 12, marginBottom: 16, flexWrap: "wrap" }}>
<div style={{ flex: 1, minWidth: isMobile ? "100%" : 160, background: "#E8294A18", border: "1px solid #E8294A44", borderTop: "3px solid #E8294A", borderRadius: 12, padding: "14px 16px" }}>
<div style={{ fontSize: 10, fontWeight: 700, color: "#fca5a5", textTransform: "uppercase", letterSpacing: "0.1em" }}>🚨 Urgent Deadline</div>
<div style={{ fontFamily: "'Fredoka', sans-serif", fontSize: 32, fontWeight: 700, color: "#fca5a5", marginTop: 4 }}>
{loopData.urgent.length}
</div>
{loopData.urgent.length > 0 ? (
<div style={{ marginTop: 8, borderTop: "1px solid #E8294A33", paddingTop: 8 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: "#fca5a5" }}>{loopData.urgent[0].title}</div>
<div style={{ fontSize: 11, color: "#fca5a5", opacity: 0.7, marginTop: 2 }}>{loopData.urgent[0].due}</div>
</div>
) : (
<div style={{ marginTop: 8, borderTop: "1px solid #E8294A33", paddingTop: 8 }}>
<div style={{ fontSize: 12, fontStyle: "italic", color: "#fca5a5", opacity: 0.9 }}>🦝 No fires to put out! The den is safe. Time to focus on growth. ✨</div>
</div>
)}
</div>
<div style={{ flex: 1, minWidth: isMobile ? "100%" : 130, background: "#22c55e18", border: "1px solid #22c55e44", borderTop: "3px solid #22c55e", borderRadius: 12, padding: "14px 16px" }}>
<div style={{ fontSize: 10, fontWeight: 700, color: "#86efac", textTransform: "uppercase", letterSpacing: "0.1em" }}>Invoices</div>
<div style={{ fontFamily: "'Fredoka', sans-serif", fontSize: 32, fontWeight: 700, color: "#86efac", marginTop: 4 }}>{loopData.invoices.length}</div>
</div>
<div style={{ flex: 1, minWidth: isMobile ? "100%" : 130, background: "#7B3FA818", border: "1px solid #7B3FA844", borderTop: `3px solid ${tweak.accentPurple}`, borderRadius: 12, padding: "14px 16px" }}>
<div style={{ fontSize: 10, fontWeight: 700, color: "#c4b5fd", textTransform: "uppercase", letterSpacing: "0.1em" }}>Active Loops</div>
<div style={{ fontFamily: "'Fredoka', sans-serif", fontSize: 32, fontWeight: 700, color: "#c4b5fd", marginTop: 4 }}>{loopData.active.length}</div>
</div>
</div>
{/* Two columns */}
<div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "1fr 1fr", gap: 12 }}>
<div>
<div style={{ fontSize: 10, fontWeight: 700, color: "#86efac", textTransform: "uppercase", letterSpacing: "0.1em", marginBottom: 8, paddingLeft: 4, display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ display: "inline-block", width: 8, height: 8, borderRadius: "50%", background: "#22c55e" }} /> Invoices
</div>
{loopData.invoices.map((item, i) =>
<Card key={i} color="#22c55e" style={{ marginBottom: 6, borderLeft: "3px solid #22c55e55" }}>
<div style={{ fontSize: 13, fontWeight: 700, color: "#f0ecff", fontFamily: "Nunito, sans-serif" }}>💰 {item.title}</div>
<div style={{ fontSize: 12, color: "#c8c0e0", marginTop: 4, fontFamily: "Nunito, sans-serif" }}>{item.sub}</div>
</Card>
)}
</div>
<div>
<div style={{ fontSize: 10, fontWeight: 700, color: "#c4b5fd", textTransform: "uppercase", letterSpacing: "0.1em", marginBottom: 8, paddingLeft: 4, display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ display: "inline-block", width: 8, height: 8, borderRadius: "50%", background: tweak.accentPurple }} /> Active
</div>
{loopData.active.map((item, i) =>
<Card key={i} color={tweak.accentPurple} style={{ marginBottom: 6, borderLeft: `3px solid ${tweak.accentPurple}55` }}>
<div style={{ fontSize: 13, fontWeight: 700, color: "#f0ecff", fontFamily: "Nunito, sans-serif" }}>🟣 {item.title}</div>
<div style={{ fontSize: 12, color: "#c8c0e0", marginTop: 4, fontFamily: "Nunito, sans-serif" }}>{item.sub}</div>
</Card>
)}
</div>
</div>
</div>);
}
// ─── PARKING LOT ─────────────────────────────────────────────────────────────
function ParkingLotSection({ tweak, onNav, isMobile, parkingLot }) {
const items = parkingLot || [];
if (items.length === 0) {
return (
<div style={{ marginBottom: 32 }}>
<SectionHeader title="Parking Lot" color={tweak.accentPurple} action="View All" onAction={() => onNav("parking-lot")} />
<Card>
<div style={{ color: "#9d94c0", padding: 16, textAlign: "center" }}>
No items in the parking lot! 🦝
</div>
</Card>
</div>
);
}
return (
<div style={{ marginBottom: 32 }}>
<SectionHeader title="Parking Lot" color={tweak.accentPurple} action="View All" onAction={() => onNav("parking-lot")} />
<div style={{ display: "grid", gridTemplateColumns: isMobile ? "1fr" : "repeat(auto-fill, minmax(220px, 1fr))", gap: 10 }}>
{items.map((item, i) =>
<Card key={i} color={tweak.accentPurple} style={{ borderLeft: `3px solid ${tweak.accentPurple}55`, textAlign: "left" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "flex-start", gap: 8, marginBottom: 6 }}>
<div style={{ width: 8, height: 8, borderRadius: "50%", background: "#22c55e", flexShrink: 0 }} />
<div style={{ fontSize: 12, fontWeight: 700, color: "#c0b4e0", fontFamily: "Nunito, sans-serif", textAlign: "left" }}>{item.when}</div>
</div>
<div style={{ fontSize: 14, fontWeight: 700, color: "#ffffff", fontFamily: "Nunito, sans-serif", marginBottom: 4, textAlign: "left" }}>{item.title}</div>
<div style={{ fontSize: 12, color: "#c0b4e0", fontFamily: "Nunito, sans-serif", textAlign: "left" }}>{item.sub}</div>
</Card>
)}
</div>
</div>);
}
// ─── TEAM CARD ──────────────────────────────────────────────────────────────
function TeamCard({ member, isMobile }) {
const [hov, setHov] = React.useState(false);
// Map member names to photo paths (static assets)
const photoMap = {
'Wayne': '../assets/images/wayne.png',
'Kali': '../assets/images/kali.png',
'Shine': '../assets/images/shine.png',
'Joey': '../assets/images/joey.png',
'Colby': '../assets/images/colby.png',
'Caleigh': '../assets/images/caleigh.png',
'Alice': '../assets/images/alice.png',
'Snickers': '../assets/images/snickers.png'
};
const photo = photoMap[member.name] || null;
const hasPhoto = photo !== null;
return (
<div onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)} style={{
background: hov ? "#302d50" : "#252240",
borderLeft: "1px solid rgba(255,255,255,0.1)",
borderRight: "1px solid rgba(255,255,255,0.1)",
borderBottom: "1px solid rgba(255,255,255,0.1)",
borderTop: `3px solid ${member.color}`,
borderRadius: 12,
padding: isMobile ? "12px 14px" : "16px 18px",
transition: "all 0.18s",
opacity: member.status === 'limited' ? 0.8 : 1
}}>
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 12 }}>
<div style={{
width: isMobile ? 40 : 48,
height: isMobile ? 40 : 48,
borderRadius: "50%",
flexShrink: 0,
border: `2px solid ${member.color}99`,
boxShadow: `0 0 14px ${member.color}55`,
overflow: "hidden",
background: member.color + "22",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
}}>
{hasPhoto ? (
<img src={photo} alt={member.name}
style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
onError={e => { e.currentTarget.style.display = "none"; }}
/>
) : null}
<div style={{
display: hasPhoto ? "none" : "flex",
position: hasPhoto ? "absolute" : "static",
inset: 0,
alignItems: "center",
justifyContent: "center",
fontSize: isMobile ? 18 : 22
}}>{member.emoji}</div>
</div>
<div>
<div style={{ fontFamily: "'Fredoka', sans-serif", fontSize: isMobile ? 17 : 19, fontWeight: 700, color: "#ffffff" }}>{member.emoji} {member.name}</div>
<div style={{ fontFamily: "Nunito, sans-serif", fontSize: 12, color: member.color, fontWeight: 700, letterSpacing: "0.02em" }}>{member.role}</div>
</div>
</div>
<ul style={{ paddingLeft: 0, listStyle: "none", marginBottom: member.status === "limited" ? 10 : 0 }}>
<div style={{ fontSize: 10, fontWeight: 700, color: "#9d94c0", textTransform: "uppercase", letterSpacing: "0.1em", marginBottom: 8 }}>Currently Working On</div>
{member.items && member.items.map((item, j) => (
<li key={j} style={{ fontFamily: "Nunito, sans-serif", fontSize: isMobile ? 12 : 13, color: "#d0c8e8", marginBottom: 5, paddingLeft: 16, position: "relative" }}>
<span style={{ position: "absolute", left: 0, color: member.color }}>•</span>
{item}
</li>
))}
</ul>
{member.status === "limited" && (
<div style={{ background: "#F7921E22", border: "1px solid #F7921E55", borderRadius: 6, padding: "4px 10px", fontSize: 11, color: "#F7921E", fontFamily: "Nunito, sans-serif", fontWeight: 700, display: "inline-block" }}>Limited availability</div>
)}
</div>
);
}
// ─── TEAM SECTION ────────────────────────────────────────────────────────────
function TeamSection({ tweak, onNav, isMobile, teamData }) {
const members = teamData || [];
// Static color mapping for team members
const colorMap = {
'Wayne': '#E8294A',
'Kali': '#7B3FA8',
'Shine': '#26BFD9',
'Joey': '#F7921E',
'Colby': '#26BFD9',
'Caleigh': '#F7921E'
};
// Enhance member data with colors
const enhancedMembers = members.map(m => ({
...m,
color: colorMap[m.name] || tweak.accentPurple
}));
if (members.length === 0) {
return (
<div style={{ marginBottom: 32 }}>
<SectionHeader title="Who's Working on What" color={tweak.accentTeal} action="View All" onAction={() => onNav("team")} />
<Card>
<div style={{ color: "#9d94c0", padding: 16, textAlign: "center" }}>
Loading team data... 🦝
</div>
</Card>
</div>
);
}
return (
<div style={{ marginBottom: 32 }}>
<SectionHeader title="Who's Working on What" color={tweak.accentTeal} action="View All" onAction={() => onNav("team")} />
<div className="team-grid" style={{
display: "grid",
gridTemplateColumns: isMobile ? "1fr" : "repeat(auto-fill, minmax(200px, 1fr))",
gap: 10
}}>
{enhancedMembers.map((member, i) => (
<TeamCard key={i} member={member} isMobile={isMobile} />
))}
</div>
{/* Fun family footer */}
<div style={{ textAlign: "center", marginTop: 28, paddingTop: 28, borderTop: "1px solid rgba(255,255,255,0.08)" }}>
{!isMobile && (
<div style={{ display: "flex", justifyContent: "center", gap: 6, marginBottom: 10, opacity: 0.6 }}>
{["#E8294A", "#26BFD9", "#F7921E", "#7B3FA8", "#F9C01C"].map((c, i) =>
<div key={i} style={{ width: 6, height: 6, borderRadius: "50%", background: c, boxShadow: `0 0 4px ${c}` }} />
)}
</div>
)}
<div style={{ fontFamily: "'Fredoka', sans-serif", fontSize: 16, color: "#9080c0", marginBottom: 4 }}>
Wayne · Kali · Alice · Snickers the Seal 🦭
</div>
<div style={{ fontFamily: "Nunito, sans-serif", fontSize: 13, color: "#b8b0d8", fontStyle: "italic" }}>
Filling the world with joy, wonder &amp; laughter — one show at a time.
</div>
</div>
</div>);
}
// ─── PLACEHOLDER PAGE ────────────────────────────────────────────────────────
function PlaceholderPage({ page, tweak, onNav }) {
const pageInfo = NAV_ITEMS.find((n) => n.id === page) || { label: page, color: tweak.accentPurple, icon: "📄" };
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "60vh", textAlign: "center", padding: 40 }}>
<div style={{ fontSize: 56, marginBottom: 20 }}>{pageInfo.icon}</div>
<h1 style={{ fontFamily: "'Fredoka', sans-serif", fontSize: 36, color: pageInfo.color, marginBottom: 12 }}>{pageInfo.label}</h1>
<p style={{ fontFamily: "Nunito, sans-serif", color: "#c0b4e0", fontSize: 15, marginBottom: 24 }}>This page is part of the real app — this prototype shows the Overview redesign.</p>
<button onClick={() => onNav("overview")} style={{
background: pageInfo.color + "22", border: `1px solid ${pageInfo.color}55`,
borderRadius: 10, color: pageInfo.color, padding: "10px 20px",
fontFamily: "'Fredoka', sans-serif", fontSize: 17, cursor: "pointer"
}}>← Back to Overview</button>
</div>);
}
// ─── OVERVIEW PAGE ───────────────────────────────────────────────────────────
function OverviewPage({ tweak, onNav, isMobile }) {
// State for live data
const [loops, setLoops] = React.useState({ urgent: [], invoices: [], active: [] });
const [threeThings, setThreeThings] = React.useState([]);
const [calendarData, setCalendarData] = React.useState({ date: "", events: [] });
const [parkingLot, setParkingLot] = React.useState([]);
const [teamData, setTeamData] = React.useState([]);
const [loading, setLoading] = React.useState(true);
// Fetch live data on mount
React.useEffect(() => {
const fetchData = async () => {
// Fetch all data independently so one failure doesn't break others
const fetchPromises = [
// Fetch open loops
fetch('../tracking/open-loops.md')
.then(r => r.text())
.then(md => setLoops(parseOpenLoopsMarkdown(md)))
.catch(err => console.error('Failed to load open loops:', err)),
// Fetch 3 things
fetch('../tracking/3-things.md')
.then(r => r.text())
.then(md => setThreeThings(parseThreeThingsMarkdown(md)))
.catch(err => console.error('Failed to load 3 things:', err)),
// Fetch calendar data
fetch('../tracking/calendar-data.json')
.then(r => r.json())
.then(json => setCalendarData(json))
.catch(err => console.error('Failed to load calendar:', err)),
// Fetch parking lot
fetch('../tracking/parking-lot.md')
.then(r => r.text())
.then(md => setParkingLot(parseParkingLotMarkdown(md)))
.catch(err => console.error('Failed to load parking lot:', err)),
// Fetch team data
fetch('../tracking/team-current-working-on.md')
.then(r => r.text())
.then(md => setTeamData(parseTeamMarkdown(md)))
.catch(err => console.error('Failed to load team:', err))
];
await Promise.all(fetchPromises);
setLoading(false);
};
fetchData();
}, []);
// Calculate totals
const totalLoops = loops.urgent.length + loops.invoices.length + loops.active.length;
// Get current date for display
const today = new Date();
const dayName = today.toLocaleDateString('en-US', { weekday: 'long' });
const monthName = today.toLocaleDateString('en-US', { month: 'long' });
const dayNum = today.getDate();
const dateDisplay = `${dayName}, ${monthName} ${dayNum}${dayNum === 1 ? 'st' : dayNum === 2 ? 'nd' : dayNum === 3 ? 'rd' : 'th'}`;
// Calculate hours free based on time of day and calendar
const getHoursFree = () => {
const now = new Date();
const hour = now.getHours();
// 7 PM - 7 AM: Good Night
if (hour >= 19 || hour < 7) {
return { value: "🌙", label: "Good Night", trend: "Rest & recharge" };
}
// 4 PM - 7 PM: Wind-down time (unless there's an event)
if (hour >= 16 && hour < 19) {
const hasEventNow = calendarData.events && calendarData.events.some(e => {
const eventHour = parseInt(e.time && e.time.split(':')[0]);
return eventHour === hour || eventHour === hour + 1;
});
if (hasEventNow) {
const event = calendarData.events.find(e => {
const eventHour = parseInt(e.time && e.time.split(':')[0]);
return eventHour === hour || eventHour === hour + 1;
});
return { value: "▶", label: "Event", trend: event && event.title || "In progress" };
}
return { value: "🌅", label: "Wind-down", trend: "Wrap up the day" };
}
// 9 AM - 4 PM: Calculate free hours
// Working hours: 9 AM - 4 PM = 7 hours
const workHours = 7;
const blockedHours = calendarData.events && calendarData.events.reduce((total, event) => {
if (event && event.duration) return total + event && event.duration;
return total + 1; // Default 1 hour per event
}, 0) || 0;
const freeHours = Math.max(0, workHours - blockedHours);
return { value: freeHours.toString(), label: "Hours Free", trend: freeHours > 3 ? "Plenty of time" : "Schedule tight" };
};
return (
<div>
<MissionBanner tweak={tweak} isMobile={isMobile} />
{/* Page header */}
<div style={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
alignItems: isMobile ? "flex-start" : "center",
justifyContent: "space-between",
marginBottom: 24,
gap: isMobile ? "12px" : "0"
}}>
<div>
<h1 style={{
fontFamily: "'Fredoka', sans-serif",
fontSize: isMobile ? 24 : 30,
fontWeight: 700,
color: "#ffffff",
lineHeight: 1.1
}}>
Good {new Date().getHours() < 12 ? "morning" : new Date().getHours() < 17 ? "afternoon" : "evening"}, Wayne! 🎩
</h1>
<p style={{ fontFamily: "Nunito, sans-serif", fontSize: 13, color: "#c0b4e0", marginTop: 4 }}>
{dateDisplay} — Step right up, it's showtime. ✦
</p>
</div>
</div>
{/* Last updated bar */}
<div style={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
justifyContent: isMobile ? "flex-start" : "flex-end",
alignItems: isMobile ? "flex-start" : "center",
gap: 10,
marginBottom: 20
}}>
<span style={{ fontFamily: "Nunito, sans-serif", fontSize: 12, color: "#9080c0" }}>Last updated: {new Date().toLocaleTimeString()}</span>
<button onClick={() => window.location.reload()} style={{
background: "#1a1530", border: "1px solid rgba(255,255,255,0.12)", borderRadius: 7,
color: "#c0b4e0", padding: "5px 12px", fontSize: 12, cursor: "pointer",
fontFamily: "Nunito, sans-serif", fontWeight: 600, display: "flex", alignItems: "center", gap: 6
}}>🔄 Refresh</button>
</div>
{/* Stats row */}
<div className="stats-row" style={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
gap: 12,
marginBottom: 32,
flexWrap: "wrap"
}}>
// Calculate hours free based on time of day and calendar
const getHoursFree = () => {
const now = new Date();
const hour = now.getHours();
// 7 PM - 7 AM: Good Night
if (hour >= 19 || hour < 7) {
return { value: "🌙", label: "Good Night", trend: "Rest & recharge" };
}
// 4 PM - 7 PM: Wind-down time (unless there's an event)
if (hour >= 16 && hour < 19) {
const hasEventNow = calendarData.events && calendarData.events.some(e => {
const eventHour = parseInt(e.time && e.time.split(':')[0]);
return eventHour === hour || eventHour === hour + 1;
});
if (hasEventNow) {
const event = calendarData.events.find(e => {
const eventHour = parseInt(e.time && e.time.split(':')[0]);
return eventHour === hour || eventHour === hour + 1;
});
return { value: "▶", label: "Event", trend: event && event.title || "In progress" };
}
return { value: "🌅", label: "Wind-down", trend: "Wrap up the day" };
}
// 9 AM - 4 PM: Calculate free hours
// Working hours: 9 AM - 4 PM = 7 hours
const workHours = 7;
const blockedHours = calendarData.events && calendarData.events.reduce((total, event) => {
if (event && event.duration) return total + event && event.duration;
return total + 1; // Default 1 hour per event
}, 0) || 0;
const freeHours = Math.max(0, workHours - blockedHours);
return { value: freeHours.toString(), label: "Hours Free", trend: freeHours > 3 ? "Plenty of time" : "Schedule tight" };
};
const hoursFree = getHoursFree();
<StatCard value={hoursFree.value} label={hoursFree.label} trend={hoursFree.trend} trendColor="#86efac" color={tweak.accentTeal} onClick={() => onNav("calendar")} isMobile={isMobile} />
<StatCard value={totalLoops.toString()} label="Open Loops" trend={totalLoops > 10 ? "Needs attention" : "Manageable"} trendColor={totalLoops > 10 ? "#fbbf24" : "#86efac"} color={tweak.accentOrange} onClick={() => onNav("open-loops")} isMobile={isMobile} />
<RaccoonWisdomCard tweak={tweak} isMobile={isMobile} openLoopsCount={totalLoops} threeThings={threeThings} />
</div>
<ThreeThingsSection tweak={tweak} isMobile={isMobile} threeThings={threeThings} />
<ScheduleSection tweak={tweak} onNav={onNav} isMobile={isMobile} calendarData={calendarData} />
<OpenLoopsSection tweak={tweak} onNav={onNav} isMobile={isMobile} loops={loops} />
<ParkingLotSection tweak={tweak} onNav={onNav} isMobile={isMobile} parkingLot={parkingLot} />
<TeamSection tweak={tweak} onNav={onNav} isMobile={isMobile} teamData={teamData} />
</div>);
}
// ─── TWEAKS PANEL ────────────────────────────────────────────────────────────
function TweaksPanel({ tweak, setTweak, onClose }) {
const ColorSwatch = ({ label, k }) => (
<div style={{ marginBottom: 10 }}>
<div style={{ fontFamily: "Nunito, sans-serif", fontSize: 11, color: "#c0b4e0", marginBottom: 4 }}>{label}</div>
<input type="color" value={tweak[k]} onChange={(e) => setTweak(k, e.target.value)}
style={{ width: 40, height: 28, border: "none", borderRadius: 6, cursor: "pointer", background: "none" }} />
</div>
);
return (
<div style={{
position: "fixed", bottom: 24, right: 24, width: 220,
background: "#0a0815", border: "1px solid rgba(255,255,255,0.12)", borderRadius: 16,
padding: 20, zIndex: 200, boxShadow: "0 8px 40px rgba(0,0,0,0.6)"
}}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<div style={{ fontFamily: "'Fredoka', sans-serif", fontSize: 16, color: "#ffffff" }}>Tweaks</div>
<button onClick={onClose} style={{ background: "none", border: "none", color: "#c0b4e0", cursor: "pointer", fontSize: 16 }}>✕</button>
</div>
<ColorSwatch label="Brand Red" k="accentRed" />
<ColorSwatch label="Brand Purple" k="accentPurple" />
<ColorSwatch label="Brand Teal" k="accentTeal" />
<ColorSwatch label="Brand Orange" k="accentOrange" />
<div style={{ marginTop: 14, paddingTop: 14, borderTop: "1px solid rgba(255,255,255,0.08)" }}>
<div style={{ fontFamily: "Nunito, sans-serif", fontSize: 11, color: "#c0b4e0", marginBottom: 8 }}>Dark Mode Shade</div>
{[
{ label: "Deep Purple Night", bg: "#0f0c1e", sidebar: "#0a0815", card: "#1a1530" },
{ label: "Midnight Navy", bg: "#080c1a", sidebar: "#050810", card: "#111828" },
{ label: "Dark Charcoal", bg: "#111111", sidebar: "#0a0a0a", card: "#1a1a1a" }].
map((opt) =>
<button key={opt.label} onClick={() => setTweak({ bgBody: opt.bg, bgSidebar: opt.sidebar, bgCard: opt.card })} style={{
display: "block", width: "100%", textAlign: "left",
background: opt.bg === tweak.bgBody ? "#1e1740" : "none",
border: "none", borderRadius: 7, color: "#c0b4e0", fontSize: 12,
fontFamily: "Nunito, sans-serif", padding: "6px 10px", cursor: "pointer", marginBottom: 2
}}>{opt.label}</button>
)}
</div>
</div>);
}
// ─── APP ROOT ────────────────────────────────────────────────────────────────
function App() {
const [activePage, setActivePage] = React.useState("overview");
const [showTweaks, setShowTweaks] = React.useState(false);
const [tweak, setTweakState] = React.useState(TWEAK_DEFAULTS);
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
const [isMobile, setIsMobile] = React.useState(false);
const [openLoopsCount, setOpenLoopsCount] = React.useState(0);
// Detect mobile on mount and resize
React.useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth <= 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const setTweak = (keyOrObj, val) => {
if (typeof keyOrObj === "object") {
setTweakState((prev) => ({ ...prev, ...keyOrObj }));
window.parent.postMessage({ type: "__edit_mode_set_keys", edits: keyOrObj }, "*");
} else {
setTweakState((prev) => ({ ...prev, [keyOrObj]: val }));
window.parent.postMessage({ type: "__edit_mode_set_keys", edits: { [keyOrObj]: val } }, "*");
}
};
React.useEffect(() => {
const handler = (e) => {
if (e.data?.type === "__activate_edit_mode") setShowTweaks(true);
if (e.data?.type === "__deactivate_edit_mode") setShowTweaks(false);
};
window.addEventListener("message", handler);
window.parent.postMessage({ type: "__edit_mode_available" }, "*");
return () => window.removeEventListener("message", handler);
}, []);
// Apply CSS vars for theming
React.useEffect(() => {
document.body.style.background = tweak.bgBody;
const root = document.documentElement;
root.style.setProperty('--bg-card', tweak.bgCard);
root.style.setProperty('--bg-card-hover', tweak.bgCard + 'cc');
root.style.setProperty('--bg-sidebar', tweak.bgSidebar);
}, [tweak.bgBody, tweak.bgCard, tweak.bgSidebar]);
// Handle navigation with mobile menu close
const handleNav = (page) => {
setActivePage(page);
setMobileMenuOpen(false);
};
// Fetch open loops count for sidebar badge
React.useEffect(() => {
fetch('../tracking/open-loops.md')
.then(r => r.text())
.then(md => {
const loops = parseOpenLoopsMarkdown(md);
const total = loops.urgent.length + loops.invoices.length + loops.active.length;
setOpenLoopsCount(total);
})
.catch(err => console.error('Failed to load open loops for badge:', err));
}, []);
// Mobile styles
const appStyles = isMobile
? { display: "flex", flexDirection: "column", minHeight: "100vh" }
: { display: "flex", minHeight: "100vh" };
const mainStyles = isMobile
? { marginLeft: 0, flex: 1, padding: "15px", maxWidth: "100%", minHeight: "100vh", boxSizing: "border-box" }
: { marginLeft: 240, flex: 1, padding: "28px 36px", maxWidth: 1100, minHeight: "100vh", boxSizing: "border-box" };
return (
<div style={appStyles}>
{/* Mobile menu button */}
{isMobile && (
<button
className="mobile-menu-btn"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
style={{
display: "flex",
position: "fixed",
top: 10,
right: 10,
zIndex: 99999,
background: "#1e1640",
border: "2px solid #7B3FA8",
color: "#ede9f7",
padding: 12,
borderRadius: 8,
fontSize: 24,
width: 48,
height: 48,
alignItems: "center",
justifyContent: "center",
boxShadow: "0 4px 12px rgba(0,0,0,0.5)"
}}
>
{mobileMenuOpen ? '✕' : '☰'}
</button>
)}
<Sidebar
activePage={activePage}
onNav={handleNav}
tweak={tweak}
mobileCollapsed={isMobile && !mobileMenuOpen}
isMobile={isMobile}
openLoopsCount={openLoopsCount}
/>
<main className="main-content" style={mainStyles}>
{activePage === "overview" ?
<OverviewPage tweak={tweak} onNav={handleNav} isMobile={isMobile} /> :
<PlaceholderPage page={activePage} tweak={tweak} onNav={handleNav} />
}
</main>
{showTweaks &&
<TweaksPanel tweak={tweak} setTweak={setTweak} onClose={() => {
setShowTweaks(false);
window.parent.postMessage({ type: "__edit_mode_dismissed" }, "*");
}} />
}
</div>);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
// Register Service Worker for PWA
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('../js/sw.js')
.then((registration) => {
console.log('[PWA] Service Worker registered:', registration.scope);
})
.catch((error) => {
console.log('[PWA] Service Worker registration failed:', error);
});
});
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment