fix: remove services from main, compact calendar + large events panel
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m11s

This commit is contained in:
Cosmo
2026-04-16 09:15:55 +00:00
parent 8321fc9252
commit 254ecbfd14
2 changed files with 142 additions and 143 deletions

View File

@@ -4,7 +4,6 @@ import { WeatherWidget } from "@/components/widgets/WeatherWidget";
import { CalendarWidget } from "@/components/widgets/CalendarWidget"; import { CalendarWidget } from "@/components/widgets/CalendarWidget";
import { ClaudeUsageWidget } from "@/components/widgets/ClaudeUsageWidget"; import { ClaudeUsageWidget } from "@/components/widgets/ClaudeUsageWidget";
import { ClaudeApiWidget } from "@/components/widgets/ClaudeApiWidget"; import { ClaudeApiWidget } from "@/components/widgets/ClaudeApiWidget";
import { ServicesGrid } from "@/components/widgets/ServicesGrid";
import { DashboardHeader } from "@/components/widgets/DashboardHeader"; import { DashboardHeader } from "@/components/widgets/DashboardHeader";
export default function DashboardPage() { export default function DashboardPage() {
@@ -21,15 +20,10 @@ export default function DashboardPage() {
<CalendarWidget /> <CalendarWidget />
</div> </div>
<div className="space-y-5"> <div className="space-y-5">
<div className="grid grid-cols-2 gap-5 lg:grid-cols-1"> <ClaudeUsageWidget />
<ClaudeUsageWidget /> <ClaudeApiWidget />
<ClaudeApiWidget />
</div>
</div> </div>
</div> </div>
{/* Services */}
<ServicesGrid />
</div> </div>
); );
} }

View File

@@ -27,7 +27,6 @@ export function CalendarWidget() {
const [newEvent, setNewEvent] = useState({ title: "", startTime: "09:00", endTime: "10:00", allDay: false }); const [newEvent, setNewEvent] = useState({ title: "", startTime: "09:00", endTime: "10:00", allDay: false });
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [createError, setCreateError] = useState(""); const [createError, setCreateError] = useState("");
const [calError, setCalError] = useState<string | null>(null);
const monthStr = `${year}-${String(month + 1).padStart(2, "0")}`; const monthStr = `${year}-${String(month + 1).padStart(2, "0")}`;
@@ -35,8 +34,6 @@ export function CalendarWidget() {
try { try {
const res = await fetch(`/api/calendar?month=${monthStr}`); const res = await fetch(`/api/calendar?month=${monthStr}`);
const data = await res.json(); const data = await res.json();
if (data.error && data.events?.length === 0) setCalError(data.error);
else setCalError(null);
setMonthEvents(data.events ?? []); setMonthEvents(data.events ?? []);
} catch { setMonthEvents([]); } } catch { setMonthEvents([]); }
}, [monthStr]); }, [monthStr]);
@@ -68,7 +65,6 @@ export function CalendarWidget() {
const daysInMonth = new Date(year, month + 1, 0).getDate(); const daysInMonth = new Date(year, month + 1, 0).getDate();
const startOffset = firstDay === 0 ? 6 : firstDay - 1; const startOffset = firstDay === 0 ? 6 : firstDay - 1;
const cells = Array(startOffset).fill(null).concat(Array.from({ length: daysInMonth }, (_, i) => i + 1)); const cells = Array(startOffset).fill(null).concat(Array.from({ length: daysInMonth }, (_, i) => i + 1));
const datesWithEvents = new Set(monthEvents.map(e => (e.start || "").split("T")[0])); const datesWithEvents = new Set(monthEvents.map(e => (e.start || "").split("T")[0]));
const createEvent = async () => { const createEvent = async () => {
@@ -86,155 +82,164 @@ export function CalendarWidget() {
setNewEvent({ title: "", startTime: "09:00", endTime: "10:00", allDay: false }); setNewEvent({ title: "", startTime: "09:00", endTime: "10:00", allDay: false });
fetchDay(selectedDate); fetchDay(selectedDate);
fetchMonth(); fetchMonth();
} catch (e: unknown) { setCreateError(e instanceof Error ? e.message : "Ошибка создания"); } } catch (e: unknown) { setCreateError(e instanceof Error ? e.message : "Ошибка"); }
finally { setCreating(false); } finally { setCreating(false); }
}; };
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,"0")}-${String(today.getDate()).padStart(2,"0")}`; const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,"0")}-${String(today.getDate()).padStart(2,"0")}`;
if (calError === "Google Calendar not configured") {
return (
<div className="card card-accent-emerald p-5 flex flex-col gap-4 items-center justify-center min-h-[200px]">
<div className="text-4xl">📅</div>
<div className="text-sm font-medium text-white">Google Calendar</div>
<div className="text-xs text-slate-400 text-center">Не настроен. Добавь переменные окружения в Coolify.</div>
</div>
);
}
return ( return (
<div className="card card-accent-violet p-5 flex flex-col gap-4"> <div className="card card-violet flex flex-col gap-0" style={{ borderTop: "2px solid #8b5cf6" }}>
{/* Header */} <div className="grid grid-cols-1 md:grid-cols-2 gap-0 divide-x divide-white/5">
<div className="flex items-center justify-between">
<button onClick={prevMonth} className="p-1.5 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white transition-colors">
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm font-semibold gradient-text">{MONTHS[month]} {year}</span>
<button onClick={nextMonth} className="p-1.5 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white transition-colors">
<ChevronRight className="w-4 h-4" />
</button>
</div>
{/* Weekday labels */} {/* LEFT: compact calendar */}
<div className="grid grid-cols-7 gap-0.5"> <div className="p-4 flex flex-col gap-3">
{WEEK_DAYS.map(d => ( {/* Month nav */}
<div key={d} className="text-center text-[10px] font-semibold text-slate-600 py-1">{d}</div>
))}
{cells.map((day, i) => {
if (!day) return <div key={`empty-${i}`} />;
const dateStr = `${year}-${String(month+1).padStart(2,"0")}-${String(day).padStart(2,"0")}`;
const isToday = dateStr === todayStr;
const isSelected = dateStr === selectedDate;
const hasEvents = datesWithEvents.has(dateStr);
return (
<button
key={dateStr}
onClick={() => handleDayClick(day)}
className={`relative flex flex-col items-center justify-center aspect-square rounded-xl text-sm transition-all ${
isSelected
? "text-white font-semibold"
: isToday
? "text-violet-300 font-semibold"
: "text-slate-400 hover:text-white hover:bg-white/5"
}`}
style={isSelected ? { background: "linear-gradient(135deg, #6366f1, #8b5cf6)" } :
isToday ? { background: "rgba(139,92,246,0.15)", boxShadow: "inset 0 0 0 1px rgba(139,92,246,0.4)" } : {}}
>
{day}
{hasEvents && (
<span className={`absolute bottom-1 w-1 h-1 rounded-full ${isSelected ? "bg-white" : "bg-violet-400"}`} />
)}
</button>
);
})}
</div>
{/* Day events panel */}
{selectedDate && (
<div className="border-t border-white/5 pt-3 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-400"> <button onClick={prevMonth} className="p-1 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white transition-colors">
{new Date(selectedDate + "T12:00:00").toLocaleDateString("ru-RU", { day: "numeric", month: "long" })} <ChevronLeft className="w-4 h-4" />
</span> </button>
<button <span className="text-sm font-semibold gradient-text">{MONTHS[month]} {year}</span>
onClick={() => setShowCreate(s => !s)} <button onClick={nextMonth} className="p-1 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white transition-colors">
className="flex items-center gap-1 text-xs text-violet-400 hover:text-violet-300 transition-colors" <ChevronRight className="w-4 h-4" />
>
<Plus className="w-3 h-3" /> Добавить
</button> </button>
</div> </div>
{showCreate && ( {/* Grid */}
<div className="bg-white/3 rounded-xl p-3 space-y-2 border border-white/5"> <div className="grid grid-cols-7 gap-0.5">
<input {WEEK_DAYS.map(d => (
className="w-full bg-white/5 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-600 outline-none focus:ring-1 focus:ring-violet-500 border border-white/5" <div key={d} className="text-center text-[9px] font-semibold text-slate-600 pb-1">{d}</div>
placeholder="Название события" ))}
value={newEvent.title} {cells.map((day, i) => {
onChange={e => setNewEvent(n => ({ ...n, title: e.target.value }))} if (!day) return <div key={`e-${i}`} />;
onKeyDown={e => e.key === "Enter" && createEvent()} const dateStr = `${year}-${String(month+1).padStart(2,"0")}-${String(day).padStart(2,"0")}`;
/> const isToday = dateStr === todayStr;
<div className="flex items-center gap-2"> const isSelected = dateStr === selectedDate;
<label className="flex items-center gap-1.5 text-xs text-slate-400 cursor-pointer"> const hasEvents = datesWithEvents.has(dateStr);
<input type="checkbox" className="accent-violet-500" checked={newEvent.allDay} return (
onChange={e => setNewEvent(n => ({ ...n, allDay: e.target.checked }))} /> <button
Весь день key={dateStr}
</label> onClick={() => handleDayClick(day)}
className={`relative flex flex-col items-center justify-center rounded-lg text-xs transition-all py-1.5 ${
isSelected ? "text-white font-semibold" :
isToday ? "text-violet-300 font-semibold" :
"text-slate-400 hover:text-white hover:bg-white/5"
}`}
style={isSelected ? { background: "linear-gradient(135deg,#6366f1,#8b5cf6)" } :
isToday ? { background: "rgba(139,92,246,0.15)", boxShadow: "inset 0 0 0 1px rgba(139,92,246,0.4)" } : {}}
>
{day}
{hasEvents && (
<span className={`absolute bottom-0.5 w-1 h-1 rounded-full ${isSelected ? "bg-white" : "bg-violet-400"}`} />
)}
</button>
);
})}
</div>
</div>
{/* RIGHT: events for selected day */}
<div className="p-4 flex flex-col gap-3 min-h-[300px]">
{!selectedDate ? (
<div className="flex-1 flex flex-col items-center justify-center text-slate-600 gap-2">
<span className="text-3xl">📅</span>
<span className="text-xs">Выбери день</span>
</div>
) : (
<>
{/* Day header */}
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-white">
{new Date(selectedDate + "T12:00:00").toLocaleDateString("ru-RU", { day: "numeric", month: "long" })}
</span>
<button
onClick={() => setShowCreate(s => !s)}
className="flex items-center gap-1 text-xs text-violet-400 hover:text-violet-300 transition-colors px-2 py-1 rounded-lg hover:bg-white/5"
>
<Plus className="w-3 h-3" /> Добавить
</button>
</div> </div>
{!newEvent.allDay && (
<div className="flex gap-2"> {/* Create form */}
<input type="time" value={newEvent.startTime} onChange={e => setNewEvent(n => ({ ...n, startTime: e.target.value }))} {showCreate && (
className="flex-1 bg-white/5 rounded-lg px-2 py-1.5 text-xs text-white outline-none focus:ring-1 focus:ring-violet-500" /> <div className="bg-white/3 rounded-xl p-3 space-y-2 border border-white/5 flex-shrink-0">
<span className="text-slate-600 self-center"></span> <input
<input type="time" value={newEvent.endTime} onChange={e => setNewEvent(n => ({ ...n, endTime: e.target.value }))} className="w-full bg-white/5 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-600 outline-none focus:ring-1 focus:ring-violet-500 border border-white/5"
className="flex-1 bg-white/5 rounded-lg px-2 py-1.5 text-xs text-white outline-none focus:ring-1 focus:ring-violet-500" /> placeholder="Название события"
value={newEvent.title}
onChange={e => setNewEvent(n => ({ ...n, title: e.target.value }))}
onKeyDown={e => e.key === "Enter" && createEvent()}
autoFocus
/>
<label className="flex items-center gap-1.5 text-xs text-slate-400 cursor-pointer">
<input type="checkbox" className="accent-violet-500" checked={newEvent.allDay}
onChange={e => setNewEvent(n => ({ ...n, allDay: e.target.checked }))} />
Весь день
</label>
{!newEvent.allDay && (
<div className="flex gap-2 items-center">
<input type="time" value={newEvent.startTime} onChange={e => setNewEvent(n => ({ ...n, startTime: e.target.value }))}
className="flex-1 bg-white/5 rounded-lg px-2 py-1.5 text-xs text-white outline-none focus:ring-1 focus:ring-violet-500" />
<span className="text-slate-600 text-xs"></span>
<input type="time" value={newEvent.endTime} onChange={e => setNewEvent(n => ({ ...n, endTime: e.target.value }))}
className="flex-1 bg-white/5 rounded-lg px-2 py-1.5 text-xs text-white outline-none focus:ring-1 focus:ring-violet-500" />
</div>
)}
{createError && <div className="text-xs text-red-400">{createError}</div>}
<div className="flex gap-2">
<button onClick={createEvent} disabled={creating || !newEvent.title.trim()}
className="flex-1 text-white text-xs py-2 rounded-lg transition-colors disabled:opacity-50"
style={{ background: "linear-gradient(135deg,#6366f1,#8b5cf6)" }}>
{creating ? "Создаю..." : "Создать в Google Calendar"}
</button>
<button onClick={() => setShowCreate(false)} className="p-2 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white">
<X className="w-3.5 h-3.5" />
</button>
</div>
</div> </div>
)} )}
{createError && <div className="text-xs text-red-400">{createError}</div>}
<div className="flex gap-2">
<button onClick={createEvent} disabled={creating || !newEvent.title.trim()}
className="flex-1 text-white text-xs py-2 rounded-lg transition-colors disabled:opacity-50"
style={{ background: "linear-gradient(135deg, #6366f1, #8b5cf6)" }}>
{creating ? "Создаю..." : "Создать"}
</button>
<button onClick={() => setShowCreate(false)} className="p-2 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white">
<X className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
{loadingDay ? ( {/* Events list */}
<div className="space-y-1.5 animate-pulse"> <div className="flex-1 overflow-y-auto space-y-2">
{[1,2].map(i => <div key={i} className="h-8 bg-white/5 rounded-lg" />)} {loadingDay ? (
</div> <div className="space-y-2 animate-pulse">
) : dayEvents.length === 0 ? ( {[1,2,3].map(i => <div key={i} className="h-14 bg-white/5 rounded-xl" />)}
<div className="text-xs text-slate-600 text-center py-2">Нет событий</div>
) : (
<div className="space-y-1.5 max-h-40 overflow-y-auto">
{dayEvents.map(ev => (
<div key={ev.id} className="flex items-start gap-2 p-2.5 rounded-xl bg-white/3 border border-white/5 group">
<div className="w-0.5 h-full min-h-[32px] rounded-full flex-shrink-0"
style={{ background: "linear-gradient(to bottom, #8b5cf6, #6366f1)" }} />
<div className="flex-1 min-w-0">
<div className="text-xs font-medium text-white truncate">{ev.title}</div>
<div className="text-[10px] text-slate-500 flex items-center gap-1 mt-0.5">
<Clock className="w-2.5 h-2.5" />
{ev.allDay ? "Весь день" : formatTime(ev.start)}
{ev.end && !ev.allDay && `${formatTime(ev.end)}`}
</div>
</div> </div>
{ev.htmlLink && ( ) : dayEvents.length === 0 ? (
<a href={ev.htmlLink} target="_blank" rel="noopener noreferrer" <div className="flex flex-col items-center justify-center h-full text-slate-600 gap-2 py-8">
className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-white transition-all"> <span className="text-2xl">🗓</span>
<ExternalLink className="w-3 h-3" /> <span className="text-xs">Нет событий</span>
</a> <button onClick={() => setShowCreate(true)} className="text-xs text-violet-400 hover:text-violet-300 mt-1">
)} + Создать событие
</div> </button>
))} </div>
</div> ) : (
dayEvents.map(ev => (
<div key={ev.id} className="flex items-start gap-3 p-3 rounded-xl bg-white/3 border border-white/5 group hover:bg-white/5 transition-colors">
<div className="w-1 self-stretch rounded-full flex-shrink-0 mt-0.5"
style={{ background: "linear-gradient(to bottom,#8b5cf6,#6366f1)", minHeight: "32px" }} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-white truncate">{ev.title}</div>
<div className="text-xs text-slate-500 flex items-center gap-1 mt-0.5">
<Clock className="w-3 h-3" />
{ev.allDay ? "Весь день" : formatTime(ev.start)}
{ev.end && !ev.allDay && `${formatTime(ev.end)}`}
</div>
</div>
{ev.htmlLink && (
<a href={ev.htmlLink} target="_blank" rel="noopener noreferrer"
className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-violet-400 transition-all flex-shrink-0">
<ExternalLink className="w-4 h-4" />
</a>
)}
</div>
))
)}
</div>
</>
)} )}
</div> </div>
)} </div>
</div> </div>
); );
} }