mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 22:46:39 +02:00
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>
212 lines
7 KiB
TypeScript
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 →</div>
|
|
</div>
|
|
<button className={styles.logoutBtn} onClick={handleLogout}>
|
|
Abmelden
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Main Content */}
|
|
<main className={styles.main}>
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|