feat: add IntranetServices page and services.json for external tools
This commit is contained in:
27
nest-intra/public/services.json
Normal file
27
nest-intra/public/services.json
Normal file
@@ -0,0 +1,27 @@
|
||||
[
|
||||
{
|
||||
"category": "Development",
|
||||
"services": [
|
||||
{ "name": "GitHub", "url": "https://github.com/CrowMate" },
|
||||
{ "name": "Plane", "url": "https://plane.crowmate.fr/" },
|
||||
{ "name": "Jenkins", "url": "https://jenkins.crowmate.fr/" },
|
||||
{ "name": "Excalidraw", "url": "https://excalidraw.com/#room=dd1651be168b191eee57,LYP7GsVY6EKoyyp6vxWUGQ" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Infrastructure",
|
||||
"services": [
|
||||
{ "name": "Proxmox", "url": "https://proxmox.devgoblin.me/" },
|
||||
{ "name": "Cloudflare", "url": "https://cloudflare.com/" },
|
||||
{ "name": "Coolify", "url": "https://deploy.crowmate.fr/" },
|
||||
{ "name": "Grafana", "url": "https://grafana.crowmate.fr/" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Communication",
|
||||
"services": [
|
||||
{ "name": "Google Drive", "url": "https://drive.google.com/drive/folders/1hegR6sCQ5a5BGfUgZKZu8MwBY7o7SeQb" },
|
||||
{ "name": "Mail", "url": "https://zimbra1.mail.ovh.net/" }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -14,6 +14,7 @@ const IntranetFeed = lazy(() => import('./pages/intranet/IntranetFeed'))
|
||||
const IntranetEvents = lazy(() => import('./pages/intranet/IntranetEvents'));
|
||||
const IntranetUsers = lazy(() => import('./pages/intranet/IntranetUsers'));
|
||||
const IntranetModeration = lazy(() => import('./pages/intranet/IntranetModeration'));
|
||||
const IntranetServices = lazy(() => import('./pages/intranet/IntranetServices'));
|
||||
|
||||
// ── App ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -46,6 +47,7 @@ export default function App() {
|
||||
<Route path="events" element={<IntranetEvents />} />
|
||||
<Route path="users" element={<IntranetUsers />} />
|
||||
<Route path="moderation" element={<IntranetModeration />} />
|
||||
<Route path="services" element={<IntranetServices />} />
|
||||
</Route>
|
||||
|
||||
{/* Redirect root to intranet */}
|
||||
|
||||
@@ -9,6 +9,7 @@ const INTRANET_LINKS = [
|
||||
{ to: '/intranet/events', label: 'Events', icon: '[E]', end: false },
|
||||
{ to: '/intranet/users', label: 'Users', icon: '[U]', end: false },
|
||||
{ to: '/intranet/moderation', label: 'Moderation', icon: '[M]', end: false },
|
||||
{ to: '/intranet/services', label: 'Services', icon: '[S]', end: false },
|
||||
];
|
||||
|
||||
export function IntranetLayout() {
|
||||
|
||||
187
nest-intra/src/pages/intranet/IntranetServices.tsx
Normal file
187
nest-intra/src/pages/intranet/IntranetServices.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user