Add self hosted icons, fix some colors and style status icon

This commit is contained in:
2025-02-06 10:54:12 -05:00
parent 29b0bc6806
commit 44461ceef8
5 changed files with 119 additions and 77 deletions

View File

@@ -1,38 +1,78 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
import { Button } from "@/components/ui/button"
import { Moon, Sun, Monitor, CheckCircle2, XCircle, AlertCircle, Clock } from "lucide-react"
import { Moon, Sun, Monitor} from "lucide-react"
import * as LucideIcons from "lucide-react"
import { useTheme } from "@/components/theme-provider"
import { useMetrics } from "./hooks/useMetrics"
import { ServiceStatus } from "./types/metrics"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import {
Server,
Box,
Play,
Activity,
Shield,
Database,
Download,
Home,
Tv,
Globe,
Gauge,
HardDrive,
Search,
Settings,
LayoutDashboard,
Package,
Smartphone
} from "lucide-react"
interface ServiceCard {
name: string
description: string
url: string
icon: React.ReactNode
icon?: React.ReactNode
iconName?: string
category: 'system' | 'media' | 'monitoring' | 'tools' | 'acot' | 'home'
monitorName?: string // Optional monitor name that matches Uptime Kuma
monitorName?: string
}
const getIconUrl = (name: string | undefined, isDark: boolean = false): string => {
if (!name) return ''
// Convert to selfh.st reference format
const ref = name.toLowerCase().replace(/[^a-z0-9]/g, '-')
// Use light version for dark mode if available
const suffix = isDark ? '-light' : ''
return `https://cdn.jsdelivr.net/gh/selfhst/icons/png/${ref}${suffix}.png`
}
function ServiceIcon({ service, isDark }: { service: ServiceCard, isDark: boolean }) {
if (service.icon) {
return <div className="h-6 w-6">{service.icon}</div>
}
if (service.iconName) {
// Handle Lucide icons
if (service.iconName.startsWith('lucide-')) {
const iconName = service.iconName.replace('lucide-', '')
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('')
// Cast to any to avoid TypeScript complexity with dynamic imports
const Icon = (LucideIcons as any)[iconName] || LucideIcons.Box
return <Icon className="h-6 w-6" />
}
// Handle selfh.st icons
return (
<img
src={getIconUrl(service.iconName, isDark)}
className="h-6 w-6 object-contain"
alt={`${service.name} icon`}
onError={(e) => {
if (isDark && e.currentTarget.src.includes('-light')) {
e.currentTarget.src = getIconUrl(service.iconName, false)
}
}}
/>
)
}
// Default category icons
const categoryIcons: Record<string, string> = {
system: 'Server',
media: 'Play',
monitoring: 'Activity',
tools: 'Settings',
acot: 'LayoutDashboard',
home: 'Home'
}
// Cast to any to avoid TypeScript complexity with dynamic imports
const Icon = (LucideIcons as any)[categoryIcons[service.category] || 'Box']
return <Icon className="h-6 w-6" />
}
const services: ServiceCard[] = [
@@ -40,28 +80,28 @@ const services: ServiceCard[] = [
name: "Portainer",
description: "Container Management",
url: "https://portainer.kent.pw",
icon: <Box className="h-6 w-6" />,
iconName: "portainer",
category: "system"
},
{
name: "Gitea",
description: "Git Server",
url: "https://gitea.kent.pw",
icon: <Server className="h-6 w-6" />,
iconName: "gitea",
category: "system"
},
{
name: "Cockpit",
description: "Server Management",
url: "https://cockpit.kent.pw",
icon: <Settings className="h-6 w-6" />,
iconName: "cockpit",
category: "system"
},
{
name: "DiskStation",
description: "Synology NAS",
url: "https://diskstation.kent.pw",
icon: <HardDrive className="h-6 w-6" />,
iconName: "synology",
category: "system",
monitorName: "Synology Diskstation"
},
@@ -69,42 +109,43 @@ const services: ServiceCard[] = [
name: "Plex",
description: "Media Server",
url: "https://plex.kent.pw",
icon: <Play className="h-6 w-6" />,
iconName: "plex",
category: "media"
},
{
name: "Sonarr",
description: "TV Show Management",
url: "https://sonarr.kent.pw",
icon: <Tv className="h-6 w-6" />,
iconName: "sonarr",
category: "media"
},
{
name: "Jackett",
description: "Torrent Indexer",
url: "https://jackett.kent.pw",
icon: <Search className="h-6 w-6" />,
iconName: "jackett",
category: "media"
},
{
name: "Deluge",
description: "Torrent Client",
url: "https://deluge.kent.pw",
icon: <Download className="h-6 w-6" />,
iconName: "deluge",
category: "media"
},
{
name: "Uptime",
description: "Service Monitoring",
url: "https://uptime.kent.pw",
icon: <Activity className="h-6 w-6" />,
category: "monitoring"
iconName: "uptime-kuma",
category: "monitoring",
monitorName: "Uptime Kuma"
},
{
name: "AdGuard",
description: "Network Ad Blocking",
url: "https://adguard.kent.pw",
icon: <Shield className="h-6 w-6" />,
iconName: "adguard-home",
category: "system",
monitorName: "Adguard Home"
},
@@ -112,7 +153,7 @@ const services: ServiceCard[] = [
name: "NocoDB",
description: "Database Platform",
url: "https://noco.kent.pw",
icon: <Database className="h-6 w-6" />,
iconName: "nocodb",
category: "tools",
monitorName: "NocoDB"
},
@@ -120,7 +161,7 @@ const services: ServiceCard[] = [
name: "IT Tools",
description: "Developer Utilities",
url: "https://ittools.kent.pw",
icon: <Settings className="h-6 w-6" />,
iconName: "it-tools",
category: "tools",
monitorName: "IT Tools"
},
@@ -128,14 +169,14 @@ const services: ServiceCard[] = [
name: "Firefox",
description: "Browser Instance",
url: "https://firefox.kent.pw",
icon: <Globe className="h-6 w-6" />,
iconName: "firefox",
category: "tools"
},
{
name: "Speedtest",
description: "Network Speed Monitor",
url: "https://speedtest.kent.pw",
icon: <Gauge className="h-6 w-6" />,
iconName: "speedtest-tracker",
category: "monitoring",
monitorName: "Speedtest Tracker"
},
@@ -143,7 +184,7 @@ const services: ServiceCard[] = [
name: "Drive",
description: "File Storage",
url: "https://drive.kent.pw",
icon: <HardDrive className="h-6 w-6" />,
iconName: "synology",
category: "tools",
monitorName: "Synology Drive"
},
@@ -151,28 +192,28 @@ const services: ServiceCard[] = [
name: "Dashboard",
description: "ACOT Dashboard",
url: "https://dashboard.kent.pw",
icon: <LayoutDashboard className="h-6 w-6" />,
iconName: "lucide-layout-dashboard",
category: "acot"
},
{
name: "Inventory",
description: "ACOT Inventory",
url: "https://inventory.kent.pw",
icon: <Package className="h-6 w-6" />,
iconName: "lucide-box",
category: "acot"
},
{
name: "Homebridge",
description: "HomeKit Bridge",
url: "https://homebridge.kent.pw",
icon: <Home className="h-6 w-6" />,
iconName: "homebridge",
category: "home"
},
{
name: "Scrypted",
description: "Smart Home Integration",
url: "https://scrypted.kent.pw",
icon: <Smartphone className="h-6 w-6" />,
iconName: "scrypted",
category: "home"
}
]
@@ -212,16 +253,16 @@ function StatusIndicator({ status, responseTime, certDaysRemaining }: {
responseTime: number,
certDaysRemaining: number
}) {
const getStatusIcon = () => {
const getStatusColor = () => {
switch (status) {
case 'up':
return <CheckCircle2 className="h-4 w-4 text-green-500" />
return 'bg-green-500'
case 'down':
return <XCircle className="h-4 w-4 text-red-500" />
return 'bg-red-500'
case 'pending':
return <Clock className="h-4 w-4 text-yellow-500" />
return 'bg-yellow-500'
case 'maintenance':
return <AlertCircle className="h-4 w-4 text-blue-500" />
return 'bg-blue-500'
}
}
@@ -238,8 +279,8 @@ function StatusIndicator({ status, responseTime, certDaysRemaining }: {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
{getStatusIcon()}
<TooltipTrigger className="bg-transparent border-none px-0 pr-2 py-0">
<div className={`w-3 h-3 rounded-full ${getStatusColor()}`} />
</TooltipTrigger>
<TooltipContent>
<p>{getStatusText()}</p>
@@ -254,6 +295,9 @@ function ServiceSection({ title, services, metrics }: {
services: ServiceCard[],
metrics: Record<string, any>
}) {
const { theme } = useTheme()
const isDark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
return (
<div className="space-y-4">
<div className="flex items-center">
@@ -268,42 +312,40 @@ function ServiceSection({ title, services, metrics }: {
certDaysRemaining: 0
}
// Get background color based on service
const bgColor = (() => {
switch (service.name) {
case "Portainer":
return "bg-blue-50 dark:bg-blue-950/30" // Portainer blue
return "bg-blue-50 dark:bg-blue-950/30"
case "Gitea":
return "bg-green-50 dark:bg-green-950/30" // Gitea green
return "bg-green-50 dark:bg-green-950/30"
case "Plex":
return "bg-orange-50 dark:bg-orange-950/30" // Plex orange
return "bg-orange-50 dark:bg-orange-950/30"
case "Sonarr":
return "bg-blue-50 dark:bg-blue-950/30" // Sonarr blue
return "bg-blue-50 dark:bg-blue-950/30"
case "AdGuard":
return "bg-emerald-50 dark:bg-emerald-950/30" // AdGuard green
return "bg-emerald-50 dark:bg-emerald-950/30"
case "Homebridge":
return "bg-purple-50 dark:bg-purple-950/30" // Homebridge purple
return "bg-purple-50 dark:bg-purple-950/30"
case "Scrypted":
return "bg-indigo-50 dark:bg-indigo-950/30" // Scrypted blue/purple
return "bg-indigo-50 dark:bg-indigo-950/30"
case "Dashboard":
case "Inventory":
return "bg-sky-50 dark:bg-sky-950/30" // ACOT blue
return "bg-sky-50 dark:bg-sky-950/30"
case "DiskStation":
return "bg-slate-50 dark:bg-slate-950/30" // Synology gray
return "bg-slate-50 dark:bg-slate-950/30"
case "Deluge":
return "bg-green-50 dark:bg-green-950/30" // Deluge green
return "bg-green-50 dark:bg-green-950/30"
case "Firefox":
return "bg-orange-50 dark:bg-orange-950/30" // Firefox orange
return "bg-orange-50 dark:bg-orange-950/30"
case "Uptime":
return "bg-emerald-50 dark:bg-emerald-950/30" // Uptime green (success color)
return "bg-emerald-50 dark:bg-emerald-950/30"
case "Speedtest":
return "bg-blue-50 dark:bg-blue-950/30" // Speedtest blue
return "bg-blue-50 dark:bg-blue-950/30"
default:
return "bg-gray-50 dark:bg-gray-950/30" // Default subtle background
return "bg-gray-50 dark:bg-gray-950/30"
}
})()
// Get icon color based on service
const iconColor = (() => {
switch (service.name) {
case "Portainer":
@@ -349,7 +391,9 @@ function ServiceSection({ title, services, metrics }: {
<Card className={bgColor}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center">
<div className={iconColor}>{service.icon}</div>
<div className={iconColor}>
<ServiceIcon service={service} isDark={isDark} />
</div>
<CardTitle className="ml-2">{service.name}</CardTitle>
</div>
<StatusIndicator
@@ -369,9 +413,8 @@ function ServiceSection({ title, services, metrics }: {
</div>
)
}
function App() {
const { metrics, loading, error } = useMetrics(import.meta.env.VITE_UPTIME_API_KEY)
const { metrics } = useMetrics()
const systemServices = services.filter(s => s.category === 'system')
const mediaServices = services.filter(s => s.category === 'media')
const monitoringServices = services.filter(s => s.category === 'monitoring')
@@ -380,7 +423,7 @@ function App() {
const homeServices = services.filter(s => s.category === 'home')
return (
<div className="min-h-screen bg-gray-100 dark:bg-background p-8 space-y-8">
<div className="min-h-screen p-8 space-y-8">
<div className="flex justify-between items-center">
<h1 className="text-4xl font-bold">Kent.pw Homepage</h1>
<ThemeToggle />
@@ -391,7 +434,7 @@ function App() {
<ServiceSection title="Tools" services={toolServices} metrics={metrics} />
<ServiceSection title="ACOT" services={acotServices} metrics={metrics} />
<ServiceSection title="Home" services={homeServices} metrics={metrics} />
</div>
</div>
)
}

View File

@@ -1,8 +1,7 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider, useTheme as useNextTheme } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
import type { ThemeProviderProps } from "next-themes"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { MetricsData, ServiceMetrics } from '../types/metrics'
import { MetricsData } from '../types/metrics'
const parseMetricsData = (data: string): MetricsData => {
const metrics: MetricsData = {}
@@ -92,7 +92,7 @@ const parseMetricsData = (data: string): MetricsData => {
return metrics
}
export function useMetrics(apiKey: string) {
export function useMetrics() {
const [metrics, setMetrics] = useState<MetricsData>({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)

View File

@@ -73,7 +73,7 @@ button:focus-visible {
@layer base {
:root {
--background: 0 0% 100%;
--background: 210 20% 96%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;

View File

@@ -20,8 +20,8 @@ export default defineConfig(({ mode }) => {
target: 'https://uptime.kent.pw',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
configure: (proxy, _options) => {
proxy.on('proxyReq', (proxyReq, _req, _res) => {
proxyReq.setHeader('Authorization', `Basic ${Buffer.from(':' + apiKey).toString('base64')}`)
})
}