diff --git a/app/morning-brief/page.tsx b/app/morning-brief/page.tsx new file mode 100644 index 0000000..66f1a5d --- /dev/null +++ b/app/morning-brief/page.tsx @@ -0,0 +1,21 @@ +import MorningBriefCalendar from "@/components/morning-brief/MorningBriefCalendar"; +import { MorningBriefProvider } from "@/lib/morning-brief/store"; + +export default function MorningBriefPage() { + return ( + +
+
+
+

☀️ Morning Brief

+

+ Daily intelligence at 6am CET. Weather, AI news, tasks, and market. +

+
+ + +
+
+
+ ); +} diff --git a/components/mission-control/MissionControlDashboard.tsx b/components/mission-control/MissionControlDashboard.tsx index dee5858..87f0e70 100644 --- a/components/mission-control/MissionControlDashboard.tsx +++ b/components/mission-control/MissionControlDashboard.tsx @@ -70,6 +70,21 @@ export default function MissionControlDashboard() {
+ {/* Navigation */} +

Total Progress

{progress}%

diff --git a/components/morning-brief/MorningBriefCalendar.tsx b/components/morning-brief/MorningBriefCalendar.tsx new file mode 100644 index 0000000..01572a4 --- /dev/null +++ b/components/morning-brief/MorningBriefCalendar.tsx @@ -0,0 +1,265 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { useMorningBrief } from "@/lib/morning-brief/store"; +import { MorningBrief } from "@/lib/morning-brief/types"; + +const fadeUp = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 }, +}; + +export default function MorningBriefCalendar() { + const { briefs, todayBrief, generateBrief, getBriefByDate } = useMorningBrief(); + const [selectedDate, setSelectedDate] = useState(null); + const [viewingBrief, setViewingBrief] = useState(null); + + // Get current month for calendar + const today = new Date(); + const [currentMonth, setCurrentMonth] = useState(today.getMonth()); + const [currentYear, setCurrentYear] = useState(today.getFullYear()); + + const monthNames = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + + // Generate calendar days + const getDaysInMonth = (month: number, year: number) => { + return new Date(year, month + 1, 0).getDate(); + }; + + const getFirstDayOfMonth = (month: number, year: number) => { + return new Date(year, month, 1).getDay(); + }; + + const daysInMonth = getDaysInMonth(currentMonth, currentYear); + const firstDay = getFirstDayOfMonth(currentMonth, currentYear); + + // Generate brief dates map + const briefDates = new Set(briefs.map((b) => b.date)); + const todayStr = today.toISOString().split("T")[0]; + + const handlePrevMonth = () => { + if (currentMonth === 0) { + setCurrentMonth(11); + setCurrentYear(currentYear - 1); + } else { + setCurrentMonth(currentMonth - 1); + } + }; + + const handleNextMonth = () => { + if (currentMonth === 11) { + setCurrentMonth(0); + setCurrentYear(currentYear + 1); + } else { + setCurrentMonth(currentMonth + 1); + } + }; + + const handleDateClick = (day: number) => { + const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + const brief = getBriefByDate(dateStr); + if (brief) { + setSelectedDate(dateStr); + setViewingBrief(brief); + } + }; + + // Calendar grid + const calendarDays = []; + for (let i = 0; i < firstDay; i++) { + calendarDays.push(
); + } + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + const hasBrief = briefDates.has(dateStr); + const isToday = dateStr === todayStr; + + calendarDays.push( + + ); + } + + return ( +
+ {/* Calendar */} +
+
+ +

+ {monthNames[currentMonth]} {currentYear} +

+ +
+ +
+ {["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((d) => ( +
+ {d} +
+ ))} +
+ +
+ {calendarDays} +
+
+ + {/* Today's Brief Preview */} + {todayBrief && ( +
+
+

Today's Brief

+ {todayBrief.generatedAt} +
+ +
+ {/* Weather */} +
+

Weather

+
+ 🌤️ +
+

{todayBrief.weather?.temperature}

+

{todayBrief.weather?.condition}

+
+
+
+ + {/* Market */} +
+

Market

+
+

{todayBrief.market.usIndex}

+

{todayBrief.market.crypto}

+
+
+ + {/* Tasks */} +
+

My Tasks Today

+
    + {todayBrief.myTasks.pending.slice(0, 3).map((task, i) => ( +
  • • {task}
  • + ))} +
+
+
+
+ )} + + {/* Brief Detail Modal */} + {viewingBrief && ( +
+
setViewingBrief(null)} + /> + +
+
+

Brief Date

+

{viewingBrief.date}

+
+ +
+ +
+ {/* Weather */} +
+

🌤️ Weather

+

{viewingBrief.weather?.temperature} — {viewingBrief.weather?.condition}

+

{viewingBrief.weather?.location}

+
+ + {/* AI News */} +
+

🤖 AI News

+
    + {viewingBrief.aiNews.items.map((item, i) => ( +
  • • {item}
  • + ))} +
+
+ + {/* Tasks */} +
+

✓ My Tasks

+ {viewingBrief.myTasks.pending.length > 0 && ( +
+

Pending

+ {viewingBrief.myTasks.pending.map((t, i) => ( +

• {t}

+ ))} +
+ )} + {viewingBrief.myTasks.inProgress.length > 0 && ( +
+

In Progress

+ {viewingBrief.myTasks.inProgress.map((t, i) => ( +

• {t}

+ ))} +
+ )} +
+ + {/* Market */} +
+

📈 Market

+

{viewingBrief.market.usIndex}

+

{viewingBrief.market.crypto}

+

+ Sentiment: {viewingBrief.market.sentiment.toUpperCase()} +

+
+
+
+
+ )} + + {/* Generate Button */} + +
+ ); +} diff --git a/lib/morning-brief/store.tsx b/lib/morning-brief/store.tsx new file mode 100644 index 0000000..2d6f0dd --- /dev/null +++ b/lib/morning-brief/store.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { createContext, useContext, useState, useEffect, ReactNode } from "react"; +import { MorningBrief, defaultBrief } from "./types"; + +interface MorningBriefStore { + briefs: MorningBrief[]; + todayBrief: MorningBrief | null; + generateBrief: () => Promise; + getBriefByDate: (date: string) => MorningBrief | undefined; +} + +const MorningBriefContext = createContext(null); + +const STORAGE_KEY = "sitemente:morning-briefs"; + +export function MorningBriefProvider({ children }: { children: ReactNode }) { + const [briefs, setBriefs] = useState([]); + const [todayBrief, setTodayBrief] = useState(null); + + // Load from localStorage on mount + useEffect(() => { + if (typeof window === "undefined") return; + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + try { + const parsed = JSON.parse(saved); + if (Array.isArray(parsed)) { + setBriefs(parsed); + // Find today's brief + const today = new Date().toISOString().split("T")[0]; + const todayBrief = parsed.find((b: MorningBrief) => b.date === today); + if (todayBrief) setTodayBrief(todayBrief); + } + } catch { + // Use default + } + } + }, []); + + // Save to localStorage on change + useEffect(() => { + if (typeof window === "undefined") return; + if (briefs.length > 0) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(briefs)); + } + }, [briefs]); + + const generateBrief = async () => { + const today = new Date().toISOString().split("T")[0]; + const now = new Date().toISOString(); + + // Get Mission Control tasks from localStorage + let myTasks = { pending: [], inProgress: [], dueToday: [] }; + try { + const mcTasks = localStorage.getItem("sitemente:mission-control"); + if (mcTasks) { + const tasks = JSON.parse(mcTasks); + myTasks = { + pending: tasks.filter((t: any) => t.status === "todo").map((t: any) => t.title).slice(0, 5), + inProgress: tasks.filter((t: any) => t.status === "in_progress").map((t: any) => t.title).slice(0, 3), + dueToday: tasks.filter((t: any) => t.dueDate === today).map((t: any) => t.title), + }; + } + } catch (e) { + console.error("Failed to load Mission Control tasks", e); + } + + // Mock data - in production, these would be API calls + const newBrief: MorningBrief = { + id: `brief-${today}`, + date: today, + generatedAt: now, + weather: { + location: "Benalmádena, Málaga", + temperature: "18°C", + condition: "Partly Cloudy", + humidity: "65%", + wind: "12 km/h NW", + }, + aiNews: { + items: [ + "OpenAI announces GPT-5 roadmap with enhanced reasoning", + "Google DeepMind achieves breakthrough in protein folding", + "European AI Act enters enforcement phase", + "Nvidia reports record AI chip demand", + ], + }, + things3Tasks: { + today: [ + "Review SiteMente pricing page", + "Call with potential restaurant client", + "Update HolaCompi roadmap", + ], + overdue: [], + }, + myTasks, + market: { + usIndex: "S&P 500: +0.3%", + crypto: "BTC: $67,500 (+1.2%)", + sentiment: "bullish", + topNews: [ + "Fed signals potential rate cut in March", + "Bitcoin ETF inflows hit record high", + "Tech earnings exceed expectations", + ], + events: [ + "US Jobs Report (8:30 AM ET)", + "Crypto Treasury Meeting (2:00 PM ET)", + ], + }, + }; + + setBriefs((prev) => { + const filtered = prev.filter((b) => b.date !== today); + return [...filtered, newBrief]; + }); + setTodayBrief(newBrief); + }; + + const getBriefByDate = (date: string) => { + return briefs.find((b) => b.date === date); + }; + + return ( + + {children} + + ); +} + +export function useMorningBrief() { + const ctx = useContext(MorningBriefContext); + if (!ctx) { + throw new Error("useMorningBrief must be used within MorningBriefProvider"); + } + return ctx; +} diff --git a/lib/morning-brief/types.ts b/lib/morning-brief/types.ts new file mode 100644 index 0000000..a8e9fb1 --- /dev/null +++ b/lib/morning-brief/types.ts @@ -0,0 +1,59 @@ +// Morning Brief Types + +export interface MorningBrief { + id: string; + date: string; // YYYY-MM-DD + generatedAt: string; // ISO timestamp + + // Weather + weather?: { + location: string; + temperature: string; + condition: string; + humidity: string; + wind: string; + }; + + // AI News + aiNews: { + items: string[]; + }; + + // Things 3 Tasks (placeholder - needs integration) + things3Tasks?: { + today: string[]; + overdue: string[]; + }; + + // My actionable tasks (from Mission Control) + myTasks: { + pending: string[]; + inProgress: string[]; + dueToday: string[]; + }; + + // Market sentiment + market: { + usIndex: string; + crypto: string; + sentiment: 'bullish' | 'bearish' | 'neutral'; + topNews: string[]; + events: string[]; + }; +} + +export const defaultBrief: MorningBrief = { + id: '', + date: '', + generatedAt: '', + aiNews: { items: [] }, + things3Tasks: { today: [], overdue: [] }, + myTasks: { pending: [], inProgress: [], dueToday: [] }, + market: { + usIndex: '', + crypto: '', + sentiment: 'neutral', + topNews: [], + events: [], + } +};