Created
May 2, 2026 02:42
-
-
Save SagesOfRPG/afa991f250e3b0698da7eac6359484cb to your computer and use it in GitHub Desktop.
Mission Control v3 index.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 & 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