import React, { FormEvent, useEffect, useMemo, useState } from 'react'; import { createRoot } from 'react-dom/client'; import './styles.css'; type User = { id: number; name: string; email: string }; type Project = { id: number; title: string; description: string | null; status: 'ideia' | 'ativo' | 'pausado' | 'concluido' | 'abandonado'; priority: 'baixa' | 'media' | 'alta'; next_action: string | null; last_progress_at: string | null; due_date: string | null; stale_after_days: number; notes: string | null; days_stale?: number; }; type Task = { id: number; project_id: number | null; project_title?: string | null; title: string; notes: string | null; due_at: string | null; completed_at: string | null; importance: 'baixa' | 'media' | 'alta'; recurrence_rule: 'nenhuma' | 'diaria' | 'semanal' | 'mensal'; email_reminder: number; }; type StudyPlan = { id: number; title: string; objective: string | null; weekly_minutes: number; materials: string | null; progress: number; status: 'ativo' | 'pausado' | 'concluido'; next_session_at: string | null; }; type Settings = { email_to: string; alerts_enabled: number; digest_hour: number; timezone: string; }; type Dashboard = { date: string; focusTasks: Task[]; staleProjects: Project[]; studyQueue: StudyPlan[]; counts: { projects: number; tasks: number; study_plans: number }; }; type Review = { newProjects: Project[]; completedTasks: Task[]; overdueTasks: Task[]; staleProjects: Project[]; }; type LoadErrors = Partial>; const emptyProject = { title: '', description: '', status: 'ideia', priority: 'media', next_action: '', last_progress_at: '', due_date: '', stale_after_days: 7, notes: '', }; const emptyTask = { title: '', project_id: '', notes: '', due_at: '', importance: 'media', recurrence_rule: 'nenhuma', email_reminder: true, }; const emptyStudy = { title: '', objective: '', weekly_minutes: 120, materials: '', progress: 0, status: 'ativo', next_session_at: '', }; async function api(path: string, options: RequestInit = {}): Promise { const url = new URL(`${appBasePath()}api/${path}`, window.location.origin); const response = await fetch(url, { credentials: 'same-origin', headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) }, ...options, }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(payload.error ?? 'Erro inesperado.'); } return payload as T; } function appBasePath(): string { const pathname = window.location.pathname; const publicIndex = pathname.indexOf('/public'); if (publicIndex >= 0) { return `${pathname.slice(0, publicIndex + '/public'.length).replace(/\/$/, '')}/`; } return pathname.endsWith('/') ? pathname : `${pathname}/`; } function normalizeDateTime(value: string): string | null { if (!value) return null; return value.includes('T') ? `${value.replace('T', ' ')}:00` : value; } function formatDate(value?: string | null): string { if (!value) return 'Sem data'; return value.replace('T', ' ').slice(0, 16); } function App() { const [booting, setBooting] = useState(true); const [hasUser, setHasUser] = useState(false); const [user, setUser] = useState(null); useEffect(() => { Promise.all([ api<{ hasUser: boolean }>('setup/status'), api<{ user: User | null }>('me').catch(() => ({ user: null })), ]) .then(([setup, me]) => { setHasUser(setup.hasUser); setUser(me.user); }) .finally(() => setBooting(false)); }, []); if (booting) return ; if (!hasUser) return { setHasUser(true); setUser(nextUser); }} />; if (!user) return ; return setUser(null)} />; } function Splash() { return (
PC

Carregando seu painel...

); } function SetupScreen({ onReady }: { onReady: (user: User) => void }) { const [form, setForm] = useState({ name: '', email: '', password: '' }); const [error, setError] = useState(''); async function submit(event: FormEvent) { event.preventDefault(); setError(''); try { const payload = await api<{ user: User }>('setup', { method: 'POST', body: JSON.stringify(form) }); onReady(payload.user); } catch (err) { setError((err as Error).message); } } return (
setForm({ ...form, name })} /> setForm({ ...form, email })} /> setForm({ ...form, password })} /> {error &&

{error}

}
); } function LoginScreen({ onLogin }: { onLogin: (user: User) => void }) { const [form, setForm] = useState({ email: '', password: '' }); const [error, setError] = useState(''); async function submit(event: FormEvent) { event.preventDefault(); setError(''); try { const payload = await api<{ user: User }>('auth/login', { method: 'POST', body: JSON.stringify(form) }); onLogin(payload.user); } catch (err) { setError((err as Error).message); } } return (
setForm({ ...form, email })} /> setForm({ ...form, password })} /> {error &&

{error}

}
); } function AuthCard({ title, subtitle, children }: { title: string; subtitle: string; children: React.ReactNode }) { return (
PC

{title}

{subtitle}

{children}
); } function Workspace({ user, onLogout }: { user: User; onLogout: () => void }) { const [view, setView] = useState('hoje'); const [dashboard, setDashboard] = useState(null); const [projects, setProjects] = useState([]); const [tasks, setTasks] = useState([]); const [studyPlans, setStudyPlans] = useState([]); const [settings, setSettings] = useState(null); const [review, setReview] = useState(null); const [notice, setNotice] = useState(''); const [loadErrors, setLoadErrors] = useState({}); async function loadOne(key: keyof LoadErrors, loader: () => Promise) { try { await loader(); setLoadErrors((current) => ({ ...current, [key]: undefined })); } catch (err) { setLoadErrors((current) => ({ ...current, [key]: (err as Error).message })); } } const loaders = useMemo(() => ({ dashboard: () => api<{ dashboard: Dashboard }>('dashboard').then((r) => setDashboard(r.dashboard)), projects: () => api<{ projects: Project[] }>('projects').then((r) => setProjects(r.projects)), tasks: () => api<{ tasks: Task[] }>('tasks').then((r) => setTasks(r.tasks)), study: () => api<{ studyPlans: StudyPlan[] }>('study-plans').then((r) => setStudyPlans(r.studyPlans)), settings: () => api<{ settings: Settings }>('settings').then((r) => setSettings(r.settings)), review: () => api<{ review: Review }>('review/weekly').then((r) => setReview(r.review)), }), []); async function refreshAll() { await Promise.all([ loadOne('dashboard', loaders.dashboard), loadOne('projects', loaders.projects), loadOne('tasks', loaders.tasks), loadOne('study', loaders.study), ]); } useEffect(() => { refreshAll(); loadOne('settings', loaders.settings); }, []); useEffect(() => { if (view === 'tarefas') loadOne('tasks', loaders.tasks); if (view === 'estudos') loadOne('study', loaders.study); if (view === 'projetos') loadOne('projects', loaders.projects); if (view === 'revisao') loadOne('review', loaders.review); if (view === 'config') loadOne('settings', loaders.settings); }, [view]); async function logout() { await api('auth/logout', { method: 'POST' }); onLogout(); } const nav = [ ['hoje', 'Hoje'], ['projetos', 'Projetos'], ['tarefas', 'Tarefas'], ['estudos', 'Estudos'], ['revisao', 'Revisao'], ['config', 'Config'], ]; return (
{notice && } {view === 'hoje' && } {view === 'projetos' && } {view === 'tarefas' && } {view === 'estudos' && } {view === 'revisao' && } {view === 'config' && settings && loadOne('settings', loaders.settings)} notify={setNotice} />}
); } function DashboardView({ dashboard, error, onRefresh }: { dashboard: Dashboard | null; error?: string; onRefresh: () => void }) { if (error) return ; if (!dashboard) return

Carregando...

; return (
Atualizar} />
( )} /> ( )} /> ( )} />
); } function ProjectsView({ projects, error, reload, notify }: { projects: Project[]; error?: string; reload: () => void; notify: (text: string) => void }) { const [form, setForm] = useState(emptyProject); async function submit(event: FormEvent) { event.preventDefault(); await api('projects', { method: 'POST', body: JSON.stringify(form) }); setForm(emptyProject); notify('Projeto criado.'); reload(); } async function update(project: Project, patch: Partial) { await api(`projects/${project.id}`, { method: 'PUT', body: JSON.stringify({ ...project, ...patch }) }); reload(); } return (
{error && }
setForm({ ...form, title })} /> setForm({ ...form, next_action })} /> setForm({ ...form, priority })} options={['baixa', 'media', 'alta']} /> setForm({ ...form, stale_after_days: Number(value) })} />