516 lines
21 KiB
TypeScript
516 lines
21 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useCallback } from "react";
|
||
import Card from "@/components/ui/Card";
|
||
|
||
const today = () => new Date().toISOString().split("T")[0];
|
||
|
||
interface MarketData {
|
||
btc: string;
|
||
eth: string;
|
||
sol: string;
|
||
btcChange: string;
|
||
ethChange: string;
|
||
solChange: string;
|
||
}
|
||
|
||
interface SitementeStatus {
|
||
serverUptime: string;
|
||
demoPages: string;
|
||
leadStatus: string;
|
||
}
|
||
|
||
interface WarmLead {
|
||
name: string;
|
||
business: string;
|
||
note: string;
|
||
}
|
||
|
||
interface FormState {
|
||
date: string;
|
||
location: string;
|
||
weather: string;
|
||
marketData: MarketData;
|
||
aiNewsHeadlines: string[];
|
||
skillsToLearn: string[];
|
||
euAiActStatus: "OK" | "Risk" | "Opportunity";
|
||
sitementeStatus: SitementeStatus;
|
||
warmLeads: WarmLead[];
|
||
dayPriorities: string[];
|
||
skippedTasks: string[];
|
||
autoExecutionSummary: string;
|
||
}
|
||
|
||
const defaultForm: FormState = {
|
||
date: today(),
|
||
location: "Benalmádena",
|
||
weather: "",
|
||
marketData: { btc: "", eth: "", sol: "", btcChange: "", ethChange: "", solChange: "" },
|
||
aiNewsHeadlines: ["", "", "", "", ""],
|
||
skillsToLearn: ["", "", ""],
|
||
euAiActStatus: "OK",
|
||
sitementeStatus: { serverUptime: "", demoPages: "", leadStatus: "" },
|
||
warmLeads: [
|
||
{ name: "", business: "", note: "" },
|
||
{ name: "", business: "", note: "" },
|
||
{ name: "", business: "", note: "" },
|
||
],
|
||
dayPriorities: ["", "", "", "", ""],
|
||
skippedTasks: [""],
|
||
autoExecutionSummary: "",
|
||
};
|
||
|
||
export default function MorningBriefPage() {
|
||
const [form, setForm] = useState<FormState>(defaultForm);
|
||
const [saving, setSaving] = useState(false);
|
||
const [saved, setSaved] = useState(false);
|
||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||
const [loadingDate, setLoadingDate] = useState(false);
|
||
|
||
const loadBriefForDate = useCallback(async (date: string) => {
|
||
setLoadingDate(true);
|
||
try {
|
||
const res = await fetch(`/api/morning?date=${date}`);
|
||
const data = await res.json();
|
||
if (data) {
|
||
setForm({
|
||
date: data.date,
|
||
location: data.location || "Benalmádena",
|
||
weather: data.weather || "",
|
||
marketData: typeof data.marketData === "string" ? JSON.parse(data.marketData) : (data.marketData || defaultForm.marketData),
|
||
aiNewsHeadlines: typeof data.aiNewsHeadlines === "string" ? JSON.parse(data.aiNewsHeadlines) : (data.aiNewsHeadlines || defaultForm.aiNewsHeadlines),
|
||
skillsToLearn: typeof data.skillsToLearn === "string" ? JSON.parse(data.skillsToLearn) : (data.skillsToLearn || defaultForm.skillsToLearn),
|
||
euAiActStatus: data.euAiActStatus || "OK",
|
||
sitementeStatus: typeof data.sitementeStatus === "string" ? JSON.parse(data.sitementeStatus) : (data.sitementeStatus || defaultForm.sitementeStatus),
|
||
warmLeads: typeof data.warmLeads === "string" ? JSON.parse(data.warmLeads) : (data.warmLeads || defaultForm.warmLeads),
|
||
dayPriorities: typeof data.dayPriorities === "string" ? JSON.parse(data.dayPriorities) : (data.dayPriorities || defaultForm.dayPriorities),
|
||
skippedTasks: typeof data.skippedTasks === "string" ? JSON.parse(data.skippedTasks) : (data.skippedTasks || defaultForm.skippedTasks),
|
||
autoExecutionSummary: data.autoExecutionSummary || "",
|
||
});
|
||
} else {
|
||
setForm({ ...defaultForm, date });
|
||
}
|
||
} catch {
|
||
setForm({ ...defaultForm, date });
|
||
} finally {
|
||
setLoadingDate(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadBriefForDate(today());
|
||
}, [loadBriefForDate]);
|
||
|
||
const validate = (): boolean => {
|
||
const errs: Record<string, string> = {};
|
||
if (!form.location.trim()) errs.location = "Location is required";
|
||
if (!form.weather.trim()) errs.weather = "Weather is required";
|
||
if (!form.dayPriorities.some((p) => p.trim())) errs.dayPriorities = "At least one priority is required";
|
||
setErrors(errs);
|
||
return Object.keys(errs).length === 0;
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
if (!validate()) return;
|
||
setSaving(true);
|
||
try {
|
||
const res = await fetch("/api/morning", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(form),
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
setErrors({ submit: err.error || "Failed to save" });
|
||
} else {
|
||
setSaved(true);
|
||
setTimeout(() => setSaved(false), 3000);
|
||
}
|
||
} catch {
|
||
setErrors({ submit: "Network error. Please try again." });
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const updateArrayItem = (field: keyof FormState, index: number, value: string) => {
|
||
setForm((prev) => {
|
||
const arr = [...(prev[field] as string[])];
|
||
arr[index] = value;
|
||
return { ...prev, [field]: arr };
|
||
});
|
||
};
|
||
|
||
const addArrayItem = (field: keyof FormState, defaultVal: string = "") => {
|
||
setForm((prev) => ({ ...prev, [field]: [...(prev[field] as string[]), defaultVal] }));
|
||
};
|
||
|
||
const removeArrayItem = (field: keyof FormState, index: number) => {
|
||
setForm((prev) => {
|
||
const arr = (prev[field] as string[]).filter((_, i) => i !== index);
|
||
return { ...prev, [field]: arr };
|
||
});
|
||
};
|
||
|
||
const updateLead = (index: number, field: keyof WarmLead, value: string) => {
|
||
setForm((prev) => {
|
||
const leads = [...prev.warmLeads];
|
||
leads[index] = { ...leads[index], [field]: value };
|
||
return { ...prev, warmLeads: leads };
|
||
});
|
||
};
|
||
|
||
const changeColor = (val: string) => {
|
||
const n = parseFloat(val);
|
||
if (isNaN(n)) return "text-slate-400";
|
||
return n >= 0 ? "text-emerald-400" : "text-red-400";
|
||
};
|
||
|
||
return (
|
||
<div className="p-6 max-w-4xl mx-auto">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div>
|
||
<h1 className="text-xl font-bold text-white flex items-center gap-2">
|
||
<span className="text-amber-400">☀</span> Morning Brief
|
||
</h1>
|
||
<p className="text-sm text-slate-500 mt-0.5">Horus daily intelligence log</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
type="date"
|
||
value={form.date}
|
||
onChange={(e) => {
|
||
setForm((prev) => ({ ...prev, date: e.target.value }));
|
||
loadBriefForDate(e.target.value);
|
||
}}
|
||
className="bg-[#1a1d27] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-300 focus:outline-none focus:border-amber-500/50"
|
||
/>
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={saving || loadingDate}
|
||
className="px-4 py-2 bg-amber-500 hover:bg-amber-400 disabled:opacity-50 text-black font-semibold text-sm rounded-lg transition-colors"
|
||
>
|
||
{saving ? "Saving…" : saved ? "✓ Saved" : "Save Brief"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{errors.submit && (
|
||
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
||
{errors.submit}
|
||
</div>
|
||
)}
|
||
|
||
{loadingDate && (
|
||
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-blue-400 text-sm">
|
||
Loading brief for selected date…
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-5">
|
||
{/* Location & Weather */}
|
||
<Card title="📍 Location & Weather" accent="amber">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-xs text-slate-500 mb-1.5">Location</label>
|
||
<input
|
||
type="text"
|
||
value={form.location}
|
||
onChange={(e) => setForm((p) => ({ ...p, location: e.target.value }))}
|
||
placeholder="Benalmádena"
|
||
className={`w-full bg-[#0f1117] border ${errors.location ? "border-red-500/50" : "border-[#2a2d3a]"} rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50`}
|
||
/>
|
||
{errors.location && <p className="text-xs text-red-400 mt-1">{errors.location}</p>}
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-slate-500 mb-1.5">Weather</label>
|
||
<input
|
||
type="text"
|
||
value={form.weather}
|
||
onChange={(e) => setForm((p) => ({ ...p, weather: e.target.value }))}
|
||
placeholder="e.g. 22°C, Sunny"
|
||
className={`w-full bg-[#0f1117] border ${errors.weather ? "border-red-500/50" : "border-[#2a2d3a]"} rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50`}
|
||
/>
|
||
{errors.weather && <p className="text-xs text-red-400 mt-1">{errors.weather}</p>}
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Market Prices */}
|
||
<Card title="📈 Market Prices" accent="blue">
|
||
<div className="grid grid-cols-3 gap-4">
|
||
{(["btc", "eth", "sol"] as const).map((coin) => (
|
||
<div key={coin}>
|
||
<label className="block text-xs text-slate-500 mb-1.5 uppercase font-mono">{coin}</label>
|
||
<input
|
||
type="text"
|
||
value={form.marketData[coin]}
|
||
onChange={(e) =>
|
||
setForm((p) => ({ ...p, marketData: { ...p.marketData, [coin]: e.target.value } }))
|
||
}
|
||
placeholder="$0.00"
|
||
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 mb-2"
|
||
/>
|
||
<input
|
||
type="text"
|
||
value={form.marketData[`${coin}Change` as keyof MarketData]}
|
||
onChange={(e) =>
|
||
setForm((p) => ({
|
||
...p,
|
||
marketData: { ...p.marketData, [`${coin}Change`]: e.target.value },
|
||
}))
|
||
}
|
||
placeholder="24h % (e.g. +2.5)"
|
||
className={`w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-blue-500/50 ${changeColor(form.marketData[`${coin}Change` as keyof MarketData])}`}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* AI News Headlines */}
|
||
<Card title="🤖 AI News Headlines" subtitle="3–5 headlines" accent="purple">
|
||
<div className="space-y-2">
|
||
{form.aiNewsHeadlines.map((h, i) => (
|
||
<div key={i} className="flex gap-2">
|
||
<span className="text-xs text-slate-600 font-mono mt-2.5 w-4">{i + 1}.</span>
|
||
<input
|
||
type="text"
|
||
value={h}
|
||
onChange={(e) => updateArrayItem("aiNewsHeadlines", i, e.target.value)}
|
||
placeholder={`Headline ${i + 1}`}
|
||
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-purple-500/50"
|
||
/>
|
||
{form.aiNewsHeadlines.length > 3 && (
|
||
<button
|
||
onClick={() => removeArrayItem("aiNewsHeadlines", i)}
|
||
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
{form.aiNewsHeadlines.length < 7 && (
|
||
<button
|
||
onClick={() => addArrayItem("aiNewsHeadlines")}
|
||
className="text-xs text-purple-400 hover:text-purple-300 transition-colors mt-1"
|
||
>
|
||
+ Add headline
|
||
</button>
|
||
)}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Skills to Learn */}
|
||
<Card title="🎯 Top Skills to Learn" subtitle="Top 3" accent="green">
|
||
<div className="space-y-2">
|
||
{form.skillsToLearn.map((s, i) => (
|
||
<div key={i} className="flex gap-2">
|
||
<span className="text-xs text-slate-600 font-mono mt-2.5 w-4">{i + 1}.</span>
|
||
<input
|
||
type="text"
|
||
value={s}
|
||
onChange={(e) => updateArrayItem("skillsToLearn", i, e.target.value)}
|
||
placeholder={`Skill ${i + 1}`}
|
||
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-emerald-500/50"
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* EU AI Act Status */}
|
||
<Card title="🇪🇺 EU AI Act Status" accent="amber">
|
||
<div className="flex gap-3">
|
||
{(["OK", "Risk", "Opportunity"] as const).map((status) => (
|
||
<button
|
||
key={status}
|
||
onClick={() => setForm((p) => ({ ...p, euAiActStatus: status }))}
|
||
className={`flex-1 py-2.5 rounded-lg text-sm font-medium border transition-all ${
|
||
form.euAiActStatus === status
|
||
? status === "OK"
|
||
? "bg-emerald-500/20 border-emerald-500/50 text-emerald-400"
|
||
: status === "Risk"
|
||
? "bg-red-500/20 border-red-500/50 text-red-400"
|
||
: "bg-blue-500/20 border-blue-500/50 text-blue-400"
|
||
: "bg-transparent border-[#2a2d3a] text-slate-500 hover:border-slate-500"
|
||
}`}
|
||
>
|
||
{status === "OK" ? "✓ OK" : status === "Risk" ? "⚠ Risk" : "💡 Opportunity"}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* SiteMente Status */}
|
||
<Card title="🌐 SiteMente Status" accent="blue">
|
||
<div className="space-y-3">
|
||
<div>
|
||
<label className="block text-xs text-slate-500 mb-1.5">Server Uptime</label>
|
||
<input
|
||
type="text"
|
||
value={form.sitementeStatus.serverUptime}
|
||
onChange={(e) =>
|
||
setForm((p) => ({ ...p, sitementeStatus: { ...p.sitementeStatus, serverUptime: e.target.value } }))
|
||
}
|
||
placeholder="e.g. 99.9% — All systems operational"
|
||
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-slate-500 mb-1.5">Demo Pages</label>
|
||
<input
|
||
type="text"
|
||
value={form.sitementeStatus.demoPages}
|
||
onChange={(e) =>
|
||
setForm((p) => ({ ...p, sitementeStatus: { ...p.sitementeStatus, demoPages: e.target.value } }))
|
||
}
|
||
placeholder="e.g. 3 live, 1 in review"
|
||
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-xs text-slate-500 mb-1.5">Lead Status (Supabase)</label>
|
||
<textarea
|
||
value={form.sitementeStatus.leadStatus}
|
||
onChange={(e) =>
|
||
setForm((p) => ({ ...p, sitementeStatus: { ...p.sitementeStatus, leadStatus: e.target.value } }))
|
||
}
|
||
placeholder="e.g. 12 leads in pipeline, 3 hot prospects, 1 demo scheduled"
|
||
rows={2}
|
||
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 resize-none"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Warm Leads */}
|
||
<Card title="🔥 Warm Leads to Contact" subtitle="Top 3" accent="red">
|
||
<div className="space-y-4">
|
||
{form.warmLeads.map((lead, i) => (
|
||
<div key={i} className="p-3 bg-[#0f1117] rounded-lg border border-[#2a2d3a]">
|
||
<p className="text-xs text-slate-600 font-mono mb-2">Lead #{i + 1}</p>
|
||
<div className="grid grid-cols-2 gap-2 mb-2">
|
||
<input
|
||
type="text"
|
||
value={lead.name}
|
||
onChange={(e) => updateLead(i, "name", e.target.value)}
|
||
placeholder="Name"
|
||
className="bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50"
|
||
/>
|
||
<input
|
||
type="text"
|
||
value={lead.business}
|
||
onChange={(e) => updateLead(i, "business", e.target.value)}
|
||
placeholder="Business"
|
||
className="bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50"
|
||
/>
|
||
</div>
|
||
<input
|
||
type="text"
|
||
value={lead.note}
|
||
onChange={(e) => updateLead(i, "note", e.target.value)}
|
||
placeholder="Note / context"
|
||
className="w-full bg-[#13151f] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-red-500/50"
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Day Priorities */}
|
||
<Card title="⚡ Day Priorities" subtitle="5–7 numbered priorities" accent="amber">
|
||
{errors.dayPriorities && (
|
||
<p className="text-xs text-red-400 mb-2">{errors.dayPriorities}</p>
|
||
)}
|
||
<div className="space-y-2">
|
||
{form.dayPriorities.map((p, i) => (
|
||
<div key={i} className="flex gap-2">
|
||
<span className="text-xs text-amber-500/70 font-mono mt-2.5 w-5">{i + 1}.</span>
|
||
<input
|
||
type="text"
|
||
value={p}
|
||
onChange={(e) => updateArrayItem("dayPriorities", i, e.target.value)}
|
||
placeholder={`Priority ${i + 1}`}
|
||
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50"
|
||
/>
|
||
{form.dayPriorities.length > 3 && (
|
||
<button
|
||
onClick={() => removeArrayItem("dayPriorities", i)}
|
||
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
{form.dayPriorities.length < 7 && (
|
||
<button
|
||
onClick={() => addArrayItem("dayPriorities")}
|
||
className="text-xs text-amber-400 hover:text-amber-300 transition-colors mt-1"
|
||
>
|
||
+ Add priority
|
||
</button>
|
||
)}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Skipped Tasks */}
|
||
<Card title="⏭ Skipped Tasks" subtitle="Tasks carried over from yesterday">
|
||
<div className="space-y-2">
|
||
{form.skippedTasks.map((t, i) => (
|
||
<div key={i} className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={t}
|
||
onChange={(e) => updateArrayItem("skippedTasks", i, e.target.value)}
|
||
placeholder="Skipped task"
|
||
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-slate-500/50"
|
||
/>
|
||
{form.skippedTasks.length > 1 && (
|
||
<button
|
||
onClick={() => removeArrayItem("skippedTasks", i)}
|
||
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
<button
|
||
onClick={() => addArrayItem("skippedTasks")}
|
||
className="text-xs text-slate-400 hover:text-slate-300 transition-colors mt-1"
|
||
>
|
||
+ Add skipped task
|
||
</button>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Auto-Execution Summary */}
|
||
<Card title="🦅 Horus Auto-Execution" subtitle="What Horus will do without asking" accent="amber">
|
||
<textarea
|
||
value={form.autoExecutionSummary}
|
||
onChange={(e) => setForm((p) => ({ ...p, autoExecutionSummary: e.target.value }))}
|
||
placeholder="e.g. Monitor BTC for breakout above $70k, send SiteMente weekly report to leads, update Notion task board…"
|
||
rows={4}
|
||
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50 resize-none"
|
||
/>
|
||
</Card>
|
||
|
||
{/* Save Button */}
|
||
<div className="flex justify-end pb-6">
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={saving}
|
||
className="px-6 py-2.5 bg-amber-500 hover:bg-amber-400 disabled:opacity-50 text-black font-semibold text-sm rounded-lg transition-colors"
|
||
>
|
||
{saving ? "Saving…" : saved ? "✓ Brief Saved!" : "Save Morning Brief"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|