exame fisico
This commit is contained in:
1 parent
9f2fe75ff6
commit
939293ba60
8 files changed
+560
-37
No files matched your search
@@ -0,0 +1,197 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { CameraControls, Html } from "@react-three/drei";
|
||||
import { Mannequin } from "../components/viewer/Mannequin.js";
|
||||
import { EXAM_POINTS, TECHNIQUES, type ExamTechnique } from "../data/physicalExam.js";
|
||||
|
||||
// Módulo de estudo pro exame físico: o mesmo boneco esquemático do módulo
|
||||
// de Planos e Eixos, agora com pontos clicáveis mostrando ONDE e COMO se
|
||||
// faz cada manobra (inspeção/palpação/percussão/ausculta) — pensado pra
|
||||
// quem está estudando propedêutica, não pra troca do atlas 3D real (que
|
||||
// seria overkill pra um boneco de pontos de exame, que não muda por
|
||||
// paciente).
|
||||
export default function PhysicalExamPage() {
|
||||
const [activeTechniques, setActiveTechniques] = useState<Set<ExamTechnique>>(
|
||||
() => new Set(TECHNIQUES.map((t) => t.key)),
|
||||
);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [leftOpen, setLeftOpen] = useState(false);
|
||||
const [rightOpen, setRightOpen] = useState(false);
|
||||
|
||||
function toggleTechnique(key: ExamTechnique) {
|
||||
setActiveTechniques((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
const visiblePoints = useMemo(
|
||||
() => EXAM_POINTS.filter((p) => activeTechniques.has(p.technique)),
|
||||
[activeTechniques],
|
||||
);
|
||||
|
||||
const selected = EXAM_POINTS.find((p) => p.id === selectedId) ?? null;
|
||||
const techniqueColor = (t: ExamTechnique) => TECHNIQUES.find((x) => x.key === t)?.color ?? "#fff";
|
||||
|
||||
return (
|
||||
<div className="h-full relative md:grid md:grid-cols-[260px_1fr_320px]">
|
||||
<aside
|
||||
className={`fixed md:static inset-y-0 left-0 z-30 w-[86vw] max-w-[280px] md:w-auto
|
||||
border-r border-white/10 bg-panel p-4 space-y-4 overflow-y-auto
|
||||
transition-transform duration-200 md:translate-x-0
|
||||
${leftOpen ? "translate-x-0" : "-translate-x-full"}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setLeftOpen(false)}
|
||||
className="md:hidden float-right text-slate-400 hover:text-white text-sm px-2 py-1"
|
||||
aria-label="Fechar painel"
|
||||
>
|
||||
✕ Fechar
|
||||
</button>
|
||||
<div>
|
||||
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Técnicas de Exame</h3>
|
||||
<div className="space-y-1.5">
|
||||
{TECHNIQUES.map((t) => (
|
||||
<label key={t.key} className="flex items-center gap-2 text-sm cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={activeTechniques.has(t.key)}
|
||||
onChange={() => toggleTechnique(t.key)}
|
||||
className="accent-accent"
|
||||
/>
|
||||
<span className="inline-block w-2.5 h-2.5 rounded-full" style={{ backgroundColor: t.color }} />
|
||||
{t.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-white/10">
|
||||
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Como usar</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
Marque as técnicas que quer estudar e clique nos pontos coloridos
|
||||
sobre o boneco. Cada ponto explica onde encostar, como fazer a
|
||||
manobra e o que ela avalia — pensado como revisão rápida de
|
||||
propedêutica, não substitui a prática supervisionada.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-white/10 space-y-1">
|
||||
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-2">Sistemas cobertos</h3>
|
||||
{[...new Set(EXAM_POINTS.map((p) => p.system))].map((sys) => (
|
||||
<p key={sys} className="text-xs text-slate-400">
|
||||
{sys} ({EXAM_POINTS.filter((p) => p.system === sys).length})
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{(leftOpen || rightOpen) && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-20 md:hidden"
|
||||
onClick={() => {
|
||||
setLeftOpen(false);
|
||||
setRightOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="relative h-full min-h-0">
|
||||
<button
|
||||
onClick={() => setLeftOpen(true)}
|
||||
className="md:hidden absolute top-3 left-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg"
|
||||
>
|
||||
☰ Técnicas
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setRightOpen(true)}
|
||||
className="md:hidden absolute top-3 right-3 z-10 bg-panel/90 border border-white/10 rounded-lg px-3 py-2 text-xs shadow-lg"
|
||||
>
|
||||
ℹ️ Detalhes
|
||||
</button>
|
||||
|
||||
<Canvas dpr={[1, 1.5]} camera={{ position: [1.6, 1.3, 2.0], fov: 45 }}>
|
||||
<color attach="background" args={["#0b1220"]} />
|
||||
<ambientLight intensity={0.65} />
|
||||
<directionalLight position={[2, 3, 2]} intensity={1} />
|
||||
|
||||
<Mannequin />
|
||||
|
||||
{visiblePoints.map((p) => {
|
||||
const isSelected = p.id === selectedId;
|
||||
const color = techniqueColor(p.technique);
|
||||
return (
|
||||
<Html key={p.id} position={p.position} center distanceFactor={4} zIndexRange={[10, 0]}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedId(p.id);
|
||||
setRightOpen(true);
|
||||
}}
|
||||
title={p.name}
|
||||
className="w-4 h-4 rounded-full border-2 border-white/80 transition-transform hover:scale-125"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
boxShadow: isSelected ? `0 0 0 5px ${color}55` : undefined,
|
||||
transform: isSelected ? "scale(1.4)" : undefined,
|
||||
}}
|
||||
/>
|
||||
</Html>
|
||||
);
|
||||
})}
|
||||
|
||||
<CameraControls makeDefault />
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
<aside
|
||||
className={`fixed md:static inset-y-0 right-0 z-30 w-[86vw] max-w-[320px] md:w-auto
|
||||
border-l border-white/10 bg-panel p-4 overflow-y-auto space-y-3
|
||||
transition-transform duration-200 md:translate-x-0
|
||||
${rightOpen ? "translate-x-0" : "translate-x-full"}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setRightOpen(false)}
|
||||
className="md:hidden text-slate-400 hover:text-white text-sm px-2 py-1"
|
||||
aria-label="Fechar painel"
|
||||
>
|
||||
✕ Fechar
|
||||
</button>
|
||||
|
||||
{!selected ? (
|
||||
<p className="text-sm text-slate-400">
|
||||
Clique em um ponto colorido sobre o boneco pra ver a técnica, o
|
||||
local exato e o que ela avalia.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span
|
||||
className="inline-block text-xs px-2 py-1 rounded-full mb-2"
|
||||
style={{
|
||||
backgroundColor: techniqueColor(selected.technique) + "33",
|
||||
color: techniqueColor(selected.technique),
|
||||
}}
|
||||
>
|
||||
{TECHNIQUES.find((t) => t.key === selected.technique)?.label}
|
||||
</span>
|
||||
<h2 className="text-lg font-semibold text-white">{selected.name}</h2>
|
||||
<p className="text-xs text-slate-500">{selected.system}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-1">Como fazer</h3>
|
||||
<p className="text-sm text-slate-300 leading-relaxed">{selected.howTo}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs uppercase tracking-wide text-slate-400 mb-1">O que avalia</h3>
|
||||
<p className="text-sm text-slate-300 leading-relaxed">{selected.whatFor}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user