feat: add IntranetServices page and services.json for external tools

This commit is contained in:
Thibault Pouch
2026-02-27 10:32:44 +01:00
parent 4607e79b8b
commit 16b047e49f
4 changed files with 217 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
import { useState, useEffect } from 'react';
interface Service {
name: string;
url: string;
}
interface ServiceCategory {
category: string;
services: Service[];
}
function getFaviconUrl(url: string): string {
try {
const domain = new URL(url).hostname;
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
} catch {
return '';
}
}
function ServiceCard({ service }: { service: Service }) {
return (
<a
href={service.url}
target="_blank"
rel="noopener noreferrer"
style={{
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
padding: '1.25rem 1.5rem',
display: 'flex',
alignItems: 'center',
gap: '1rem',
textDecoration: 'none',
transition: 'border-color 0.2s, background 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'var(--color-yellow)';
e.currentTarget.style.background = 'rgba(37,99,235,0.05)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--color-border)';
e.currentTarget.style.background = 'var(--color-surface)';
}}
>
<img
src={getFaviconUrl(service.url)}
alt=""
width={24}
height={24}
style={{ flexShrink: 0, borderRadius: '4px' }}
onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none';
}}
/>
<div style={{ minWidth: 0 }}>
<div style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-text)', fontSize: '1rem' }}>
{service.name}
</div>
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-muted)',
fontSize: '0.7rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{service.url}
</div>
</div>
</a>
);
}
export default function IntranetServices() {
const [categories, setCategories] = useState<ServiceCategory[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetch('/services.json')
.then((res) => {
if (!res.ok) throw new Error('Failed to load services.json');
return res.json();
})
.then((data: ServiceCategory[]) => {
setCategories(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []);
const totalServices = categories.reduce((sum, cat) => sum + cat.services.length, 0);
return (
<div>
<div style={{ marginBottom: '2rem' }}>
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-muted)',
fontSize: '0.7rem',
letterSpacing: '0.15em',
marginBottom: '0.5rem',
}}
>
INTRANET / SERVICES
</div>
<h1 style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-text)', fontSize: '1.8rem' }}>
QUICK LINKS
</h1>
<p style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.78rem', marginTop: '0.4rem' }}>
External services and tools. Edit <code style={{ background: 'var(--color-bg-alt)', padding: '0.1rem 0.35rem', borderRadius: '3px', fontSize: '0.75rem' }}>public/services.json</code> to update this list.
</p>
</div>
{loading && (
<div style={{ fontFamily: 'var(--font-mono)', color: 'var(--color-text-muted)', fontSize: '0.8rem' }}>
Loading services...
</div>
)}
{error && (
<div
style={{
color: 'var(--color-red)',
fontFamily: 'var(--font-mono)',
fontSize: '0.78rem',
padding: '0.75rem 1rem',
background: 'rgba(220,38,38,0.06)',
border: '1px solid rgba(220,38,38,0.2)',
borderRadius: '4px',
}}
>
{error}
</div>
)}
{!loading && !error && categories.map((cat) => (
<div key={cat.category} style={{ marginBottom: '2rem' }}>
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-amber)',
fontSize: '0.7rem',
letterSpacing: '0.15em',
textTransform: 'uppercase',
marginBottom: '0.75rem',
}}
>
{cat.category}
<span style={{ color: 'var(--color-text-muted)', marginLeft: '0.5rem' }}>
({cat.services.length})
</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: '0.75rem' }}>
{cat.services.map((service) => (
<ServiceCard key={service.url} service={service} />
))}
</div>
</div>
))}
{!loading && !error && totalServices === 0 && (
<div
style={{
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
padding: '2rem',
textAlign: 'center',
color: 'var(--color-text-muted)',
fontFamily: 'var(--font-mono)',
fontSize: '0.8rem',
}}
>
No services configured. Add entries to public/services.json.
</div>
)}
</div>
);
}