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> </div>
{/* Services */}
<ServicesGrid />
</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,42 +82,36 @@ 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 ( return (
<div className="card card-accent-emerald p-5 flex flex-col gap-4 items-center justify-center min-h-[200px]"> <div className="card card-violet flex flex-col gap-0" style={{ borderTop: "2px solid #8b5cf6" }}>
<div className="text-4xl">📅</div> <div className="grid grid-cols-1 md:grid-cols-2 gap-0 divide-x divide-white/5">
<div className="text-sm font-medium text-white">Google Calendar</div>
<div className="text-xs text-slate-400 text-center">Не настроен. Добавь переменные окружения в Coolify.</div>
</div>
);
}
return ( {/* LEFT: compact calendar */}
<div className="card card-accent-violet p-5 flex flex-col gap-4"> <div className="p-4 flex flex-col gap-3">
{/* Header */} {/* Month nav */}
<div className="flex items-center justify-between"> <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"> <button onClick={prevMonth} className="p-1 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white transition-colors">
<ChevronLeft className="w-4 h-4" /> <ChevronLeft className="w-4 h-4" />
</button> </button>
<span className="text-sm font-semibold gradient-text">{MONTHS[month]} {year}</span> <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"> <button onClick={nextMonth} className="p-1 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white transition-colors">
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</button> </button>
</div> </div>
{/* Weekday labels */} {/* Grid */}
<div className="grid grid-cols-7 gap-0.5"> <div className="grid grid-cols-7 gap-0.5">
{WEEK_DAYS.map(d => ( {WEEK_DAYS.map(d => (
<div key={d} className="text-center text-[10px] font-semibold text-slate-600 py-1">{d}</div> <div key={d} className="text-center text-[9px] font-semibold text-slate-600 pb-1">{d}</div>
))} ))}
{cells.map((day, i) => { {cells.map((day, i) => {
if (!day) return <div key={`empty-${i}`} />; if (!day) return <div key={`e-${i}`} />;
const dateStr = `${year}-${String(month+1).padStart(2,"0")}-${String(day).padStart(2,"0")}`; const dateStr = `${year}-${String(month+1).padStart(2,"0")}-${String(day).padStart(2,"0")}`;
const isToday = dateStr === todayStr; const isToday = dateStr === todayStr;
const isSelected = dateStr === selectedDate; const isSelected = dateStr === selectedDate;
@@ -130,61 +120,67 @@ export function CalendarWidget() {
<button <button
key={dateStr} key={dateStr}
onClick={() => handleDayClick(day)} onClick={() => handleDayClick(day)}
className={`relative flex flex-col items-center justify-center aspect-square rounded-xl text-sm transition-all ${ className={`relative flex flex-col items-center justify-center rounded-lg text-xs transition-all py-1.5 ${
isSelected isSelected ? "text-white font-semibold" :
? "text-white font-semibold" isToday ? "text-violet-300 font-semibold" :
: isToday "text-slate-400 hover:text-white hover:bg-white/5"
? "text-violet-300 font-semibold"
: "text-slate-400 hover:text-white hover:bg-white/5"
}`} }`}
style={isSelected ? { background: "linear-gradient(135deg,#6366f1,#8b5cf6)" } : 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)" } : {}} isToday ? { background: "rgba(139,92,246,0.15)", boxShadow: "inset 0 0 0 1px rgba(139,92,246,0.4)" } : {}}
> >
{day} {day}
{hasEvents && ( {hasEvents && (
<span className={`absolute bottom-1 w-1 h-1 rounded-full ${isSelected ? "bg-white" : "bg-violet-400"}`} /> <span className={`absolute bottom-0.5 w-1 h-1 rounded-full ${isSelected ? "bg-white" : "bg-violet-400"}`} />
)} )}
</button> </button>
); );
})} })}
</div> </div>
</div>
{/* Day events panel */} {/* RIGHT: events for selected day */}
{selectedDate && ( <div className="p-4 flex flex-col gap-3 min-h-[300px]">
<div className="border-t border-white/5 pt-3 space-y-2"> {!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"> <div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-400"> <span className="text-sm font-semibold text-white">
{new Date(selectedDate + "T12:00:00").toLocaleDateString("ru-RU", { day: "numeric", month: "long" })} {new Date(selectedDate + "T12:00:00").toLocaleDateString("ru-RU", { day: "numeric", month: "long" })}
</span> </span>
<button <button
onClick={() => setShowCreate(s => !s)} onClick={() => setShowCreate(s => !s)}
className="flex items-center gap-1 text-xs text-violet-400 hover:text-violet-300 transition-colors" 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" /> Добавить <Plus className="w-3 h-3" /> Добавить
</button> </button>
</div> </div>
{/* Create form */}
{showCreate && ( {showCreate && (
<div className="bg-white/3 rounded-xl p-3 space-y-2 border border-white/5"> <div className="bg-white/3 rounded-xl p-3 space-y-2 border border-white/5 flex-shrink-0">
<input <input
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="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"
placeholder="Название события" placeholder="Название события"
value={newEvent.title} value={newEvent.title}
onChange={e => setNewEvent(n => ({ ...n, title: e.target.value }))} onChange={e => setNewEvent(n => ({ ...n, title: e.target.value }))}
onKeyDown={e => e.key === "Enter" && createEvent()} onKeyDown={e => e.key === "Enter" && createEvent()}
autoFocus
/> />
<div className="flex items-center gap-2">
<label className="flex items-center gap-1.5 text-xs text-slate-400 cursor-pointer"> <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} <input type="checkbox" className="accent-violet-500" checked={newEvent.allDay}
onChange={e => setNewEvent(n => ({ ...n, allDay: e.target.checked }))} /> onChange={e => setNewEvent(n => ({ ...n, allDay: e.target.checked }))} />
Весь день Весь день
</label> </label>
</div>
{!newEvent.allDay && ( {!newEvent.allDay && (
<div className="flex gap-2"> <div className="flex gap-2 items-center">
<input type="time" value={newEvent.startTime} onChange={e => setNewEvent(n => ({ ...n, startTime: e.target.value }))} <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" /> 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 self-center"></span> <span className="text-slate-600 text-xs"></span>
<input type="time" value={newEvent.endTime} onChange={e => setNewEvent(n => ({ ...n, endTime: e.target.value }))} <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" /> 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> </div>
@@ -194,7 +190,7 @@ export function CalendarWidget() {
<button onClick={createEvent} disabled={creating || !newEvent.title.trim()} <button onClick={createEvent} disabled={creating || !newEvent.title.trim()}
className="flex-1 text-white text-xs py-2 rounded-lg transition-colors disabled:opacity-50" className="flex-1 text-white text-xs py-2 rounded-lg transition-colors disabled:opacity-50"
style={{ background: "linear-gradient(135deg,#6366f1,#8b5cf6)" }}> style={{ background: "linear-gradient(135deg,#6366f1,#8b5cf6)" }}>
{creating ? "Создаю..." : "Создать"} {creating ? "Создаю..." : "Создать в Google Calendar"}
</button> </button>
<button onClick={() => setShowCreate(false)} className="p-2 rounded-lg hover:bg-white/5 text-slate-500 hover:text-white"> <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" /> <X className="w-3.5 h-3.5" />
@@ -203,38 +199,47 @@ export function CalendarWidget() {
</div> </div>
)} )}
{/* Events list */}
<div className="flex-1 overflow-y-auto space-y-2">
{loadingDay ? ( {loadingDay ? (
<div className="space-y-1.5 animate-pulse"> <div className="space-y-2 animate-pulse">
{[1,2].map(i => <div key={i} className="h-8 bg-white/5 rounded-lg" />)} {[1,2,3].map(i => <div key={i} className="h-14 bg-white/5 rounded-xl" />)}
</div> </div>
) : dayEvents.length === 0 ? ( ) : dayEvents.length === 0 ? (
<div className="text-xs text-slate-600 text-center py-2">Нет событий</div> <div className="flex flex-col items-center justify-center h-full text-slate-600 gap-2 py-8">
<span className="text-2xl">🗓</span>
<span className="text-xs">Нет событий</span>
<button onClick={() => setShowCreate(true)} className="text-xs text-violet-400 hover:text-violet-300 mt-1">
+ Создать событие
</button>
</div>
) : ( ) : (
<div className="space-y-1.5 max-h-40 overflow-y-auto"> dayEvents.map(ev => (
{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 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-1 self-stretch rounded-full flex-shrink-0 mt-0.5"
<div className="w-0.5 h-full min-h-[32px] rounded-full flex-shrink-0" style={{ background: "linear-gradient(to bottom,#8b5cf6,#6366f1)", minHeight: "32px" }} />
style={{ background: "linear-gradient(to bottom, #8b5cf6, #6366f1)" }} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-xs font-medium text-white truncate">{ev.title}</div> <div className="text-sm font-medium text-white truncate">{ev.title}</div>
<div className="text-[10px] text-slate-500 flex items-center gap-1 mt-0.5"> <div className="text-xs text-slate-500 flex items-center gap-1 mt-0.5">
<Clock className="w-2.5 h-2.5" /> <Clock className="w-3 h-3" />
{ev.allDay ? "Весь день" : formatTime(ev.start)} {ev.allDay ? "Весь день" : formatTime(ev.start)}
{ev.end && !ev.allDay && `${formatTime(ev.end)}`} {ev.end && !ev.allDay && `${formatTime(ev.end)}`}
</div> </div>
</div> </div>
{ev.htmlLink && ( {ev.htmlLink && (
<a href={ev.htmlLink} target="_blank" rel="noopener noreferrer" <a href={ev.htmlLink} target="_blank" rel="noopener noreferrer"
className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-white transition-all"> className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-violet-400 transition-all flex-shrink-0">
<ExternalLink className="w-3 h-3" /> <ExternalLink className="w-4 h-4" />
</a> </a>
)} )}
</div> </div>
))} ))
</div>
)} )}
</div> </div>
</>
)} )}
</div> </div>
</div>
</div>
); );
} }