diff --git a/app/layout.tsx b/app/layout.tsx index 529561d..797f31e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,6 +6,9 @@ import "./globals.css"; import { Toaster } from "@/components/ui/toaster"; // [PASSO 1.2] - Importando o nosso provider import { AppointmentsProvider } from "./context/AppointmentsContext"; +import { AccessibilityProvider } from "./context/AccessibilityContext"; +import { AccessibilityModal } from "@/components/accessibility-modal"; +import { ThemeInitializer } from "@/components/theme-initializer"; export default function RootLayout({ children, @@ -13,10 +16,14 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + {/* [PASSO 1.2] - Envolvendo a aplicação com o provider */} - {children} + + + {children} + + diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx index 8c400cb..71a6ab4 100644 --- a/components/LoginForm.tsx +++ b/components/LoginForm.tsx @@ -1,247 +1,223 @@ // Caminho: components/LoginForm.tsx -"use client" +"use client"; -import type React from "react" -import { useState } from "react" -import { useRouter } from "next/navigation" -import Link from "next/link" -import Cookies from "js-cookie" -import { jwtDecode } from "jwt-decode" -import { cn } from "@/lib/utils" +import type React from "react"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import Cookies from "js-cookie"; +import { jwtDecode } from "jwt-decode"; +import { cn } from "@/lib/utils"; // Componentes Shadcn UI -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Separator } from "@/components/ui/separator" +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { apikey } from "@/services/api.mjs"; // Hook customizado -import { useToast } from "@/hooks/use-toast" +import { useToast } from "@/hooks/use-toast"; // Ícones -import { Eye, EyeOff, Mail, Lock, Loader2, UserCheck, Stethoscope, IdCard, Receipt } from "lucide-react" +import { Eye, EyeOff, Mail, Lock, Loader2, UserCheck, Stethoscope, IdCard, Receipt } from "lucide-react"; interface LoginFormProps { - title: string - description: string - role: "secretary" | "doctor" | "patient" | "admin" | "manager" | "finance" - themeColor: "blue" | "green" | "orange" - redirectPath: string - children?: React.ReactNode + title: string; + description: string; + role: "secretary" | "doctor" | "patient" | "admin" | "manager" | "finance"; + themeColor: "blue" | "green" | "orange"; + redirectPath: string; + children?: React.ReactNode; } interface FormState { - email: string - password: string + email: string; + password: string; } // Supondo que o payload do seu token tenha esta estrutura interface DecodedToken { - name: string - email: string - role: string - exp: number - // Adicione outros campos que seu token possa ter + name: string; + email: string; + role: string; + exp: number; + // Adicione outros campos que seu token possa ter } const themeClasses = { - blue: { - iconBg: "bg-blue-100", - iconText: "text-blue-600", - button: "bg-blue-600 hover:bg-blue-700", - link: "text-blue-600 hover:text-blue-700", - focus: "focus:border-blue-500 focus:ring-blue-500", - }, - green: { - iconBg: "bg-green-100", - iconText: "text-green-600", - button: "bg-green-600 hover:bg-green-700", - link: "text-green-600 hover:text-green-700", - focus: "focus:border-green-500 focus:ring-green-500", - }, - orange: { - iconBg: "bg-orange-100", - iconText: "text-orange-600", - button: "bg-orange-600 hover:bg-orange-700", - link: "text-orange-600 hover:text-orange-700", - focus: "focus:border-orange-500 focus:ring-orange-500", - }, -} - -const roleIcons = { - secretary: UserCheck, - patient: Stethoscope, - doctor: Stethoscope, - admin: UserCheck, - manager: IdCard, - finance: Receipt, -} - -export function LoginForm({ title, description, role, themeColor, redirectPath, children }: LoginFormProps) { - const [form, setForm] = useState({ email: "", password: "" }) - const [showPassword, setShowPassword] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() - const { toast } = useToast() - - const currentTheme = themeClasses[themeColor] - const Icon = roleIcons[role] - - // ================================================================== - // AJUSTE PRINCIPAL NA LÓGICA DE LOGIN - // ================================================================== - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - - const LOGIN_URL = "https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password"; - const API_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; - - if (!API_KEY) { - toast({ - title: "Erro de Configuração", - description: "A chave da API não foi encontrada.", - variant: "destructive", - }); - setIsLoading(false); - return; - } - - try { - const response = await fetch(LOGIN_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - "apikey": API_KEY, - }, - body: JSON.stringify({ email: form.email, password: form.password }), - }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error_description || "Credenciais inválidas. Tente novamente."); - } - - const accessToken = data.access_token; - const user = data.user; - - /* =================== Verificação de Role Desativada Temporariamente =================== */ - // if (user.user_metadata.role !== role) { - // toast({ title: "Acesso Negado", ... }); - // return; - // } - /* ===================================================================================== */ - - Cookies.set("access_token", accessToken, { expires: 1, secure: true }); - localStorage.setItem('user_info', JSON.stringify(user)); - - toast({ - title: "Login bem-sucedido!", - description: `Bem-vindo(a), ${user.user_metadata.full_name || 'usuário'}! Redirecionando...`, - }); - - router.push(redirectPath); - - } catch (error) { - toast({ - title: "Erro no Login", - description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.", - }); - } finally { - setIsLoading(false); - } + blue: { + iconBg: "bg-blue-100", + iconText: "text-blue-600", + button: "bg-blue-600 hover:bg-blue-700", + link: "text-blue-600 hover:text-blue-700", + focus: "focus:border-blue-500 focus:ring-blue-500", + }, + green: { + iconBg: "bg-green-100", + iconText: "text-green-600", + button: "bg-green-600 hover:bg-green-700", + link: "text-green-600 hover:text-green-700", + focus: "focus:border-green-500 focus:ring-green-500", + }, + orange: { + iconBg: "bg-orange-100", + iconText: "text-orange-600", + button: "bg-orange-600 hover:bg-orange-700", + link: "text-orange-600 hover:text-orange-700", + focus: "focus:border-orange-500 focus:ring-orange-500", + }, }; - // O JSX do return permanece exatamente o mesmo, preservando seus ajustes. - return ( - - -
- -
-
- {title} - {description} -
-
- -
- {/* Inputs e Botão */} -
- -
- - setForm({ ...form, email: e.target.value })} - className={cn("pl-11 h-12 border-slate-200", currentTheme.focus)} - required - disabled={isLoading} - /> -
-
-
- -
- - setForm({ ...form, password: e.target.value })} - className={cn("pl-11 pr-12 h-12 border-slate-200", currentTheme.focus)} - required - disabled={isLoading} - /> - -
-
- -
- {/* Conteúdo Extra (children) */} -
- {children ? ( -
-
-
-
+const roleIcons = { + secretary: UserCheck, + patient: Stethoscope, + doctor: Stethoscope, + admin: UserCheck, + manager: IdCard, + finance: Receipt, +}; + +export function LoginForm({ title, description, role, themeColor, redirectPath, children }: LoginFormProps) { + const [form, setForm] = useState({ email: "", password: "" }); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + const { toast } = useToast(); + + const currentTheme = themeClasses[themeColor]; + const Icon = roleIcons[role]; + + // ================================================================== + // AJUSTE PRINCIPAL NA LÓGICA DE LOGIN + // ================================================================== + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + const LOGIN_URL = "https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password"; + const API_KEY = apikey; + + if (!API_KEY) { + toast({ + title: "Erro de Configuração", + description: "A chave da API não foi encontrada.", + }); + setIsLoading(false); + return; + } + + try { + const response = await fetch(LOGIN_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + apikey: API_KEY, + }, + body: JSON.stringify({ email: form.email, password: form.password }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error_description || "Credenciais inválidas. Tente novamente."); + } + + const accessToken = data.access_token; + const user = data.user; + + /* =================== Verificação de Role Desativada Temporariamente =================== */ + // if (user.user_metadata.role !== role) { + // toast({ title: "Acesso Negado", ... }); + // return; + // } + /* ===================================================================================== */ + + Cookies.set("access_token", accessToken, { expires: 1, secure: true }); + localStorage.setItem("user_info", JSON.stringify(user)); + + toast({ + title: "Login bem-sucedido!", + description: `Bem-vindo(a), ${user.user_metadata.full_name || "usuário"}! Redirecionando...`, + }); + + router.push(redirectPath); + } catch (error) { + toast({ + title: "Erro no Login", + description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.", + }); + } finally { + setIsLoading(false); + } + }; + + // O JSX do return permanece exatamente o mesmo, preservando seus ajustes. + return ( + + +
+
-
- Novo por aqui? +
+ {title} + {description}
-
- {children} -
- ) : ( - <> -
- - ou -
-
- - Voltar à página inicial - -
- - )} -
- - - ) -} \ No newline at end of file + + +
+ {/* Inputs e Botão */} +
+ +
+ + setForm({ ...form, email: e.target.value })} className={cn("pl-11 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} /> +
+
+
+ +
+ + setForm({ ...form, password: e.target.value })} className={cn("pl-11 pr-12 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} /> + +
+
+ +
+ {/* Conteúdo Extra (children) */} +
+ {children ? ( +
+
+
+
+
+
+ Novo por aqui? +
+
+ {children} +
+ ) : ( + <> +
+ + ou +
+
+ + Voltar à página inicial + +
+ + )} +
+
+ + ); +} diff --git a/services/api.mjs b/services/api.mjs index 73d3721..360c744 100644 --- a/services/api.mjs +++ b/services/api.mjs @@ -1,90 +1,84 @@ - const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co"; -const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; +const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; +export const apikey = API_KEY; var tempToken; export async function login() { - const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password", { - method: "POST", - headers: { - "Content-Type": "application/json", - Prefer: "return=representation", - "apikey": API_KEY, // valor fixo - }, - body: JSON.stringify({ email: "riseup@popcode.com.br", password: "riseup" }), - }); + const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password", { + method: "POST", + headers: { + "Content-Type": "application/json", + Prefer: "return=representation", + apikey: API_KEY, // valor fixo + }, + body: JSON.stringify({ email: "riseup@popcode.com.br", password: "riseup" }), + }); - const data = await response.json(); - - localStorage.setItem("token", data.access_token); - - return data; + const data = await response.json(); + + localStorage.setItem("token", data.access_token); + + return data; } - let loginPromise = login(); - - async function request(endpoint, options = {}) { - - if (loginPromise) { - try { - await loginPromise; - } catch (error) { - console.error("Falha na autenticação inicial:", error); - } - - loginPromise = null; - } - - const token = localStorage.getItem("token"); - - const headers = { - "Content-Type": "application/json", - "apikey": API_KEY, - ...(token ? { "Authorization": `Bearer ${token}` } : {}), - ...options.headers, - }; - - try { - const response = await fetch(`${BASE_URL}${endpoint}`, { - ...options, - headers, - }); - - if (!response.ok) { - - let errorBody = `Status: ${response.status}`; - try { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.includes("application/json")) { - const jsonError = await response.json(); - - errorBody = jsonError.message || JSON.stringify(jsonError); - } else { - errorBody = await response.text(); + if (loginPromise) { + try { + await loginPromise; + } catch (error) { + console.error("Falha na autenticação inicial:", error); } - } catch (e) { - - errorBody = `Status: ${response.status} - Falha ao ler corpo do erro.`; - } - - throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${errorBody}`); + + loginPromise = null; } - const contentType = response.headers.get("content-type"); - if (response.status === 204 || (contentType && !contentType.includes("application/json")) || !contentType) { - return {}; + + const token = localStorage.getItem("token"); + + const headers = { + "Content-Type": "application/json", + apikey: API_KEY, + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...options.headers, + }; + + try { + const response = await fetch(`${BASE_URL}${endpoint}`, { + ...options, + headers, + }); + + if (!response.ok) { + let errorBody = `Status: ${response.status}`; + try { + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("application/json")) { + const jsonError = await response.json(); + + errorBody = jsonError.message || JSON.stringify(jsonError); + } else { + errorBody = await response.text(); + } + } catch (e) { + errorBody = `Status: ${response.status} - Falha ao ler corpo do erro.`; + } + + throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${errorBody}`); + } + const contentType = response.headers.get("content-type"); + if (response.status === 204 || (contentType && !contentType.includes("application/json")) || !contentType) { + return {}; + } + return await response.json(); + } catch (error) { + console.error("Erro na requisição:", error); + throw error; } - return await response.json(); - } catch (error) { - console.error("Erro na requisição:", error); - throw error; - } } export const api = { - get: (endpoint) => request(endpoint, { method: "GET" }), - post: (endpoint, data) => request(endpoint, { method: "POST", body: JSON.stringify(data) }), - patch: (endpoint, data) => request(endpoint, { method: "PATCH", body: JSON.stringify(data) }), - delete: (endpoint) => request(endpoint, { method: "DELETE" }), -}; \ No newline at end of file + get: (endpoint) => request(endpoint, { method: "GET" }), + post: (endpoint, data) => request(endpoint, { method: "POST", body: JSON.stringify(data) }), + patch: (endpoint, data) => request(endpoint, { method: "PATCH", body: JSON.stringify(data) }), + delete: (endpoint) => request(endpoint, { method: "DELETE" }), +};