Initial React project

This commit is contained in:
Johan
2026-02-14 10:46:50 +01:00
commit 6c1f178ba9
5884 changed files with 1701440 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
import { useEffect, useMemo, useState } from 'react';
import { Sidebar } from '../../layout/components/Sidebar';
import { Topbar } from '../../layout/components/Topbar';
import { SimulationService } from '../../../mvvm/services/simulation.service';
interface JobSimulatorPageProps {
onLogout: () => Promise<void>;
onNavigate: (key: 'dashboard' | 'cv' | 'jobs' | 'beskeder' | 'ai-jobagent' | 'ai-agent' | 'simulator' | 'abonnement') => void;
}
interface InterviewItem {
id: string;
job_name: string;
company_name: string | null;
interview_date: string | null;
is_completed: boolean;
}
function asInterviews(value: unknown): InterviewItem[] {
if (!value || typeof value !== 'object') {
return [];
}
const root = value as { interviews?: unknown[] };
if (!Array.isArray(root.interviews)) {
return [];
}
return root.interviews
.map((item) => {
if (!item || typeof item !== 'object') {
return null;
}
const source = item as Record<string, unknown>;
const id = typeof source.id === 'string' ? source.id : '';
if (!id) {
return null;
}
return {
id,
job_name: typeof source.job_name === 'string' ? source.job_name : 'Interview',
company_name: typeof source.company_name === 'string' ? source.company_name : null,
interview_date: typeof source.interview_date === 'string' ? source.interview_date : null,
is_completed: Boolean(source.is_completed),
};
})
.filter((item): item is InterviewItem => Boolean(item))
.slice(0, 6);
}
function formatDate(value: string | null): string {
if (!value) {
return 'Ingen dato';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return 'Ingen dato';
}
return date.toLocaleDateString('da-DK', { day: '2-digit', month: 'short', year: 'numeric' });
}
export function JobSimulatorPage({ onLogout, onNavigate }: JobSimulatorPageProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem('arbejd.sidebar.collapsed') === '1');
const [items, setItems] = useState<InterviewItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const simulationService = useMemo(() => new SimulationService(), []);
useEffect(() => {
let active = true;
setIsLoading(true);
void simulationService
.listInterviews(10, 0)
.then((response) => {
if (!active) {
return;
}
setItems(asInterviews(response));
})
.catch(() => {
if (!active) {
return;
}
setItems([]);
})
.finally(() => {
if (active) {
setIsLoading(false);
}
});
return () => {
active = false;
};
}, [simulationService]);
return (
<section className="dashboard-layout">
<Sidebar
collapsed={sidebarCollapsed}
activeKey="simulator"
onToggle={() => setSidebarCollapsed((prev) => { const next = !prev; window.localStorage.setItem('arbejd.sidebar.collapsed', next ? '1' : '0'); return next; })}
onSelect={(key) => {
if (key === 'dashboard' || key === 'cv' || key === 'jobs' || key === 'beskeder' || key === 'ai-jobagent' || key === 'ai-agent' || key === 'simulator' || key === 'abonnement') {
onNavigate(key);
}
}}
/>
<main className="dashboard-main">
<Topbar title="Interview Simulator" userName="Anders Jensen" planLabel="Jobseeker Pro" onLogout={onLogout} />
<div className="dashboard-scroll">
<article className="glass-panel dash-card">
<div className="dash-header">
<h4>Seneste interviews</h4>
<button type="button" className="primary-btn jobs-apply-btn">Start nyt interview</button>
</div>
{isLoading ? <p>Indlæser interviews...</p> : null}
{!isLoading && items.length === 0 ? <p>Ingen interviews endnu.</p> : null}
<ul className="dashboard-feed-list">
{items.map((item) => (
<li key={item.id}>
<div className="dashboard-feed-item">
<strong>{item.job_name}</strong>
<span>{item.company_name || 'Ukendt virksomhed'} {formatDate(item.interview_date)} {item.is_completed ? 'Gennemført' : 'Ikke færdig'}</span>
</div>
</li>
))}
</ul>
</article>
</div>
</main>
</section>
);
}