project-nomad/admin/inertia/pages/home.tsx
Chris Sherwood 24f10ea3d5 feat: Use friendly app names on Dashboard with open source attribution
Updates the Dashboard to use the same user-friendly names as the Easy Setup
Wizard, giving credit to the open source projects powering each capability:

- Kiwix → Information Library (Powered by Kiwix)
- Kolibri → Education Platform (Powered by Kolibri)
- Open WebUI → AI Assistant (Powered by Open WebUI + Ollama)
- FlatNotes → Notes (Powered by FlatNotes)
- CyberChef → Data Tools (Powered by CyberChef)

Also reorders Dashboard cards to prioritize Core Capabilities first, with
Maps promoted to Core Capability status, followed by Additional Tools,
then system items (Easy Setup, Install Apps, Docs, Settings).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:43:32 -08:00

146 lines
4.0 KiB
TypeScript

import {
IconBolt,
IconHelp,
IconMapRoute,
IconPlus,
IconSettings,
IconWifiOff,
} from '@tabler/icons-react'
import { Head } from '@inertiajs/react'
import BouncingLogo from '~/components/BouncingLogo'
import AppLayout from '~/layouts/AppLayout'
import { getServiceLink } from '~/lib/navigation'
import { ServiceSlim } from '../../types/services'
import DynamicIcon, { DynamicIconName } from '~/components/DynamicIcon'
// Maps is a Core Capability (display_order: 4)
const MAPS_ITEM = {
label: 'Maps',
to: '/maps',
target: '',
description: 'View offline maps',
icon: <IconMapRoute size={48} />,
installed: true,
displayOrder: 4,
poweredBy: null,
}
// System items shown after all apps
const SYSTEM_ITEMS = [
{
label: 'Easy Setup',
to: '/easy-setup',
target: '',
description:
'Not sure where to start? Use the setup wizard to quickly configure your N.O.M.A.D.!',
icon: <IconBolt size={48} />,
installed: true,
displayOrder: 50,
poweredBy: null,
},
{
label: 'Install Apps',
to: '/settings/apps',
target: '',
description: 'Not seeing your favorite app? Install it here!',
icon: <IconPlus size={48} />,
installed: true,
displayOrder: 51,
poweredBy: null,
},
{
label: 'Docs',
to: '/docs/home',
target: '',
description: 'Read Project N.O.M.A.D. manuals and guides',
icon: <IconHelp size={48} />,
installed: true,
displayOrder: 52,
poweredBy: null,
},
{
label: 'Settings',
to: '/settings/system',
target: '',
description: 'Configure your N.O.M.A.D. settings',
icon: <IconSettings size={48} />,
installed: true,
displayOrder: 53,
poweredBy: null,
},
]
interface DashboardItem {
label: string
to: string
target: string
description: string
icon: React.ReactNode
installed: boolean
displayOrder: number
poweredBy: string | null
}
export default function Home(props: {
system: {
services: ServiceSlim[]
}
}) {
const items: DashboardItem[] = []
// Add installed services (non-dependency services only)
props.system.services
.filter((service) => service.installed && service.ui_location)
.forEach((service) => {
items.push({
label: service.friendly_name || service.service_name,
to: service.ui_location ? getServiceLink(service.ui_location) : '#',
target: '_blank',
description:
service.description ||
`Access the ${service.friendly_name || service.service_name} application`,
icon: service.icon ? (
<DynamicIcon icon={service.icon as DynamicIconName} className="!size-12" />
) : (
<IconWifiOff size={48} />
),
installed: service.installed,
displayOrder: service.display_order ?? 100,
poweredBy: service.powered_by ?? null,
})
})
// Add Maps as a Core Capability
items.push(MAPS_ITEM)
// Add system items
items.push(...SYSTEM_ITEMS)
// Sort all items by display order
items.sort((a, b) => a.displayOrder - b.displayOrder)
return (
<AppLayout>
<Head title="Command Center" />
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{items.map((item) => (
<a key={item.label} href={item.to} target={item.target}>
<div
key={item.label}
className="rounded border-desert-green border-2 bg-desert-green hover:bg-transparent hover:text-black text-white transition-colors shadow-sm h-48 flex flex-col items-center justify-center cursor-pointer text-center px-4"
>
<div className="flex items-center justify-center mb-2">{item.icon}</div>
<h3 className="font-bold text-2xl">{item.label}</h3>
{item.poweredBy && (
<p className="text-sm opacity-80">Powered by {item.poweredBy}</p>
)}
<p className="xl:text-lg mt-2">{item.description}</p>
</div>
</a>
))}
<BouncingLogo />
</div>
</AppLayout>
)
}