Initial React project
This commit is contained in:
339
src/App.tsx
339
src/App.tsx
@@ -1,245 +1,128 @@
|
||||
import './App.css';
|
||||
import { useEffect } from 'react';
|
||||
import { ForgotPasswordPage } from './presentation/auth/pages/ForgotPasswordPage';
|
||||
import { LoginPage } from './presentation/auth/pages/LoginPage';
|
||||
import { RegisterPage } from './presentation/auth/pages/RegisterPage';
|
||||
import { DashboardPage } from './presentation/dashboard/pages/DashboardPage';
|
||||
import { CvPage } from './presentation/cv/pages/CvPage';
|
||||
import { JobsPage } from './presentation/jobs/pages/JobsPage';
|
||||
import { JobDetailPage } from './presentation/jobs/pages/JobDetailPage';
|
||||
import { BeskederPage } from './presentation/messages/pages/BeskederPage';
|
||||
import { AiAgentPage } from './presentation/ai-agent/pages/AiAgentPage';
|
||||
import { AiJobAgentPage } from './presentation/ai-jobagent/pages/AiJobAgentPage';
|
||||
import { JobSimulatorPage } from './presentation/simulator/pages/JobSimulatorPage';
|
||||
import { SubscriptionPage } from './presentation/subscription/pages/SubscriptionPage';
|
||||
import { ThemeToggle } from './presentation/layout/components/ThemeToggle';
|
||||
import { useAuthViewModel } from './presentation/auth/hooks/useAuthViewModel';
|
||||
import { useBrowserRoute } from './presentation/router/useBrowserRoute';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { localStorageService } from './mvvm/services/local-storage.service';
|
||||
import { AuthPage } from './presentation/auth/pages/AuthPage';
|
||||
import { AiAgentPage } from './presentation/ai-agent/pages/AiAgentPage';
|
||||
import { CvPage } from './presentation/cv/pages/CvPage';
|
||||
import type { DashboardNavKey } from './presentation/dashboard/components/DashboardSidebar';
|
||||
import { DashboardPage } from './presentation/dashboard/pages/DashboardPage';
|
||||
import { JobDetailPage } from './presentation/jobs/pages/JobDetailPage';
|
||||
import { JobsPage } from './presentation/jobs/pages/JobsPage';
|
||||
import { MessagesPage } from './presentation/messages/pages/MessagesPage';
|
||||
|
||||
type AppPage = DashboardNavKey | 'job-detail';
|
||||
|
||||
interface JobDetailSelection {
|
||||
id: string;
|
||||
fromJobnet: boolean;
|
||||
returnPage: DashboardNavKey;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { path, navigate } = useBrowserRoute();
|
||||
const { isLoading, result, login, register, forgotPassword } = useAuthViewModel();
|
||||
const isJobDetail = path.startsWith('/jobs/');
|
||||
const jobDetailMatch = isJobDetail ? path.match(/^\/jobs\/([^/]+)\/(jobnet|arbejd)$/) : null;
|
||||
const jobIdFromPath = jobDetailMatch ? decodeURIComponent(jobDetailMatch[1]) : '';
|
||||
const fromJobnetFromPath = jobDetailMatch ? jobDetailMatch[2] === 'jobnet' : false;
|
||||
const mode =
|
||||
path === '/register'
|
||||
? 'register'
|
||||
: path === '/forgot-password'
|
||||
? 'forgot'
|
||||
: path === '/dashboard'
|
||||
? 'dashboard'
|
||||
: path === '/cv'
|
||||
? 'cv'
|
||||
: path === '/jobs'
|
||||
? 'jobs'
|
||||
: path === '/beskeder'
|
||||
? 'beskeder'
|
||||
: path === '/ai-jobagent'
|
||||
? 'ai-jobagent'
|
||||
: path === '/ai-agent'
|
||||
? 'ai-agent'
|
||||
: path === '/simulator'
|
||||
? 'simulator'
|
||||
: path === '/abonnement'
|
||||
? 'abonnement'
|
||||
: isJobDetail
|
||||
? 'job-detail'
|
||||
: 'login';
|
||||
const initialAuthenticated = useMemo(() => Boolean(window.localStorage.getItem('token')), []);
|
||||
const initialTheme = useMemo<'light' | 'dark'>(() => {
|
||||
const stored = window.localStorage.getItem('theme');
|
||||
return stored === 'dark' ? 'dark' : 'light';
|
||||
}, []);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(initialAuthenticated);
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>(initialTheme);
|
||||
const [activePage, setActivePage] = useState<AppPage>('dashboard');
|
||||
const [jobDetailSelection, setJobDetailSelection] = useState<JobDetailSelection | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = window.localStorage.getItem('token');
|
||||
const isAuthPage = path === '/login' || path === '/register' || path === '/forgot-password' || path === '/';
|
||||
|
||||
if ((path === '/dashboard' || path === '/cv' || path === '/jobs' || path === '/beskeder' || path === '/ai-jobagent' || path === '/ai-agent' || path === '/simulator' || path === '/abonnement' || isJobDetail) && !token) {
|
||||
navigate('/login', true);
|
||||
return;
|
||||
function handleNavigate(target: DashboardNavKey) {
|
||||
if (
|
||||
target === 'dashboard'
|
||||
|| target === 'jobs'
|
||||
|| target === 'cv'
|
||||
|| target === 'messages'
|
||||
|| target === 'agents'
|
||||
|| target === 'ai-agent'
|
||||
) {
|
||||
setActivePage(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAuthPage && token) {
|
||||
navigate('/dashboard', true);
|
||||
}
|
||||
}, [path, navigate, isJobDetail]);
|
||||
function handleOpenJobDetail(id: string, fromJobnet: boolean, returnPage: DashboardNavKey = 'jobs') {
|
||||
setJobDetailSelection({ id, fromJobnet, returnPage });
|
||||
setActivePage('job-detail');
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
function handleBackFromJobDetail() {
|
||||
setActivePage(jobDetailSelection?.returnPage ?? 'jobs');
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await localStorageService.clearCredentials();
|
||||
navigate('/login', true);
|
||||
setActivePage('dashboard');
|
||||
setJobDetailSelection(null);
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
|
||||
function navigateFromSidebar(key: 'dashboard' | 'cv' | 'jobs' | 'beskeder' | 'ai-jobagent' | 'ai-agent' | 'simulator' | 'abonnement') {
|
||||
if (key === 'dashboard') {
|
||||
navigate('/dashboard');
|
||||
return;
|
||||
}
|
||||
if (key === 'cv') {
|
||||
navigate('/cv');
|
||||
return;
|
||||
}
|
||||
if (key === 'jobs') {
|
||||
navigate('/jobs');
|
||||
return;
|
||||
}
|
||||
if (key === 'ai-jobagent') {
|
||||
navigate('/ai-jobagent');
|
||||
return;
|
||||
}
|
||||
if (key === 'ai-agent') {
|
||||
navigate('/ai-agent');
|
||||
return;
|
||||
}
|
||||
if (key === 'simulator') {
|
||||
navigate('/simulator');
|
||||
return;
|
||||
}
|
||||
if (key === 'abonnement') {
|
||||
navigate('/abonnement');
|
||||
return;
|
||||
}
|
||||
navigate('/beskeder');
|
||||
function handleToggleTheme() {
|
||||
setTheme((current) => {
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
window.localStorage.setItem('theme', next);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
const isAppLayoutMode = mode === 'dashboard' || mode === 'cv' || mode === 'jobs' || mode === 'job-detail' || mode === 'beskeder' || mode === 'ai-jobagent' || mode === 'ai-agent' || mode === 'simulator' || mode === 'abonnement';
|
||||
if (!isAuthenticated) {
|
||||
return <AuthPage onAuthenticated={() => setIsAuthenticated(true)} />;
|
||||
}
|
||||
|
||||
if (activePage === 'jobs') {
|
||||
return (
|
||||
<JobsPage
|
||||
onLogout={handleLogout}
|
||||
onNavigate={handleNavigate}
|
||||
onOpenJobDetail={handleOpenJobDetail}
|
||||
theme={theme}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (activePage === 'cv') {
|
||||
return <CvPage onLogout={handleLogout} onNavigate={handleNavigate} theme={theme} onToggleTheme={handleToggleTheme} />;
|
||||
}
|
||||
|
||||
if (activePage === 'messages') {
|
||||
return <MessagesPage onLogout={handleLogout} onNavigate={handleNavigate} theme={theme} onToggleTheme={handleToggleTheme} />;
|
||||
}
|
||||
|
||||
if (activePage === 'agents' || activePage === 'ai-agent') {
|
||||
return (
|
||||
<AiAgentPage
|
||||
onLogout={handleLogout}
|
||||
onNavigate={handleNavigate}
|
||||
onOpenJobDetail={handleOpenJobDetail}
|
||||
theme={theme}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (activePage === 'job-detail' && jobDetailSelection) {
|
||||
return (
|
||||
<JobDetailPage
|
||||
jobId={jobDetailSelection.id}
|
||||
fromJobnet={jobDetailSelection.fromJobnet}
|
||||
onBack={handleBackFromJobDetail}
|
||||
onLogout={handleLogout}
|
||||
onNavigate={handleNavigate}
|
||||
theme={theme}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={isAppLayoutMode ? 'auth-root dashboard-mode' : 'auth-root'}>
|
||||
<div className="orb orb-1" />
|
||||
<div className="orb orb-2" />
|
||||
<div className="orb orb-3" />
|
||||
|
||||
{mode === 'dashboard' ? (
|
||||
<DashboardPage
|
||||
onLogout={logout}
|
||||
onNavigate={navigateFromSidebar}
|
||||
onOpenJob={(jobId, fromJobnet) =>
|
||||
navigate(`/jobs/${encodeURIComponent(jobId)}/${fromJobnet ? 'jobnet' : 'arbejd'}`)
|
||||
}
|
||||
/>
|
||||
) : mode === 'cv' ? (
|
||||
<CvPage onLogout={logout} onNavigate={navigateFromSidebar} />
|
||||
) : mode === 'beskeder' ? (
|
||||
<BeskederPage onLogout={logout} onNavigate={navigateFromSidebar} />
|
||||
) : mode === 'ai-jobagent' ? (
|
||||
<AiJobAgentPage
|
||||
onLogout={logout}
|
||||
onNavigate={navigateFromSidebar}
|
||||
onOpenJob={(jobId, fromJobnet) =>
|
||||
navigate(`/jobs/${encodeURIComponent(jobId)}/${fromJobnet ? 'jobnet' : 'arbejd'}`)
|
||||
}
|
||||
/>
|
||||
) : mode === 'ai-agent' ? (
|
||||
<AiAgentPage onLogout={logout} onNavigate={navigateFromSidebar} activeNavKey="ai-agent" />
|
||||
) : mode === 'simulator' ? (
|
||||
<JobSimulatorPage onLogout={logout} onNavigate={navigateFromSidebar} />
|
||||
) : mode === 'abonnement' ? (
|
||||
<SubscriptionPage onLogout={logout} onNavigate={navigateFromSidebar} />
|
||||
) : mode === 'job-detail' && jobDetailMatch ? (
|
||||
<JobDetailPage
|
||||
jobId={jobIdFromPath}
|
||||
fromJobnet={fromJobnetFromPath}
|
||||
onLogout={logout}
|
||||
onNavigate={navigateFromSidebar}
|
||||
/>
|
||||
) : mode === 'jobs' ? (
|
||||
<JobsPage
|
||||
onLogout={logout}
|
||||
onNavigate={navigateFromSidebar}
|
||||
onOpenJob={(jobId, fromJobnet) =>
|
||||
navigate(`/jobs/${encodeURIComponent(jobId)}/${fromJobnet ? 'jobnet' : 'arbejd'}`)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<section className="auth-shell glass-panel">
|
||||
<aside className="brand-panel">
|
||||
<div className="brand-chip">
|
||||
<span>Ar</span>
|
||||
</div>
|
||||
<h1>Arbejd.com</h1>
|
||||
<p>
|
||||
AI-assisteret jobsøgning med glasdesign og fokus på flow.
|
||||
</p>
|
||||
<ul className="brand-list">
|
||||
<li>Log ind</li>
|
||||
<li>Opret konto</li>
|
||||
<li>Glemt kodeord</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<section className="form-panel glass-panel">
|
||||
<div className="auth-theme-row">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<div className="mode-tabs">
|
||||
<button
|
||||
className={mode === 'login' ? 'tab-btn active' : 'tab-btn'}
|
||||
onClick={() => navigate('/login')}
|
||||
type="button"
|
||||
>
|
||||
Log ind
|
||||
</button>
|
||||
<button
|
||||
className={mode === 'register' ? 'tab-btn active' : 'tab-btn'}
|
||||
onClick={() => navigate('/register')}
|
||||
type="button"
|
||||
>
|
||||
Opret konto
|
||||
</button>
|
||||
<button
|
||||
className={mode === 'forgot' ? 'tab-btn active' : 'tab-btn'}
|
||||
onClick={() => navigate('/forgot-password')}
|
||||
type="button"
|
||||
>
|
||||
Glemt kode
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{mode === 'login' && (
|
||||
<LoginPage
|
||||
isLoading={isLoading}
|
||||
onSubmit={async (email, password, rememberMe) => {
|
||||
const response = await login(email, password, rememberMe);
|
||||
if (response.ok) {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === 'register' && (
|
||||
<RegisterPage
|
||||
isLoading={isLoading}
|
||||
onSubmit={async (payload) => {
|
||||
const response = await register(payload);
|
||||
if (response.ok) {
|
||||
navigate('/login');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === 'forgot' && (
|
||||
<ForgotPasswordPage
|
||||
isLoading={isLoading}
|
||||
onSubmit={async (email) => {
|
||||
const response = await forgotPassword(email);
|
||||
if (response.ok) {
|
||||
navigate('/login');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<p className={result.ok ? 'status success' : 'status error'}>
|
||||
{result.message}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
<DashboardPage
|
||||
onLogout={handleLogout}
|
||||
onNavigate={handleNavigate}
|
||||
onOpenJobDetail={handleOpenJobDetail}
|
||||
theme={theme}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user