diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5948a10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +projeto-figma + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..a5504ba --- /dev/null +++ b/src/App.css @@ -0,0 +1,6 @@ +.line-clamp-2 { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..790bdc6 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,213 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' + +import './App.css' +import { AppShell } from './components/AppShell.jsx' +import { AgendaPage } from './pages/AgendaPage.jsx' +import { AnalyticsPage } from './pages/AnalyticsPage.jsx' +import { ForgotPasswordPage, LoginPage, RegisterPage } from './pages/AuthPages.jsx' +import { HomePage } from './pages/HomePage.jsx' +import { MedicalRecordsPage } from './pages/MedicalRecordsPage.jsx' +import { MessagesPage } from './pages/MessagesPage.jsx' +import { NotFoundPage } from './pages/NotFoundPage.jsx' +import { PatientDetailPage, PatientsPage } from './pages/PatientsPage.jsx' +import { ProfilePage } from './pages/ProfilePage.jsx' +import { ReportsPage } from './pages/ReportsPage.jsx' +import { SettingsPage } from './pages/SettingsPage.jsx' +import { TeamPage } from './pages/TeamPage.jsx' +import { VisitsPage } from './pages/VisitsPage.jsx' +import { patientRepository } from './repositories/patientRepository.js' + +function App() { + const [location, setLocation] = useState(() => readLocation()) + + const navigate = useCallback((to, options = {}) => { + if (options.replace) { + window.history.replaceState({}, '', to) + } else { + window.history.pushState({}, '', to) + } + + setLocation(readLocation()) + const hash = to.split('#')[1] + window.requestAnimationFrame(() => { + if (hash) { + document.getElementById(hash)?.scrollIntoView({ block: 'start' }) + } else { + window.scrollTo({ left: 0, top: 0 }) + } + }) + }, []) + + useEffect(() => { + function handlePopState() { + setLocation(readLocation()) + } + + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, []) + + const route = useMemo(() => resolveRoute(location.pathname, navigate), [location.pathname, navigate]) + + if (!route.withShell) { + return route.element + } + + return ( + + {route.element} + + ) +} + +function resolveRoute(pathname, navigate) { + if (pathname === '/' || pathname === '/login') { + return { + element: , + title: 'Login', + withShell: false, + } + } + + if (pathname === '/cadastro') { + return { + element: , + title: 'Cadastro', + withShell: false, + } + } + + if (pathname === '/recuperar-senha') { + return { + element: , + title: 'Recuperar senha', + withShell: false, + } + } + + if (pathname === '/inicio' || pathname === '/home' || pathname === '/dashboard') { + return { + element: , + title: 'Painel', + withShell: true, + } + } + + if (pathname === '/agenda') { + return { + element: , + title: 'Agenda', + withShell: true, + } + } + + if (pathname === '/pacientes') { + return { + element: , + title: 'Pacientes', + withShell: true, + } + } + + if (pathname === '/prontuario') { + return { + element: , + title: 'Prontuário', + withShell: true, + } + } + + if (pathname.startsWith('/pacientes/')) { + const patientId = pathname.split('/')[2] + const patient = patientRepository.getById(patientId) + + return { + element: patient ? ( + + ) : ( + + ), + title: patient?.name || 'Paciente nao encontrado', + withShell: true, + } + } + + if (pathname === '/consultas') { + return { + element: , + title: 'Consultas', + withShell: true, + } + } + + if (pathname === '/laudos') { + return { + element: , + title: 'Laudos', + withShell: true, + } + } + + if (pathname === '/relatorios') { + return { + element: , + title: 'Relatórios', + withShell: true, + } + } + + if (pathname === '/camunicacao' || pathname === '/comunicacao' || pathname === '/mensagens') { + return { + element: , + title: 'Comunicação', + withShell: true, + } + } + + if (pathname === '/profissionais') { + return { + element: , + title: 'Profissionais', + withShell: true, + } + } + + if (pathname === '/perfil') { + return { + element: , + title: 'Perfil', + withShell: true, + } + } + + if (pathname === '/configuracoes' || pathname === '/config') { + return { + element: , + title: 'Configurações', + withShell: true, + } + } + + return { + element: , + title: 'Tela nao encontrada', + withShell: true, + } +} + +function readLocation() { + return { + pathname: normalizePath(window.location.pathname), + search: window.location.search, + } +} + +function normalizePath(pathname) { + if (!pathname || pathname === '/') { + return '/' + } + + return pathname.replace(/\/+$/, '') +} + +export default App diff --git a/src/assets/figma/login-clinic.png b/src/assets/figma/login-clinic.png new file mode 100644 index 0000000..4ddc259 Binary files /dev/null and b/src/assets/figma/login-clinic.png differ diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/components/AppShell.jsx b/src/components/AppShell.jsx new file mode 100644 index 0000000..516ab53 --- /dev/null +++ b/src/components/AppShell.jsx @@ -0,0 +1,333 @@ +import { useMemo, useState } from 'react' + +import { BrandLogo } from './Brand.jsx' + +const navItems = [ + { href: '/inicio', label: 'Painel', icon: 'pulse', activePaths: ['/inicio', '/home', '/dashboard'] }, + { href: '/agenda', label: 'Agenda', icon: 'calendar' }, + { href: '/pacientes', label: 'Pacientes', icon: 'users', exact: true }, + { href: '/prontuario', label: 'Prontuário', icon: 'file' }, + { href: '/laudos', label: 'Laudos', icon: 'clipboard' }, + { + href: '/camunicacao', + label: 'Comunicação', + icon: 'message', + activePaths: ['/camunicacao', '/comunicacao', '/mensagens'], + }, + { href: '/relatorios', label: 'Relatórios', icon: 'chart' }, + { href: '/configuracoes', label: 'Configurações', icon: 'settings', activePaths: ['/configuracoes', '/config'] }, +] + +const titles = { + '/inicio': 'Painel', + '/home': 'Painel', + '/dashboard': 'Painel', + '/agenda': 'Agenda', + '/consultas': 'Consultas', + '/laudos': 'Laudos', + '/pacientes': 'Pacientes', + '/prontuario': 'Prontuário', + '/camunicacao': 'Comunicação', + '/comunicacao': 'Comunicação', + '/mensagens': 'Comunicação', + '/relatorios': 'Relatórios', + '/profissionais': 'Profissionais', + '/perfil': 'Perfil', + '/configuracoes': 'Configurações', + '/config': 'Configurações', +} + +export function AppShell({ children, currentPath, navigate, routeTitle }) { + const [menuOpen, setMenuOpen] = useState(false) + const [quickSearch, setQuickSearch] = useState('') + + const pageTitle = useMemo(() => { + if (currentPath.startsWith('/pacientes/') && routeTitle) { + return routeTitle + } + + return routeTitle || titles[currentPath] || 'MediConnect' + }, [currentPath, routeTitle]) + + function goTo(path) { + setMenuOpen(false) + navigate(path) + } + + return ( +
+ + Pular para conteudo + + + + + {menuOpen ? ( + +
+ + setQuickSearch(event.target.value)} + placeholder="Buscar paciente, prontuário..." + value={quickSearch} + /> +
+
+ +
+ + +
+ + {quickSearch ? ( +
+ Busca local ativa por {quickSearch}. +
+ ) : null} + + +
+
+ {pageTitle} +
+ {children} +
+ + + ) +} + +function NavItem({ active, item, onNavigate }) { + return ( + { + event.preventDefault() + onNavigate(item.href) + }} + > + + {item.label} + + ) +} + +function isActive(pathname, item) { + if (item.activePaths?.some((path) => pathname === path || pathname.startsWith(`${path}/`))) { + return true + } + + if (item.activePrefixes?.some((path) => pathname.startsWith(path))) { + return true + } + + if (item.exact) { + return pathname === item.href + } + + return pathname === item.href || pathname.startsWith(`${item.href}/`) +} + +function AppIcon({ className = 'size-5', name }) { + const common = { + className, + fill: 'none', + stroke: 'currentColor', + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: 1.8, + viewBox: '0 0 24 24', + } + + if (name === 'calendar') { + return ( + + + + ) + } + + if (name === 'users') { + return ( + + + + ) + } + + if (name === 'file') { + return ( + + + + + ) + } + + if (name === 'clipboard') { + return ( + + + + ) + } + + if (name === 'message') { + return ( + + + + ) + } + + if (name === 'chart') { + return ( + + + + + ) + } + + if (name === 'dollar') { + return ( + + + + ) + } + + if (name === 'settings') { + return ( + + + + + ) + } + + return ( + + + + ) +} + +function BellIcon({ className = 'size-5' }) { + return ( + + + + + ) +} + +function ChevronDownIcon({ className = 'size-4' }) { + return ( + + + + ) +} + +function SearchIcon({ className = 'size-4' }) { + return ( + + + + + ) +} diff --git a/src/components/Brand.jsx b/src/components/Brand.jsx new file mode 100644 index 0000000..b541cd0 --- /dev/null +++ b/src/components/Brand.jsx @@ -0,0 +1,36 @@ +export function BrandLogo({ + className = '', + iconClassName = 'size-10 rounded-[6px]', + markClassName = 'size-6', + textClassName = 'text-2xl font-bold leading-8 tracking-[-0.025em] text-white', +}) { + return ( +
+
+ +
+

MediConnect

+
+ ) +} + +export function StethoscopeIcon({ className = 'size-6' }) { + return ( + + ) +} diff --git a/src/components/ui.jsx b/src/components/ui.jsx new file mode 100644 index 0000000..b5918a1 --- /dev/null +++ b/src/components/ui.jsx @@ -0,0 +1,205 @@ +const toneClasses = { + blue: 'bg-sky-50 text-sky-700 border-sky-200', + green: 'bg-emerald-50 text-emerald-700 border-emerald-200', + amber: 'bg-amber-50 text-amber-700 border-amber-200', + red: 'bg-rose-50 text-rose-700 border-rose-200', + slate: 'bg-slate-100 text-slate-700 border-slate-200', + neutral: 'bg-white text-slate-700 border-slate-200', +} + +const buttonVariants = { + primary: + 'border-sky-700 bg-sky-700 text-white hover:bg-sky-800 focus-visible:outline-sky-700', + secondary: + 'border-slate-300 bg-white text-slate-700 hover:bg-slate-50 focus-visible:outline-slate-500', + ghost: + 'border-transparent bg-transparent text-slate-600 hover:bg-slate-100 focus-visible:outline-slate-500', + danger: + 'border-rose-600 bg-rose-600 text-white hover:bg-rose-700 focus-visible:outline-rose-600', +} + +export function Button({ + children, + className = '', + variant = 'primary', + type = 'button', + ...props +}) { + return ( + + ) +} + +export function Card({ children, className = '' }) { + return ( +
+ {children} +
+ ) +} + +export function Badge({ children, tone = 'neutral', className = '' }) { + return ( + + {children} + + ) +} + +export function PageHeader({ actions, description, eyebrow, title }) { + return ( +
+
+ {eyebrow ? ( +

+ {eyebrow} +

+ ) : null} +

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ {actions ?
{actions}
: null} +
+ ) +} + +export function StatCard({ helper, label, tone = 'slate', value }) { + return ( + +
+
+

{label}

+

{value}

+
+
+

{helper}

+
+ ) +} + +export function EmptyState({ action, description, title }) { + return ( +
+

{title}

+

{description}

+ {action ?
{action}
: null} +
+ ) +} + +export function Field({ children, hint, label }) { + return ( + + ) +} + +export function TextInput({ className = '', ...props }) { + return ( + + ) +} + +export function SelectInput({ children, className = '', ...props }) { + return ( + + ) +} + +export function Textarea({ className = '', ...props }) { + return ( +