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,265 @@
import { useEffect, useMemo, useState } from 'react';
import type { ImprovementInterface } from '../../../mvvm/models/cv-suggestion.interface';
import { Sidebar } from '../../layout/components/Sidebar';
import { Topbar } from '../../layout/components/Topbar';
import { useAiAgentViewModel } from '../hooks/useAiAgentViewModel';
interface AiAgentPageProps {
onLogout: () => Promise<void>;
onNavigate: (key: 'dashboard' | 'cv' | 'jobs' | 'beskeder' | 'ai-jobagent' | 'ai-agent' | 'simulator' | 'abonnement') => void;
activeNavKey?: 'ai-jobagent' | 'ai-agent';
}
type ImprovementType = 'education' | 'language' | 'driversLicense' | 'qualification' | 'certificate';
function iconForImprovement(type: ImprovementType): string {
if (type === 'qualification') {
return '★';
}
if (type === 'driversLicense') {
return '↗';
}
if (type === 'certificate') {
return '✓';
}
if (type === 'education') {
return '▦';
}
return '◉';
}
function classForImprovement(type: ImprovementType): string {
if (type === 'qualification') {
return 'ai-notification-card qualification';
}
if (type === 'driversLicense') {
return 'ai-notification-card drivers';
}
if (type === 'certificate') {
return 'ai-notification-card certificate';
}
if (type === 'education') {
return 'ai-notification-card education';
}
return 'ai-notification-card language';
}
function withImprovementType(value: ImprovementInterface): value is ImprovementInterface & { improvementType: ImprovementType } {
return typeof (value as { improvementType?: string }).improvementType === 'string';
}
export function AiAgentPage({ onLogout, onNavigate, activeNavKey = 'ai-agent' }: AiAgentPageProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem('arbejd.sidebar.collapsed') === '1');
const [showAddFilter, setShowAddFilter] = useState(false);
const [searchText, setSearchText] = useState('');
const [selectedSuggestionId, setSelectedSuggestionId] = useState<number | null>(null);
const [showAllNotifications, setShowAllNotifications] = useState(false);
const [expandedNotificationKey, setExpandedNotificationKey] = useState<string | null>(null);
const {
paymentOverview,
jobAgentFilters,
cvSuggestions,
isLoading,
isMutating,
error,
load,
addEscoToFilter,
getEscoSuggestions,
getSuggestionText,
} = useAiAgentViewModel();
useEffect(() => {
void load();
}, [load]);
useEffect(() => {
if (!selectedSuggestionId && cvSuggestions.length > 0) {
setSelectedSuggestionId(cvSuggestions[0].escoId);
return;
}
if (selectedSuggestionId && !cvSuggestions.some((entry) => entry.escoId === selectedSuggestionId)) {
setSelectedSuggestionId(cvSuggestions[0]?.escoId ?? null);
}
}, [selectedSuggestionId, cvSuggestions]);
const suggestions = useMemo(() => getEscoSuggestions(searchText), [getEscoSuggestions, searchText]);
const selectedSuggestion = useMemo(
() => cvSuggestions.find((entry) => entry.escoId === selectedSuggestionId) ?? cvSuggestions[0] ?? null,
[cvSuggestions, selectedSuggestionId],
);
const improvements = useMemo(() => {
const list = selectedSuggestion?.improvements ?? [];
const typed = list.filter(withImprovementType);
return showAllNotifications ? typed : typed.slice(0, 8);
}, [selectedSuggestion, showAllNotifications]);
const hasMoreNotifications = (selectedSuggestion?.improvements?.length ?? 0) > 8;
const careerAgentEnabled = Boolean(paymentOverview?.careerAgent);
async function handleAddFilter(escoId: number) {
await addEscoToFilter(escoId);
setSearchText('');
}
return (
<section className="dashboard-layout">
<Sidebar
collapsed={sidebarCollapsed}
activeKey={activeNavKey}
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="AI Agent" userName="Anders Jensen" planLabel="Jobseeker Pro" onLogout={onLogout} />
<div className="dashboard-scroll">
<article className="glass-panel dash-card ai-agent-hero">
<h3>Din AI Agent</h3>
<p>
Din AI Agent analyserer dit CV og giver dig anbefalinger til, hvordan du kan forbedre
dit CV og styrke dine jobmuligheder.
</p>
</article>
{error ? <p className="status error">{error}</p> : null}
<article className="glass-panel dash-card ai-notification-section">
<div className="ai-notification-head">
<h4>Karriereagent</h4>
<strong className="ai-notification-kicker">DIN KARRIEREAGENT FORESLÅR</strong>
<p>Boost din profil ved hjælp af kunstig intelligens. Forslagene er udvalgt til din profil, ud fra 100.000+ jobopslag</p>
</div>
<div className="ai-inline-controls">
<button
type="button"
className="primary-btn ai-add-filter-btn"
onClick={() => setShowAddFilter((prev) => !prev)}
disabled={isMutating}
>
{showAddFilter ? 'Luk filter' : '+ Tilføj filter'}
</button>
</div>
{showAddFilter ? (
<div className="ai-filter-search-wrap">
<input
className="field-input ai-filter-search"
value={searchText}
onChange={(event) => setSearchText(event.target.value)}
placeholder="Søg stilling (ESCO)..."
/>
{searchText.trim().length > 0 && suggestions.length > 0 ? (
<div className="ai-filter-suggestions glass-panel">
{suggestions.map((esco) => (
<button
key={esco.id}
type="button"
className="ai-filter-suggestion-item"
onClick={() => void handleAddFilter(esco.id)}
disabled={isMutating}
>
{esco.preferedLabelDa}
</button>
))}
</div>
) : null}
</div>
) : null}
{isLoading ? <p>Indlæser AI filtre...</p> : null}
{!isLoading && jobAgentFilters.length === 0 ? (
<p className="helper-text">Ingen aktive AI filtre endnu. Tilføj en stilling for at starte.</p>
) : null}
{!careerAgentEnabled ? (
<p className="helper-text">Denne funktion kræver et aktivt abonnement med Karriereagent.</p>
) : null}
{careerAgentEnabled && cvSuggestions.length > 0 ? (
<div className="ai-notification-source-tabs">
{cvSuggestions.map((suggestion) => (
<button
key={suggestion.escoId}
type="button"
className={selectedSuggestion?.escoId === suggestion.escoId ? 'tab-btn active' : 'tab-btn'}
onClick={() => {
setSelectedSuggestionId(suggestion.escoId);
setShowAllNotifications(false);
}}
>
{suggestion.escoName}
</button>
))}
</div>
) : null}
{careerAgentEnabled && cvSuggestions.length === 0 && !isLoading ? (
<p className="helper-text">Systemet beregner stadig dine AI filtre. Kom tilbage om lidt.</p>
) : null}
<div className="ai-notification-grid">
{improvements.map((improvement) => {
const notificationKey = `${improvement.escoId}-${improvement.improvementType}-${improvement.name}`;
const expanded = expandedNotificationKey === notificationKey;
return (
<article
key={notificationKey}
className={expanded ? `${classForImprovement(improvement.improvementType)} expanded` : classForImprovement(improvement.improvementType)}
role="button"
tabIndex={0}
onClick={() =>
setExpandedNotificationKey((current) => (current === notificationKey ? null : notificationKey))
}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setExpandedNotificationKey((current) => (current === notificationKey ? null : notificationKey));
}
}}
>
<div className="ai-notification-icon" aria-hidden>
{iconForImprovement(improvement.improvementType)}
</div>
<div>
<strong>{improvement.name}</strong>
<p>{getSuggestionText(improvement.jobChanceIncrease)}</p>
<span>+{improvement.jobChanceIncrease}% relevans</span>
<div className={expanded ? 'ai-notification-extra-wrap expanded' : 'ai-notification-extra-wrap'}>
<div className="ai-notification-extra">
<p>{improvement.description || 'Ingen ekstra beskrivelse tilgængelig endnu.'}</p>
{typeof improvement.estimatedDurationMonths === 'number' ? (
<small>Estimeret varighed: {improvement.estimatedDurationMonths} måneder</small>
) : null}
</div>
</div>
</div>
</article>
);
})}
</div>
{hasMoreNotifications ? (
<button
type="button"
className="secondary-btn ai-show-more-btn"
onClick={() => setShowAllNotifications((prev) => !prev)}
>
{showAllNotifications ? 'Vis færre' : 'Vis flere'}
</button>
) : null}
</article>
</div>
</main>
</section>
);
}