From e3e641444c15d3a45a3dec4ae082bfc69a960826 Mon Sep 17 00:00:00 2001 From: Nezumi-2711 Date: Wed, 19 Aug 2026 21:59:29 +0700 Subject: [PATCH] fix: improve the ui --- .gitignore | 2 + src/App.tsx | 9 +- src/components/ThemeToggle.tsx | 48 ++++ .../status/components/SystemOverviewCard.tsx | 232 ++++++++++++++++++ src/index.css | 16 ++ src/pages/Dashboard.tsx | 37 +-- src/pages/SignIn.tsx | 215 +++++++--------- src/providers/ThemeProvider.tsx | 105 ++++++++ 8 files changed, 507 insertions(+), 157 deletions(-) create mode 100644 src/components/ThemeToggle.tsx create mode 100644 src/features/status/components/SystemOverviewCard.tsx create mode 100644 src/providers/ThemeProvider.tsx diff --git a/.gitignore b/.gitignore index a547bf3..50c8dda 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env diff --git a/src/App.tsx b/src/App.tsx index df93e0d..33bd710 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,13 @@ import { QueryProvider } from "@/providers/QueryProvider" +import { ThemeProvider } from "@/providers/ThemeProvider" import { AppRouter } from "@/router" export default function App() { return ( - - - + + + + + ) } diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..152861d --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -0,0 +1,48 @@ +import { Moon, Sun } from "lucide-react" +import { useTheme } from "@/providers/ThemeProvider" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +interface ThemeToggleProps { + className?: string + showLabel?: boolean + variant?: "outline" | "ghost" | "default" | "secondary" + size?: "default" | "sm" | "lg" | "icon" +} + +export function ThemeToggle({ + className, + showLabel = false, + variant = "outline", + size = "icon", +}: ThemeToggleProps) { + const { theme, resolvedTheme, toggleTheme } = useTheme() + const isDark = resolvedTheme === "dark" + + return ( + + ) +} diff --git a/src/features/status/components/SystemOverviewCard.tsx b/src/features/status/components/SystemOverviewCard.tsx new file mode 100644 index 0000000..de0a79b --- /dev/null +++ b/src/features/status/components/SystemOverviewCard.tsx @@ -0,0 +1,232 @@ +import { Activity, AlertTriangle, Server, HardDrive, Cpu, AlertCircle, User, CheckCircle2 } from "lucide-react" +import { Card, CardContent } from "@/components/ui/card" +import { Progress } from "@/components/ui/progress" +import { Skeleton } from "@/components/ui/skeleton" +import { Badge } from "@/components/ui/badge" +import { formatBytes } from "@/lib/format" +import type { StatusResponse } from "../api/status.types" + +interface SystemOverviewCardProps { + status?: StatusResponse + isLoading: boolean + error?: Error | null +} + +export function SystemOverviewCard({ status, isLoading, error }: SystemOverviewCardProps) { + if (isLoading) { + return ( + + +
+ {/* Gateway Skeleton */} +
+ + + +
+ {/* Storage Skeleton */} +
+ + + +
+ {/* Multipart Skeleton */} +
+ + + +
+
+
+
+ ) + } + + if (error || !status) { + return ( + +
+ +
+
+ +
+
+

Gateway Service Unavailable

+

+ {error?.message || "Failed to query live status from Cloudflare Worker"} +

+
+
+
+ + ) + } + + const isDegraded = status.gateway.status === "degraded" || !status.drive.connected + const drive = status.drive + const quota = drive.quota + const isUnlimited = quota?.limit == null + const percentUsed = quota?.percentUsed ?? 0 + const isMultipartEnabled = status.gateway.multipartEnabled ?? false + const etagStyle = status.gateway.etagStyle ?? "md5" + + return ( + + {/* Top accent bar with unified status gradient */} +
+ + +
+ + {/* Section 1: Gateway Engine */} +
+
+ + Gateway Engine + +
+ +
+
+ +
+
+ + {isDegraded ? "Degraded" : "Online & Healthy"} +
+
+ +
+
+ {isDegraded ? ( + + ) : ( + + )} + + {isDegraded + ? drive.error || "Drive connection degraded" + : `Edge routing · Region: ${status.gateway.region}`} + +
+ {status.gateway.docsEnabled && ( + + Docs + + )} +
+
+ + {/* Section 2: S3 Storage Backend (Google Drive) */} +
+
+ + S3 Storage Backend + +
+ +
+
+ + {!drive.connected ? ( +
+
Google Drive
+

+ + {drive.error || "Drive connection error"} +

+
+ ) : ( +
+
+
+ {formatBytes(quota?.usage)} +
+
+ {isUnlimited ? "Unlimited" : `/ ${formatBytes(quota?.limit)}`} +
+
+ + {!isUnlimited && ( +
+ 90 + ? "bg-red-500" + : percentUsed > 75 + ? "bg-amber-500" + : "bg-gradient-to-r from-blue-600 to-cyan-500" + } + /> +
+ {percentUsed}% used + {formatBytes(quota?.free)} free +
+
+ )} +
+ )} + +
+ + + {drive.account?.email || drive.account?.displayName || "Google Drive"} + +
+
+ + {/* Section 3: Multipart Uploads */} +
+
+ + Multipart Uploads + +
+ +
+
+ +
+
+ {isMultipartEnabled ? "Enabled" : "Disabled"} + + {etagStyle.toUpperCase()} ETag + +
+
+ +

+ + + {isMultipartEnabled + ? "Durable Objects resumable state" + : "ALLOW_MULTIPART disabled"} + +

+
+ +
+
+ + ) +} diff --git a/src/index.css b/src/index.css index fe702ae..1f6ba78 100644 --- a/src/index.css +++ b/src/index.css @@ -73,6 +73,16 @@ --radius-xl: calc(var(--radius) + 4px); } +html { + font-size: 16.5px; +} + +@media (min-width: 768px) { + html { + font-size: 17px; + } +} + body { margin: 0; min-height: 100vh; @@ -91,6 +101,12 @@ body { linear-gradient(to bottom, rgba(99, 102, 241, 0.05) 1px, transparent 1px); } +.dark .bg-grid-pattern { + background-image: + linear-gradient(to right, rgba(99, 102, 241, 0.08) 1px, transparent 1px), + linear-gradient(to bottom, rgba(99, 102, 241, 0.08) 1px, transparent 1px); +} + .bg-gradient-brand { background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%); } diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index ae390b8..299f52a 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -13,11 +13,10 @@ import { useAuth } from "@/features/auth/hooks/useAuth" import { useStatus } from "@/features/status/hooks/useStatus" import { useBucketStats } from "@/features/status/hooks/useBucketStats" import { statusKeys } from "@/features/status/api/status.keys" -import { GatewayStatusCard } from "@/features/status/components/GatewayStatusCard" -import { DriveQuotaCard } from "@/features/status/components/DriveQuotaCard" -import { MultipartStatusCard } from "@/features/status/components/MultipartStatusCard" +import { SystemOverviewCard } from "@/features/status/components/SystemOverviewCard" import { BucketStatsTable } from "@/features/status/components/BucketStatsTable" import { Button } from "@/components/ui/button" +import { ThemeToggle } from "@/components/ThemeToggle" import { formatRelativeTime } from "@/lib/format" export default function Dashboard() { @@ -62,13 +61,6 @@ export default function Dashboard() { return (
- {/* Background ambient colorful glow effects */} -
-
-
-
-
- {/* Navigation Header */}
@@ -106,6 +98,7 @@ export default function Dashboard() { Admin Mode
+
- {/* Status / Quick Overview cards */} -
- - - -
+ {/* Unified Status / Quick Overview */} + {/* Feature Capabilities & Specifications */}
diff --git a/src/pages/SignIn.tsx b/src/pages/SignIn.tsx index cf139f0..304d5d1 100644 --- a/src/pages/SignIn.tsx +++ b/src/pages/SignIn.tsx @@ -1,19 +1,12 @@ import React, { useState } from "react" import { useNavigate, Navigate } from "react-router" -import { Eye, EyeOff, Lock, Database, ArrowRight, Loader2, AlertCircle, Sparkles, Shield, Cloud } from "lucide-react" +import { Eye, EyeOff, Lock, Database, ArrowRight, Loader2, AlertCircle } from "lucide-react" import { useAuth } from "@/features/auth/hooks/useAuth" import { ApiError } from "@/lib/api-client" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" +import { ThemeToggle } from "@/components/ThemeToggle" function getErrorMessage(error: Error | null): string | null { if (!error) return null @@ -75,134 +68,104 @@ export default function SignIn() { } return ( -
- {/* Background ambient colorful glow effects */} -
-
-
-
+
+ {/* Theme Toggle in top-right */} +
+
-
- {/* Logo and Brand Header */} -
-
-
-
- -
-
- -
+
+ {/* Brand & Header */} +
+
+
- -
- - Cloudflare Worker & Drive Bridge -
- -

- S3 Drive Storage +

+ S3 Drive Storage

-

- High-performance management console for Google Drive S3 gateway +

+ Enter your management password to access the console

{/* Sign In Card */} - -
- - - - Sign In to Console - - - Enter your gateway access password to unlock management tools - - - -
- - {/* Error Alert */} - {error && ( -
- -
{error}
-
- )} - - {/* Password Input Field */} -
- -
-
- -
- { - setPassword(e.target.value) - if (signInError) resetSignInError() - }} - placeholder="Enter management password" - className="pl-10 pr-10 h-11 text-sm bg-background/80 border-border/80 rounded-xl focus-visible:ring-2 focus-visible:ring-blue-500/40 focus-visible:border-blue-500 transition-all" - disabled={isSigningIn} - autoComplete="current-password" - autoFocus - /> - -
-
-
- - - - -
- + +
{error}
+
+ )} - {/* Security badge footer */} -
- - Protected by S3 Drive Storage Gateway • SHA-256 Auth + {/* Password Input Field */} +
+ +
+
+ +
+ { + setPassword(e.target.value) + if (signInError) resetSignInError() + }} + placeholder="••••••••••••" + className="pl-9 pr-9 h-10 text-sm bg-background/60 border-border rounded-lg focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:border-primary transition-all" + disabled={isSigningIn} + autoComplete="current-password" + autoFocus + /> + +
+
+ + +
+ + {/* Minimal Footer */} +

+ Protected by S3 Drive Gateway Auth +

) diff --git a/src/providers/ThemeProvider.tsx b/src/providers/ThemeProvider.tsx new file mode 100644 index 0000000..c83f804 --- /dev/null +++ b/src/providers/ThemeProvider.tsx @@ -0,0 +1,105 @@ +import * as React from "react" + +export type Theme = "light" | "dark" | "system" + +interface ThemeProviderProps { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} + +interface ThemeContextType { + theme: Theme + resolvedTheme: "light" | "dark" + setTheme: (theme: Theme) => void + toggleTheme: () => void +} + +const ThemeContext = React.createContext(undefined) + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "s3-drive-ui-theme", +}: ThemeProviderProps) { + const [theme, setThemeState] = React.useState(() => { + try { + const stored = localStorage.getItem(storageKey) as Theme | null + if (stored === "light" || stored === "dark" || stored === "system") { + return stored + } + } catch { + // localStorage may be unavailable in some private browsing modes + } + return defaultTheme + }) + + const [resolvedTheme, setResolvedTheme] = React.useState<"light" | "dark">(() => { + if (theme === "system") { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" + } + return theme + }) + + React.useEffect(() => { + const root = document.documentElement + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + + const applyTheme = () => { + const currentResolved = + theme === "system" ? (mediaQuery.matches ? "dark" : "light") : theme + + setResolvedTheme(currentResolved) + root.classList.remove("light", "dark") + root.classList.add(currentResolved) + root.style.colorScheme = currentResolved + } + + applyTheme() + + const listener = () => { + if (theme === "system") { + applyTheme() + } + } + + mediaQuery.addEventListener("change", listener) + return () => mediaQuery.removeEventListener("change", listener) + }, [theme]) + + const setTheme = React.useCallback( + (newTheme: Theme) => { + try { + localStorage.setItem(storageKey, newTheme) + } catch { + // ignore storage write errors + } + setThemeState(newTheme) + }, + [storageKey] + ) + + const toggleTheme = React.useCallback(() => { + setTheme(resolvedTheme === "dark" ? "light" : "dark") + }, [resolvedTheme, setTheme]) + + const value = React.useMemo( + () => ({ + theme, + resolvedTheme, + setTheme, + toggleTheme, + }), + [theme, resolvedTheme, setTheme, toggleTheme] + ) + + return {children} +} + +export function useTheme(): ThemeContextType { + const context = React.useContext(ThemeContext) + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider") + } + return context +}