fix: remove services from main, compact calendar + large events panel
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m11s
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m11s
This commit is contained in:
@@ -4,7 +4,6 @@ import { WeatherWidget } from "@/components/widgets/WeatherWidget";
|
||||
import { CalendarWidget } from "@/components/widgets/CalendarWidget";
|
||||
import { ClaudeUsageWidget } from "@/components/widgets/ClaudeUsageWidget";
|
||||
import { ClaudeApiWidget } from "@/components/widgets/ClaudeApiWidget";
|
||||
import { ServicesGrid } from "@/components/widgets/ServicesGrid";
|
||||
import { DashboardHeader } from "@/components/widgets/DashboardHeader";
|
||||
|
||||
export default function DashboardPage() {
|
||||
@@ -21,15 +20,10 @@ export default function DashboardPage() {
|
||||
<CalendarWidget />
|
||||
</div>
|
||||
<div className="space-y-5">
|
||||
<div className="grid grid-cols-2 gap-5 lg:grid-cols-1">
|
||||
<ClaudeUsageWidget />
|
||||
<ClaudeApiWidget />
|
||||
</div>
|
||||
<ClaudeUsageWidget />
|
||||
<ClaudeApiWidget />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Services */}
|
||||
<ServicesGrid />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ export function CalendarWidget() {
|
||||
const [newEvent, setNewEvent] = useState({ title: "", startTime: "09:00", endTime: "10:00", allDay: false });
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [createError, setCreateError] = useState("");
|
||||
const [calError, setCalError] = useState<string | null>(null);
|
||||
|
||||
const monthStr = `${year}-${String(month + 1).padStart(2, "0")}`;
|
||||
|
||||
@@ -35,8 +34,6 @@ export function CalendarWidget() {
|
||||
try {
|
||||
const res = await fetch(`/api/calendar?month=${monthStr}`);
|
||||
const data = await res.json();
|
||||
if (data.error && data.events?.length === 0) setCalError(data.error);
|
||||
else setCalError(null);
|
||||
setMonthEvents(data.events ?? []);
|
||||
} catch { setMonthEvents([]); }
|
||||
}, [monthStr]);
|
||||
@@ -68,7 +65,6 @@ export function CalendarWidget() {
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const startOffset = firstDay === 0 ? 6 : firstDay - 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 createEvent = async () => {
|
||||
@@ -86,155 +82,164 @@ export function CalendarWidget() {
|
||||
setNewEvent({ title: "", startTime: "09:00", endTime: "10:00", allDay: false });
|
||||
fetchDay(selectedDate);
|
||||
fetchMonth();
|
||||
} catch (e: unknown) { setCreateError(e instanceof Error ? e.message : "Ошибка создания"); }
|
||||
} catch (e: unknown) { setCreateError(e instanceof Error ? e.message : "Ошибка"); }
|
||||
finally { setCreating(false); }
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="card card-accent-violet p-5 flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<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>
|
||||
<div className="card card-violet flex flex-col gap-0" style={{ borderTop: "2px solid #8b5cf6" }}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-0 divide-x divide-white/5">
|
||||
|
||||
{/* Weekday labels */}
|
||||
<div className="grid grid-cols-7 gap-0.5">
|
||||
{WEEK_DAYS.map(d => (
|
||||
<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">
|
||||
{/* LEFT: compact calendar */}
|
||||
<div className="p-4 flex flex-col gap-3">
|
||||
{/* Month nav */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-slate-400">
|
||||
{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"
|
||||
>
|
||||
<Plus className="w-3 h-3" /> Добавить
|
||||
<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" />
|
||||
</button>
|
||||
<span className="text-sm font-semibold gradient-text">{MONTHS[month]} {year}</span>
|
||||
<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" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showCreate && (
|
||||
<div className="bg-white/3 rounded-xl p-3 space-y-2 border border-white/5">
|
||||
<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"
|
||||
placeholder="Название события"
|
||||
value={newEvent.title}
|
||||
onChange={e => setNewEvent(n => ({ ...n, title: e.target.value }))}
|
||||
onKeyDown={e => e.key === "Enter" && createEvent()}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-7 gap-0.5">
|
||||
{WEEK_DAYS.map(d => (
|
||||
<div key={d} className="text-center text-[9px] font-semibold text-slate-600 pb-1">{d}</div>
|
||||
))}
|
||||
{cells.map((day, i) => {
|
||||
if (!day) return <div key={`e-${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 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>
|
||||
{!newEvent.allDay && (
|
||||
<div className="flex gap-2">
|
||||
<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 self-center">—</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" />
|
||||
|
||||
{/* Create form */}
|
||||
{showCreate && (
|
||||
<div className="bg-white/3 rounded-xl p-3 space-y-2 border border-white/5 flex-shrink-0">
|
||||
<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"
|
||||
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>
|
||||
)}
|
||||
{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 ? (
|
||||
<div className="space-y-1.5 animate-pulse">
|
||||
{[1,2].map(i => <div key={i} className="h-8 bg-white/5 rounded-lg" />)}
|
||||
</div>
|
||||
) : dayEvents.length === 0 ? (
|
||||
<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>
|
||||
{/* Events list */}
|
||||
<div className="flex-1 overflow-y-auto space-y-2">
|
||||
{loadingDay ? (
|
||||
<div className="space-y-2 animate-pulse">
|
||||
{[1,2,3].map(i => <div key={i} className="h-14 bg-white/5 rounded-xl" />)}
|
||||
</div>
|
||||
{ev.htmlLink && (
|
||||
<a href={ev.htmlLink} target="_blank" rel="noopener noreferrer"
|
||||
className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-white transition-all">
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : dayEvents.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user