diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 0950324..dff5868 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -1,16 +1,3 @@ -import { Sidebar } from "@/components/layout/Sidebar"; - -export const dynamic = "force-dynamic"; - export default function DashboardLayout({ children }: { children: React.ReactNode }) { - return ( -
- -
-
- {children} -
-
-
- ); + return <>{children}; } diff --git a/src/app/(dashboard)/page.tsx b/src/app/(dashboard)/page.tsx index e264730..5fe5d9a 100644 --- a/src/app/(dashboard)/page.tsx +++ b/src/app/(dashboard)/page.tsx @@ -9,21 +9,29 @@ import { ClaudeUsageWidget } from "@/components/widgets/ClaudeUsageWidget"; export default function DashboardPage() { return ( -
+
- -
+ + {/* Row 1: Weather — full width hero */} +
+ +
+ + {/* Row 2: Calendar (dominant) + Claude Usage */} +
-
+
-
+ + {/* Row 3: Git + Savings — equal split */} +
-
+
); } diff --git a/src/app/api/ping/route.ts b/src/app/api/ping/route.ts index 558a88f..a344de2 100644 --- a/src/app/api/ping/route.ts +++ b/src/app/api/ping/route.ts @@ -1,22 +1,13 @@ export const dynamic = "force-dynamic"; -import { NextResponse } from "next/server"; -import { NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; -export async function GET(request: NextRequest) { - const url = request.nextUrl.searchParams.get("url"); - if (!url) { - return NextResponse.json({ status: "offline" }, { status: 400 }); - } +export async function GET(req: NextRequest) { + const target = req.nextUrl.searchParams.get("target"); + if (!target) return NextResponse.json({ ok: false }, { status: 400 }); try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); - const res = await fetch(url, { - method: "HEAD", - signal: controller.signal, - }); - clearTimeout(timeout); - return NextResponse.json({ status: res.ok ? "online" : "offline" }); + const res = await fetch(target, { signal: AbortSignal.timeout(4000), cache: "no-store" }); + return NextResponse.json({ ok: res.ok, status: res.status }); } catch { - return NextResponse.json({ status: "offline" }); + return NextResponse.json({ ok: false }); } } diff --git a/src/app/globals.css b/src/app/globals.css index 621ebc6..a5ffb50 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -5,36 +5,44 @@ @tailwind utilities; body { - background-color: #080810; + background-color: #07070f; font-family: 'Inter', system-ui, sans-serif; color: #f1f5f9; + min-height: 100vh; } +/* Scrollbar */ ::-webkit-scrollbar { width: 4px; height: 4px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: rgba(99,102,241,0.3); border-radius: 2px; } ::-webkit-scrollbar-thumb:hover { background: rgba(99,102,241,0.5); } +/* Base card — glassmorphism */ .card { - background: rgba(255,255,255,0.03); + background: rgba(255,255,255,0.028); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.07); border-radius: 20px; - transition: all 0.2s ease; + transition: all 0.25s ease; } .card:hover { - background: rgba(255,255,255,0.05); - border-color: rgba(255,255,255,0.12); - transform: translateY(-1px); - box-shadow: 0 20px 60px rgba(0,0,0,0.5); + background: rgba(255,255,255,0.045); + border-color: rgba(255,255,255,0.11); + transform: translateY(-2px); + box-shadow: 0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04); } -.card-blue { border-top: 2px solid #3b82f6 !important; } -.card-violet { border-top: 2px solid #8b5cf6 !important; } -.card-emerald { border-top: 2px solid #10b981 !important; } -.card-amber { border-top: 2px solid #f59e0b !important; } -.card-cyan { border-top: 2px solid #06b6d4 !important; } -.card-rose { border-top: 2px solid #f43f5e !important; } +/* Color-coded top borders */ +.card-blue { border-top: 2px solid #3b82f6 !important; box-shadow: 0 -1px 20px rgba(59,130,246,0.08); } +.card-violet { border-top: 2px solid #8b5cf6 !important; box-shadow: 0 -1px 20px rgba(139,92,246,0.08); } +.card-emerald { border-top: 2px solid #10b981 !important; box-shadow: 0 -1px 20px rgba(16,185,129,0.08); } +.card-amber { border-top: 2px solid #f59e0b !important; box-shadow: 0 -1px 20px rgba(245,158,11,0.08); } +.card-cyan { border-top: 2px solid #06b6d4 !important; box-shadow: 0 -1px 20px rgba(6,182,212,0.08); } +.card-rose { border-top: 2px solid #f43f5e !important; box-shadow: 0 -1px 20px rgba(244,63,94,0.08); } +.card-orange { border-top: 2px solid #f97316 !important; box-shadow: 0 -1px 20px rgba(249,115,22,0.08); } +/* Gradient text */ .gradient-text { background: linear-gradient(135deg, #818cf8, #c084fc); -webkit-background-clip: text; @@ -42,14 +50,50 @@ body { background-clip: text; } +/* Pulse animation for status dots */ @keyframes ping-slow { 0%, 100% { opacity: 0.8; transform: scale(1); } - 50% { opacity: 0.2; transform: scale(1.8); } + 50% { opacity: 0.2; transform: scale(2); } } .ping-slow { animation: ping-slow 2s ease-in-out infinite; } -.glass-card { - background: rgba(255,255,255,0.03); - border: 1px solid rgba(255,255,255,0.07); - border-radius: 20px; +/* Count-up number animation */ +@keyframes countUp { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } } +.count-up { animation: countUp 0.6s ease-out forwards; } + +/* Ambient background orbs */ +.bg-orb { + position: fixed; + border-radius: 50%; + filter: blur(80px); + pointer-events: none; + z-index: 0; +} + +/* Shimmer skeleton */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} +.skeleton { + background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.04) 75%); + background-size: 200% 100%; + animation: shimmer 1.8s infinite; + border-radius: 8px; +} + +/* Glass card variant */ +.glass-card { + background: rgba(255,255,255,0.025); + backdrop-filter: blur(16px); + border: 1px solid rgba(255,255,255,0.07); + border-radius: 16px; +} + +/* Glow effects */ +.glow-blue { box-shadow: 0 0 20px rgba(59,130,246,0.15); } +.glow-violet { box-shadow: 0 0 20px rgba(139,92,246,0.15); } +.glow-emerald { box-shadow: 0 0 20px rgba(16,185,129,0.15); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fe0a35e..e3648ae 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,20 +5,24 @@ import { Providers } from "./providers"; export const metadata: Metadata = { title: "Digital Home", description: "Personal home dashboard", - icons: { - icon: "/favicon.svg", - }, + icons: { icon: "/favicon.svg" }, }; -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - {children} + + {/* Ambient background orbs */} +
+
+
+ +
+ {children} +
); diff --git a/src/components/widgets/DashboardHeader.tsx b/src/components/widgets/DashboardHeader.tsx index 9ab9972..4bf79a5 100644 --- a/src/components/widgets/DashboardHeader.tsx +++ b/src/components/widgets/DashboardHeader.tsx @@ -4,13 +4,37 @@ import { useEffect, useState } from "react"; const MONTHS = ["января","февраля","марта","апреля","мая","июня","июля","августа","сентября","октября","ноября","декабря"]; const DAYS = ["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"]; +const SERVICES = [ + { name: "Pulse", url: "/api/ping?target=https://api.digital-home.site/health" }, + { name: "Gitea", url: "/api/ping?target=https://git.digital-home.site" }, + { name: "Coolify", url: "/api/ping?target=https://coolify.digital-home.site" }, +]; + export function DashboardHeader() { const [now, setNow] = useState(new Date()); + const [statuses, setStatuses] = useState>({}); + useEffect(() => { const i = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(i); }, []); + useEffect(() => { + const checkServices = async () => { + const results = await Promise.allSettled( + SERVICES.map(s => fetch(s.url, { signal: AbortSignal.timeout(4000) }).then(r => ({ name: s.name, ok: r.ok }))) + ); + const newStatuses: Record = {}; + results.forEach((r, i) => { + newStatuses[SERVICES[i].name] = r.status === "fulfilled" && r.value.ok; + }); + setStatuses(newStatuses); + }; + checkServices(); + const interval = setInterval(checkServices, 60000); + return () => clearInterval(interval); + }, []); + const h = now.getHours(); const greeting = h < 6 ? "Доброй ночи" : h < 12 ? "Доброе утро" : h < 17 ? "Добрый день" : "Добрый вечер"; @@ -20,15 +44,33 @@ export function DashboardHeader() {

{greeting}, Daniil 👋

-

+

{DAYS[now.getDay()]}, {now.getDate()} {MONTHS[now.getMonth()]} {now.getFullYear()}

+ {/* Service status dots */} +
+ {SERVICES.map(s => { + const online = statuses[s.name]; + const unknown = statuses[s.name] === undefined; + return ( +
+
+
+ {online && ( +
+ )} +
+ {s.name} +
+ ); + })} +
-
+
{now.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" })}
-
+
{String(now.getSeconds()).padStart(2,"0")} сек