Skip to content

Commit 1fc28de

Browse files
committed
feat(about): API /api/about con scan filesystem stats + pagina /about con StatCard, stack tech, moduli
2 parents 5e83481 + ba0921c commit 1fc28de

2 files changed

Lines changed: 174 additions & 0 deletions

File tree

web/app/about/page.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use client'
2+
3+
import Link from 'next/link'
4+
import { useEffect, useState, useCallback } from 'react'
5+
6+
type AboutData = {
7+
name: string; version: string; description: string; builtWith: string;
8+
stats: { sharedModules: number; webPages: number; apiRoutes: number; testFiles: number; cliCommands: number };
9+
modules: string[];
10+
stack: Record<string, string[]>;
11+
}
12+
13+
function StatCard({ label, value }: { label: string; value: number }) {
14+
return (
15+
<div className="flex flex-col items-center p-4 rounded-lg" style={{ background: 'var(--color-row)', border: '1px solid var(--color-border)' }}>
16+
<span className="text-2xl font-bold text-[var(--color-green)] font-mono">{value}</span>
17+
<span className="text-[10px] text-[var(--color-dim)] mt-1 text-center">{label}</span>
18+
</div>
19+
)
20+
}
21+
22+
function StackSection({ title, items }: { title: string; items: string[] }) {
23+
return (
24+
<div>
25+
<p className="text-[9px] font-bold tracking-widest text-[var(--color-dim)] uppercase mb-2">{title}</p>
26+
<div className="flex flex-wrap gap-1.5">
27+
{items.map(item => (
28+
<span key={item} className="px-2.5 py-1 rounded text-[10px] font-mono"
29+
style={{ background: 'var(--color-row)', color: 'var(--color-muted)', border: '1px solid var(--color-border)' }}>
30+
{item}
31+
</span>
32+
))}
33+
</div>
34+
</div>
35+
)
36+
}
37+
38+
export default function AboutPage() {
39+
const [data, setData] = useState<AboutData | null>(null)
40+
41+
const fetchAbout = useCallback(async () => {
42+
const res = await fetch('/api/about').catch(() => null)
43+
if (!res?.ok) return
44+
setData(await res.json())
45+
}, [])
46+
47+
useEffect(() => { fetchAbout() }, [fetchAbout])
48+
49+
if (!data) return <div className="flex items-center justify-center py-20"><p className="text-[var(--color-dim)] text-[12px]">Caricamento...</p></div>
50+
51+
return (
52+
<div style={{ animation: 'fade-in 0.35s ease both' }}>
53+
<div className="mb-8 pb-6 border-b border-[var(--color-border)]">
54+
<div className="flex items-center gap-2 mb-1">
55+
<Link href="/dashboard" className="text-[10px] text-[var(--color-dim)] hover:text-[var(--color-muted)] no-underline transition-colors">Dashboard</Link>
56+
<span className="text-[var(--color-border)]">/</span>
57+
<span className="text-[10px] text-[var(--color-muted)]">About</span>
58+
</div>
59+
<div className="mt-3">
60+
<h1 className="text-2xl font-bold tracking-tight text-[var(--color-white)]">{data.name}</h1>
61+
<p className="text-[var(--color-muted)] text-[11px] mt-1">{data.description}</p>
62+
<div className="flex items-center gap-3 mt-2">
63+
<span className="font-mono text-[11px] px-2 py-0.5 rounded" style={{ color: 'var(--color-green)', border: '1px solid rgba(0,232,122,0.3)' }}>v{data.version}</span>
64+
<span className="text-[10px] text-[var(--color-dim)]">{data.builtWith}</span>
65+
</div>
66+
</div>
67+
</div>
68+
69+
<div className="grid grid-cols-5 gap-3 mb-8">
70+
<StatCard label="Moduli Shared" value={data.stats.sharedModules} />
71+
<StatCard label="Pagine Web" value={data.stats.webPages} />
72+
<StatCard label="API Routes" value={data.stats.apiRoutes} />
73+
<StatCard label="File Test" value={data.stats.testFiles} />
74+
<StatCard label="Comandi CLI" value={data.stats.cliCommands} />
75+
</div>
76+
77+
<div className="mb-8">
78+
<h2 className="text-sm font-bold text-[var(--color-bright)] mb-4">Stack Tecnologico</h2>
79+
<div className="grid grid-cols-2 gap-4">
80+
{Object.entries(data.stack).map(([k, v]) => <StackSection key={k} title={k} items={v} />)}
81+
</div>
82+
</div>
83+
84+
<div className="mb-8">
85+
<h2 className="text-sm font-bold text-[var(--color-bright)] mb-3">Moduli</h2>
86+
<div className="flex flex-wrap gap-1.5">
87+
{data.modules.map(m => (
88+
<span key={m} className="px-2 py-1 rounded text-[10px] font-mono"
89+
style={{ background: 'rgba(0,232,122,0.06)', color: 'var(--color-green)', border: '1px solid rgba(0,232,122,0.2)' }}>
90+
{m}
91+
</span>
92+
))}
93+
</div>
94+
</div>
95+
96+
<div className="border-t border-[var(--color-border)] pt-4">
97+
<p className="text-[10px] text-[var(--color-dim)] text-center">
98+
Job Hunter Team — Piattaforma multi-agente autonoma
99+
</p>
100+
</div>
101+
</div>
102+
)
103+
}

web/app/api/about/route.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* API About — Info progetto, versione, moduli, pagine, stack tech
3+
*/
4+
import { NextResponse } from 'next/server';
5+
import fs from 'node:fs';
6+
import path from 'node:path';
7+
8+
const ROOT = path.resolve(process.cwd(), '..');
9+
10+
function countDirs(dir: string): string[] {
11+
try {
12+
return fs.readdirSync(dir, { withFileTypes: true })
13+
.filter(d => d.isDirectory())
14+
.map(d => d.name);
15+
} catch { return []; }
16+
}
17+
18+
function countFiles(dir: string, pattern: RegExp): number {
19+
let count = 0;
20+
try {
21+
const walk = (d: string) => {
22+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
23+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.next') continue;
24+
const full = path.join(d, entry.name);
25+
if (entry.isDirectory()) walk(full);
26+
else if (pattern.test(entry.name)) count++;
27+
}
28+
};
29+
walk(dir);
30+
} catch { /* ignore */ }
31+
return count;
32+
}
33+
34+
const STACK = {
35+
frontend: ['Next.js 15', 'React 19', 'TypeScript', 'Tailwind CSS'],
36+
backend: ['Node.js', 'TypeScript', 'Next.js API Routes'],
37+
shared: ['Custom modules (zero external deps)', 'node:test'],
38+
cli: ['Commander.js', 'Node.js'],
39+
infra: ['GitHub Actions CI', 'Docker-ready'],
40+
};
41+
42+
export async function GET() {
43+
const sharedModules = countDirs(path.join(ROOT, 'shared'));
44+
const webPages = countFiles(path.join(ROOT, 'web', 'app'), /^page\.tsx$/);
45+
const apiRoutes = countFiles(path.join(ROOT, 'web', 'app', 'api'), /^route\.ts$/);
46+
const testFiles = countFiles(ROOT, /\.test\.ts$/);
47+
const cliCommands = countDirs(path.join(ROOT, 'cli', 'src', 'commands')).length
48+
|| countFiles(path.join(ROOT, 'cli', 'src', 'commands'), /\.js$|\.ts$/);
49+
50+
let version = '0.0.0';
51+
try {
52+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'web', 'package.json'), 'utf-8'));
53+
version = pkg.version || version;
54+
} catch { /* default */ }
55+
56+
return NextResponse.json({
57+
name: 'Job Hunter Team',
58+
version,
59+
description: 'Piattaforma multi-agente per la ricerca lavoro automatizzata',
60+
stats: {
61+
sharedModules: sharedModules.length,
62+
webPages,
63+
apiRoutes,
64+
testFiles,
65+
cliCommands,
66+
},
67+
modules: sharedModules,
68+
stack: STACK,
69+
builtWith: 'Claude Code + Team multi-agente',
70+
});
71+
}

0 commit comments

Comments
 (0)