311 lines
12 KiB
TypeScript
311 lines
12 KiB
TypeScript
"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<string | null>(null);
|
|
const [viewingBrief, setViewingBrief] = useState<MorningBrief | null>(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(<div key={`empty-${i}`} className="h-10" />);
|
|
}
|
|
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(
|
|
<button
|
|
key={day}
|
|
onClick={() => handleDateClick(day)}
|
|
disabled={!hasBrief}
|
|
className={`h-10 rounded-lg text-sm transition ${
|
|
hasBrief
|
|
? "bg-brand-pink/20 text-white hover:bg-brand-pink/40"
|
|
: "text-white/30"
|
|
} ${isToday ? "ring-2 ring-brand-pink" : ""}`}
|
|
>
|
|
{day}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Calendar */}
|
|
<div className="rounded-xl border border-white/10 bg-white/5 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<button
|
|
onClick={handlePrevMonth}
|
|
className="p-2 rounded-lg hover:bg-white/10 transition"
|
|
>
|
|
←
|
|
</button>
|
|
<h3 className="text-lg font-semibold">
|
|
{monthNames[currentMonth]} {currentYear}
|
|
</h3>
|
|
<button
|
|
onClick={handleNextMonth}
|
|
className="p-2 rounded-lg hover:bg-white/10 transition"
|
|
>
|
|
→
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-7 gap-1 mb-2">
|
|
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((d) => (
|
|
<div key={d} className="h-8 text-center text-xs text-white/50 font-medium">
|
|
{d}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-7 gap-1">
|
|
{calendarDays}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Today's Brief Preview */}
|
|
{todayBrief && (
|
|
<div className="rounded-xl border border-brand-pink/30 bg-brand-pink/10 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold">Today's Brief</h3>
|
|
<span className="text-xs text-white/50">{todayBrief.generatedAt}</span>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{/* Weather */}
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-white/50 uppercase">Weather</p>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl">🌤️</span>
|
|
<div>
|
|
<p className="font-semibold">{todayBrief.weather?.temperature}</p>
|
|
<p className="text-sm text-white/70">{todayBrief.weather?.condition}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Market */}
|
|
<div className="space-y-2">
|
|
<p className="text-xs text-white/50 uppercase">Market</p>
|
|
<div>
|
|
<p className="font-semibold">{todayBrief.market.usIndex}</p>
|
|
<p className="text-sm text-white/70">{todayBrief.market.crypto}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tasks */}
|
|
<div className="space-y-2 md:col-span-2">
|
|
<p className="text-xs text-white/50 uppercase">My Tasks Today</p>
|
|
<ul className="space-y-1">
|
|
{todayBrief.myTasks.pending.slice(0, 3).map((task, i) => (
|
|
<li key={i} className="text-sm text-white/80">• {task}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
{/* OpenClaw Use Cases */}
|
|
{todayBrief.openclowUseCases && todayBrief.openclowUseCases.skillIdeas.length > 0 && (
|
|
<div className="space-y-2 md:col-span-2">
|
|
<p className="text-xs text-white/50 uppercase">🦞 OpenClaw Skill Ideas</p>
|
|
<ul className="space-y-1">
|
|
{todayBrief.openclowUseCases.skillIdeas.map((idea, i) => (
|
|
<li key={i} className="text-sm text-purple-300 flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={idea.selected}
|
|
onChange={() => {
|
|
const updated = { ...todayBrief.openclowUseCases };
|
|
updated.skillIdeas[i].selected = !idea.selected;
|
|
// Save to localStorage
|
|
localStorage.setItem("sitemente:openclaw-usecases", JSON.stringify(updated));
|
|
}}
|
|
className="w-4 h-4 accent-purple-500"
|
|
/>
|
|
{idea.name}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Brief Detail Modal */}
|
|
{viewingBrief && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div
|
|
className="absolute inset-0 bg-black/60"
|
|
onClick={() => setViewingBrief(null)}
|
|
/>
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
className="relative z-10 w-full max-w-2xl max-h-[80vh] overflow-y-auto rounded-2xl border border-white/20 bg-[#2d2640] p-6 text-white"
|
|
>
|
|
<div className="flex items-start justify-between mb-6">
|
|
<div>
|
|
<p className="text-xs text-white/50 uppercase">Brief Date</p>
|
|
<h3 className="text-xl font-bold">{viewingBrief.date}</h3>
|
|
</div>
|
|
<button
|
|
onClick={() => setViewingBrief(null)}
|
|
className="p-2 rounded-lg hover:bg-white/10"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
{/* Weather */}
|
|
<section>
|
|
<h4 className="text-sm font-semibold text-white/70 mb-2">🌤️ Weather</h4>
|
|
<p>{viewingBrief.weather?.temperature} — {viewingBrief.weather?.condition}</p>
|
|
<p className="text-sm text-white/60">{viewingBrief.weather?.location}</p>
|
|
</section>
|
|
|
|
{/* AI News */}
|
|
<section>
|
|
<h4 className="text-sm font-semibold text-white/70 mb-2">🤖 AI News</h4>
|
|
<ul className="space-y-1">
|
|
{viewingBrief.aiNews.items.map((item, i) => (
|
|
<li key={i} className="text-sm">• {item}</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
|
|
{/* Tasks */}
|
|
<section>
|
|
<h4 className="text-sm font-semibold text-white/70 mb-2">✓ My Tasks</h4>
|
|
{viewingBrief.myTasks.pending.length > 0 && (
|
|
<div className="mb-2">
|
|
<p className="text-xs text-white/50">Pending</p>
|
|
{viewingBrief.myTasks.pending.map((t, i) => (
|
|
<p key={i} className="text-sm">• {t}</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
{viewingBrief.myTasks.inProgress.length > 0 && (
|
|
<div>
|
|
<p className="text-xs text-white/50">In Progress</p>
|
|
{viewingBrief.myTasks.inProgress.map((t, i) => (
|
|
<p key={i} className="text-sm">• {t}</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{/* Market */}
|
|
<section>
|
|
<h4 className="text-sm font-semibold text-white/70 mb-2">📈 Market</h4>
|
|
<p className="mb-1">{viewingBrief.market.usIndex}</p>
|
|
<p className="mb-2">{viewingBrief.market.crypto}</p>
|
|
<p className="text-sm">
|
|
Sentiment: <span className={`font-semibold ${
|
|
viewingBrief.market.sentiment === "bullish" ? "text-green-400" :
|
|
viewingBrief.market.sentiment === "bearish" ? "text-red-400" : "text-white/70"
|
|
}`}>{viewingBrief.market.sentiment.toUpperCase()}</span>
|
|
</p>
|
|
</section>
|
|
|
|
{/* OpenClaw Use Cases */}
|
|
{viewingBrief.openclowUseCases && viewingBrief.openclowUseCases.skillIdeas.length > 0 && (
|
|
<section>
|
|
<h4 className="text-sm font-semibold text-white/70 mb-2">🦞 OpenClaw Skill Ideas</h4>
|
|
<ul className="space-y-1">
|
|
{viewingBrief.openclowUseCases.skillIdeas.map((idea, i) => (
|
|
<li key={i} className="text-sm text-purple-300">• {idea}</li>
|
|
))}
|
|
</ul>
|
|
{viewingBrief.openclowUseCases.topUseCases.length > 0 && (
|
|
<div className="mt-2">
|
|
<p className="text-xs text-white/50">Top Use Cases:</p>
|
|
{viewingBrief.openclowUseCases.topUseCases.map((uc, i) => (
|
|
<p key={i} className="text-sm text-white/60">• {uc}</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Generate Button */}
|
|
<button
|
|
onClick={generateBrief}
|
|
className="w-full rounded-xl bg-brand-pink py-3 font-semibold text-white hover:bg-[#ff7bc0] transition"
|
|
>
|
|
Generate Today's Brief
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|