INSIGHT-MVP/packages/frontend/src/shell/AppLayout.tsx
Thomas Reitz 65c5c7b7dd feat: use website favicons instead of manual icon upload
External links now automatically show the favicon of the target website
using Google's favicon service. No manual icon upload needed — just
enter label and URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:19:34 +01:00

212 lines
7 KiB
TypeScript

import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { useAuth } from '../auth/AuthContext';
import { UserAvatar } from '../components/UserAvatar';
import api from '../api/client';
import styles from './AppLayout.module.css';
interface ExternalLink {
id: string;
label: string;
url: string;
sortOrder: number;
}
/** Favicon-URL aus einer Website-URL ableiten (Google Favicon Service) */
function getFaviconUrl(url: string): string | null {
try {
const domain = new URL(url).hostname;
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
} catch {
return null;
}
}
export function AppLayout() {
const { user, logout } = useAuth();
const navigate = useNavigate();
const { data: externalLinks } = useQuery<ExternalLink[]>({
queryKey: ['settings', 'external-links'],
queryFn: async () => {
const res = await api.get<ExternalLink[]>('/settings/external-links');
return res.data;
},
staleTime: 5 * 60 * 1000,
});
const handleLogout = async () => {
await logout();
navigate('/login');
};
return (
<div className={styles.layout}>
{/* Sidebar */}
<aside className={styles.sidebar}>
<div className={styles.brand}>
<h2>INSIGHT</h2>
</div>
<nav className={styles.nav}>
<NavLink
to="/"
end
className={({ isActive }) =>
`${styles.navLink} ${isActive ? styles.active : ''}`
}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2 6l6-4 6 4v7a1 1 0 01-1 1H3a1 1 0 01-1-1V6z" />
<path d="M6 14V8h4v6" />
</svg>
Dashboard
</NavLink>
{/* Externe Links */}
{externalLinks && externalLinks.length > 0 && (
<>
<div className={styles.navSection}>Anwendungen</div>
{externalLinks.map((link) => {
const favicon = getFaviconUrl(link.url);
return (
<a
key={link.id}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className={styles.navLink}
>
{favicon ? (
<img
src={favicon}
alt=""
style={{
width: 16,
height: 16,
objectFit: 'contain',
borderRadius: 2,
}}
/>
) : (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10 2h4v4" />
<path d="M6 10L14 2" />
<path d="M14 9v4a1 1 0 01-1 1H3a1 1 0 01-1-1V3a1 1 0 011-1h4" />
</svg>
)}
{link.label}
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
style={{ marginLeft: 'auto', opacity: 0.4 }}
>
<path d="M6 1h3v3" />
<path d="M4 6l5-5" />
</svg>
</a>
);
})}
</>
)}
</nav>
{/* Admin-Link (nur PLATFORM_ADMIN) */}
{user?.role === 'PLATFORM_ADMIN' && (
<div className={styles.adminSection}>
<NavLink
to="/admin/users"
className={({ isActive }) =>
`${styles.adminLink} ${isActive ? styles.adminLinkActive : ''}`
}
// Alle /admin/* Pfade sollen aktiv sein
style={({ isActive }) => {
const isAdminPath =
window.location.pathname.startsWith('/admin');
return isAdminPath && !isActive
? {
color: 'white',
background: 'rgba(96, 165, 250, 0.15)',
}
: {};
}}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z" />
<circle cx="12" cy="12" r="3" />
</svg>
Administration
</NavLink>
</div>
)}
<div className={styles.userInfo}>
<div
className={styles.userProfile}
onClick={() => navigate('/profile')}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') navigate('/profile');
}}
>
<div className={styles.userProfileInner}>
<UserAvatar
firstName={user?.firstName ?? ''}
lastName={user?.lastName ?? ''}
avatar={user?.avatar}
size={36}
/>
<div className={styles.userDetails}>
<div className={styles.userName}>
{user?.firstName} {user?.lastName}
</div>
<div className={styles.userEmail}>{user?.email}</div>
</div>
</div>
<div className={styles.profileHint}>Zum Profil &rarr;</div>
</div>
<button className={styles.logoutBtn} onClick={handleLogout}>
Abmelden
</button>
</div>
</aside>
{/* Main Content */}
<main className={styles.main}>
<Outlet />
</main>
</div>
);
}