Add Apps page with local icons, favicon, and React Router navigation
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Move app links to dedicated /apps page with grouped icon tiles - Download all icons locally to public/icons/ (no CDN dependency) - Add favicon and replace header logo-mark with custom icon - Fix icon names: synology, termix, docsight-light, dbgate (png) - Add nav tabs in header for Dashboard/Apps routing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+48
-36
@@ -1,3 +1,4 @@
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
||||
import { Header } from './components/Header'
|
||||
import { ProxmoxWidget } from './components/widgets/ProxmoxWidget'
|
||||
import { NasWidget } from './components/widgets/NasWidget'
|
||||
@@ -17,49 +18,60 @@ import { LokiWidget } from './components/widgets/LokiWidget'
|
||||
import { JellyfinWidget } from './components/widgets/JellyfinWidget'
|
||||
import { NavidromeWidget } from './components/widgets/NavidromeWidget'
|
||||
import { RommWidget } from './components/widgets/RommWidget'
|
||||
import { AppsPage } from './pages/AppsPage'
|
||||
|
||||
export default function App() {
|
||||
function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="blob blob-1" />
|
||||
<div className="blob blob-2" />
|
||||
<div className="shell">
|
||||
<Header />
|
||||
<div className="section-label">Infrastructure</div>
|
||||
<div className="infra-grid">
|
||||
<ProxmoxWidget />
|
||||
<NasWidget />
|
||||
<AdGuardWidget />
|
||||
<HeadscaleWidget />
|
||||
<FritzboxWidget />
|
||||
</div>
|
||||
|
||||
<div className="section-label">Infrastructure</div>
|
||||
<div className="infra-grid">
|
||||
<ProxmoxWidget />
|
||||
<NasWidget />
|
||||
<AdGuardWidget />
|
||||
<HeadscaleWidget />
|
||||
<FritzboxWidget />
|
||||
</div>
|
||||
<div className="section-label">Media</div>
|
||||
<div className="infra-grid">
|
||||
<JellyfinWidget />
|
||||
<NavidromeWidget />
|
||||
<RommWidget />
|
||||
<ArrCalendarWidget />
|
||||
<ArrStatsWidget />
|
||||
<QbittorrentWidget />
|
||||
</div>
|
||||
|
||||
<div className="section-label">Media</div>
|
||||
<div className="infra-grid">
|
||||
<JellyfinWidget />
|
||||
<NavidromeWidget />
|
||||
<RommWidget />
|
||||
<ArrCalendarWidget />
|
||||
<ArrStatsWidget />
|
||||
<QbittorrentWidget />
|
||||
</div>
|
||||
<div className="section-label">Monitoring</div>
|
||||
<div className="infra-grid">
|
||||
<KumaWidget />
|
||||
<CrowdSecWidget />
|
||||
<GrafanaWidget />
|
||||
<PrometheusWidget />
|
||||
<LokiWidget />
|
||||
</div>
|
||||
|
||||
<div className="section-label">Monitoring</div>
|
||||
<div className="infra-grid">
|
||||
<KumaWidget />
|
||||
<CrowdSecWidget />
|
||||
<GrafanaWidget />
|
||||
<PrometheusWidget />
|
||||
<LokiWidget />
|
||||
</div>
|
||||
|
||||
<div className="section-label">Access</div>
|
||||
<div className="infra-grid">
|
||||
<AuthentikWidget />
|
||||
<VaultwardenWidget />
|
||||
</div>
|
||||
<div className="section-label">Access</div>
|
||||
<div className="infra-grid">
|
||||
<AuthentikWidget />
|
||||
<VaultwardenWidget />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className="blob blob-1" />
|
||||
<div className="blob blob-2" />
|
||||
<div className="shell">
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react'
|
||||
import { appGroups } from '../config/apps'
|
||||
|
||||
function AppTile({ name, url, icon }: { name: string; url: string; icon: string }) {
|
||||
const [imgError, setImgError] = useState(false)
|
||||
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer" className="app-tile">
|
||||
{!imgError ? (
|
||||
<img
|
||||
src={icon}
|
||||
alt={name}
|
||||
className="app-icon"
|
||||
onError={() => setImgError(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="app-icon-fallback" style={{ display: 'flex' }}>
|
||||
{name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="app-name">{name}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export function AppsSection() {
|
||||
return (
|
||||
<>
|
||||
{appGroups.map(group => (
|
||||
<div key={group.name} className="app-group">
|
||||
<div className="app-group-label">{group.name}</div>
|
||||
<div className="app-grid">
|
||||
{group.apps.map(app => (
|
||||
<AppTile key={app.name} {...app} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
|
||||
export function Header() {
|
||||
const [time, setTime] = useState('')
|
||||
@@ -18,13 +19,17 @@ export function Header() {
|
||||
return (
|
||||
<header>
|
||||
<div className="logo">
|
||||
<div className="logo-mark">S</div>
|
||||
<img src="/icons/favicon.png" alt="logo" className="logo-mark" />
|
||||
<div>
|
||||
<div className="logo-text">syco.me</div>
|
||||
<div className="logo-sub">homelab dashboard</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="topbar-right">
|
||||
<nav className="nav-tabs">
|
||||
<NavLink to="/" end className={({ isActive }) => `nav-tab${isActive ? ' nav-tab-active' : ''}`}>Dashboard</NavLink>
|
||||
<NavLink to="/apps" className={({ isActive }) => `nav-tab${isActive ? ' nav-tab-active' : ''}`}>Apps</NavLink>
|
||||
</nav>
|
||||
<div className="clock-widget">
|
||||
<div className="clock-time">{time}</div>
|
||||
<div className="clock-date">{date}</div>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
const ICONS = '/icons'
|
||||
|
||||
export interface AppLink {
|
||||
name: string
|
||||
url: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export interface AppGroup {
|
||||
name: string
|
||||
apps: AppLink[]
|
||||
}
|
||||
|
||||
export const appGroups: AppGroup[] = [
|
||||
{
|
||||
name: 'Infrastructure',
|
||||
apps: [
|
||||
{ name: 'Proxmox', url: 'https://proxmox.syco.me', icon: `${ICONS}/proxmox.svg` },
|
||||
{ name: 'Synology NAS', url: 'https://nas.syco.me', icon: `${ICONS}/synology.svg` },
|
||||
{ name: 'AdGuard Home', url: 'https://adguard.syco.me', icon: `${ICONS}/adguard-home.svg` },
|
||||
{ name: 'Headscale', url: 'https://headscale.syco.me/admin', icon: `${ICONS}/headscale.svg` },
|
||||
{ name: 'FritzBox', url: 'http://fritz.box', icon: `${ICONS}/Fritz!_Logo.svg.png` },
|
||||
{ name: 'Portainer', url: 'https://portainer.syco.me', icon: `${ICONS}/portainer.svg` },
|
||||
{ name: 'CrowdSec', url: 'https://crowdsec.syco.me', icon: `${ICONS}/crowdsec.svg` },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Auth & Access',
|
||||
apps: [
|
||||
{ name: 'Authentik', url: 'https://auth.syco.me', icon: `${ICONS}/authentik.svg` },
|
||||
{ name: 'Vaultwarden', url: 'https://vaultwarden.syco.me', icon: `${ICONS}/vaultwarden.svg` },
|
||||
{ name: 'Termix', url: 'https://termix.syco.me', icon: `${ICONS}/termix.svg` },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Media',
|
||||
apps: [
|
||||
{ name: 'Jellyfin', url: 'https://jellyfin.syco.me', icon: `${ICONS}/jellyfin.svg` },
|
||||
{ name: 'Navidrome', url: 'https://music.syco.me', icon: `${ICONS}/navidrome.svg` },
|
||||
{ name: 'Overseerr', url: 'https://overseerr.syco.me', icon: `${ICONS}/overseerr.svg` },
|
||||
{ name: 'Radarr', url: 'https://radarr.syco.me', icon: `${ICONS}/radarr.svg` },
|
||||
{ name: 'Sonarr', url: 'https://sonarr.syco.me', icon: `${ICONS}/sonarr.svg` },
|
||||
{ name: 'Lidarr', url: 'https://lidarr.syco.me', icon: `${ICONS}/lidarr.svg` },
|
||||
{ name: 'Prowlarr', url: 'https://prowlarr.syco.me', icon: `${ICONS}/prowlarr.svg` },
|
||||
{ name: 'Bazarr', url: 'https://bazarr.syco.me', icon: `${ICONS}/bazarr.svg` },
|
||||
{ name: 'qBittorrent', url: 'https://bittorrent.syco.me', icon: `${ICONS}/qbittorrent.svg` },
|
||||
{ name: 'RomM', url: 'https://romm.syco.me', icon: `${ICONS}/romm.svg` },
|
||||
{ name: 'Shoko', url: 'http://shoko.internal', icon: `${ICONS}/shoko-server.svg` },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Monitoring',
|
||||
apps: [
|
||||
{ name: 'Grafana', url: 'https://grafana.syco.me', icon: `${ICONS}/grafana.svg` },
|
||||
{ name: 'Uptime Kuma', url: 'https://uptime.syco.me', icon: `${ICONS}/uptime-kuma.svg` },
|
||||
{ name: 'Update Dashboard', url: 'https://update-dashboard.syco.me', icon: `${ICONS}/linux-update-dashboard.svg` },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Dev & Tools',
|
||||
apps: [
|
||||
{ name: 'Gitea', url: 'https://git.syco.me', icon: `${ICONS}/gitea.svg` },
|
||||
{ name: 'Woodpecker', url: 'https://woodpecker.syco.me', icon: `${ICONS}/woodpecker-ci.svg` },
|
||||
{ name: 'DbGate', url: 'https://dbgate.syco.me', icon: `${ICONS}/dbgate.png` },
|
||||
{ name: 'IT-Tools', url: 'https://it-tools.syco.me', icon: `${ICONS}/it-tools.svg` },
|
||||
{ name: 'Docsight', url: 'http://docsight.internal', icon: `${ICONS}/docsight-light.svg` },
|
||||
{ name: 'Firefly III', url: 'https://firefly.syco.me', icon: `${ICONS}/firefly-iii.svg` },
|
||||
{ name: 'Copyparty', url: 'https://copyparty.syco.me', icon: `${ICONS}/copyparty.svg` },
|
||||
{ name: 'Yu-Gi-Oh', url: 'https://yugioh.syco.me', icon: `${ICONS}/yugiohCard.jpg` },
|
||||
],
|
||||
},
|
||||
]
|
||||
+89
-3
@@ -63,10 +63,8 @@ header {
|
||||
.logo { display: flex; align-items: center; gap: 10px; }
|
||||
.logo-mark {
|
||||
width: 32px; height: 32px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||||
border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 14px; font-weight: 800; letter-spacing: -1px; color: #fff;
|
||||
object-fit: contain;
|
||||
}
|
||||
.logo-text { font-size: 18px; font-weight: 700; letter-spacing: -0.5px; }
|
||||
.logo-sub { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--muted); }
|
||||
@@ -226,6 +224,94 @@ header {
|
||||
.topbar-right { gap: 12px; }
|
||||
}
|
||||
|
||||
/* ── Nav tabs ── */
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
}
|
||||
.nav-tab {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 5px 14px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
color: var(--text2);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.nav-tab:hover { color: var(--text); }
|
||||
.nav-tab-active { background: var(--surface2); color: var(--text); }
|
||||
|
||||
/* ── Apps Section ── */
|
||||
.app-group { margin-bottom: 28px; }
|
||||
|
||||
.app-group-label {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px; font-weight: 500;
|
||||
letter-spacing: 2px; text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.app-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 12px;
|
||||
width: 90px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
text-decoration: none;
|
||||
color: var(--text2);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
transition: border-color 0.2s, transform 0.2s, color 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.app-tile:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.app-icon-fallback {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: inherit;
|
||||
line-height: 1.3;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ── */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppsSection } from '../components/AppsSection'
|
||||
|
||||
export function AppsPage() {
|
||||
return (
|
||||
<div className="shell">
|
||||
<div className="section-label">Apps</div>
|
||||
<AppsSection />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user