Merge pull request 'main' (#22) from StsDanilo/riseup-squad21:main into main

Reviewed-on: RiseUP/riseup-squad21#22
This commit is contained in:
LiraS2 2025-10-09 20:15:28 +00:00
commit 117f5845d2
14 changed files with 485 additions and 1054 deletions

View File

@ -1,157 +1,11 @@
"use client"
// Caminho: app/(doctor)/login/page.tsx
import type React from "react"
import { LoginForm } from "@/components/LoginForm";
import { useState } from "react"
import { useRouter } from "next/navigation"
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 { useToast } from "@/hooks/use-toast"
import { Eye, EyeOff, Mail, Lock, Stethoscope, Loader2 } from "lucide-react"
import Link from "next/link"
interface LoginForm {
email: string
password: string
}
export default function DoctorLogin() {
const [form, setForm] = useState<LoginForm>({ email: "", password: "" })
const [showPassword, setShowPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const { toast } = useToast()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
// Simular autenticação
setTimeout(() => {
if (form.email && form.password) {
const doctorData = {
id: "1",
name: "Dr. João Santos",
email: form.email,
phone: "(11) 98888-8888",
cpf: "987.654.321-00",
crm: "CRM/SP 123456",
specialty: "Cardiologia",
department: "Cardiologia",
permissions: ["view_patients", "manage_appointments", "create_reports"],
}
localStorage.setItem("doctorData", JSON.stringify(doctorData))
localStorage.setItem("userType", "doctor")
toast({
title: "Login realizado com sucesso!",
description: "Bem-vindo ao sistema, " + doctorData.name,
})
router.push("/doctor/medicos")
} else {
toast({
title: "Erro no login",
description: "Por favor, preencha todos os campos.",
variant: "destructive",
})
}
setIsLoading(false)
}, 1500)
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-xl border-0 bg-card/80 backdrop-blur-sm">
<CardHeader className="text-center space-y-4 pb-8">
<div className="mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
<Stethoscope className="w-8 h-8 text-green-600 dark:text-green-400" />
</div>
<div>
<CardTitle className="text-2xl font-bold text-foreground">Área do Médico</CardTitle>
<CardDescription className="text-muted-foreground mt-2">Acesse o sistema médico</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium text-foreground">
E-mail
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
id="email"
type="email"
placeholder="dr.medico@clinica.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="pl-10 h-11 border-border focus:border-green-500 focus:ring-green-500"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm font-medium text-foreground">
Senha
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Digite sua senha"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="pl-10 pr-10 h-11 border-border focus:border-green-500 focus:ring-green-500"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<Button
type="submit"
className="w-full h-11 bg-green-600 text-white hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 dark:text-white"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Entrando...
</>
) : (
"Entrar"
)}
</Button>
</form>
<div className="relative">
<Separator className="my-6" />
<span className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card px-3 text-sm text-muted-foreground">
ou
</span>
</div>
<div className="text-center">
<Link href="/" className="text-sm text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-500 font-medium hover:underline">
Voltar à página inicial
</Link>
</div>
</CardContent>
</Card>
</div>
)
export default function DoctorLoginPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-white to-green-50 flex items-center justify-center p-4">
<LoginForm title="Área do Médico" description="Acesse o sistema médico" role="doctor" themeColor="green" redirectPath="/doctor/medicos" />
</div>
);
}

View File

@ -57,8 +57,8 @@ export default function PacientesPage() {
return new Intl.DateTimeFormat('pt-BR').format(date);
};
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [currentPage, setCurrentPage] = useState(1);
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;

View File

@ -1,155 +1,12 @@
"use client"
// Caminho: app/(finance)/login/page.tsx
import type React from "react"
import { LoginForm } from "@/components/LoginForm";
import { useState } from "react"
import { useRouter } from "next/navigation"
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 { useToast } from "@/hooks/use-toast"
import { Eye, EyeOff, Mail, Lock, Stethoscope, Loader2, Receipt } from "lucide-react"
import Link from "next/link"
interface LoginForm {
email: string
password: string
}
export default function DoctorLogin() {
const [form, setForm] = useState<LoginForm>({ email: "", password: "" })
const [showPassword, setShowPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const { toast } = useToast()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
// Simular autenticação
setTimeout(() => {
if (form.email && form.password) {
const financierData = {
id: "1",
name: "Thiago Nigro",
email: form.email,
phone: "(11) 98888-8888",
cpf: "987.654.321-00",
department: "Financeiro",
permissions: ["view_reports", "manage_finances", "create_reports"],
}
localStorage.setItem("financierData", JSON.stringify(financierData))
localStorage.setItem("userType", "financier")
toast({
title: "Login realizado com sucesso!",
description: "Bem-vindo ao sistema, " + financierData.name,
})
router.push("/finance/home")
} else {
toast({
title: "Erro no login",
description: "Por favor, preencha todos os campos.",
variant: "destructive",
})
}
setIsLoading(false)
}, 1500)
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-xl border-0 bg-card/80 backdrop-blur-sm">
<CardHeader className="text-center space-y-4 pb-8">
<div className="mx-auto w-16 h-16 bg-orange-100 dark:bg-orange-900/30 rounded-full flex items-center justify-center">
<Receipt className="w-8 h-8 text-orange-600 dark:text-orange-400" />
</div>
<div>
<CardTitle className="text-2xl font-bold text-foreground">Área Financeira</CardTitle>
<CardDescription className="text-muted-foreground mt-2">Acesse o sistema financeiro</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium text-foreground">
E-mail
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
id="email"
type="email"
placeholder="financeiro@clinica.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="pl-10 h-11 border-border focus:border-orange-500 focus:ring-orange-500"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm font-medium text-foreground">
Senha
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Digite sua senha"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="pl-10 pr-10 h-11 border-border focus:border-orange-500 focus:ring-orange-500"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<Button
type="submit"
className="w-full h-11 bg-orange-600 text-white hover:bg-orange-700 dark:bg-orange-500 dark:hover:bg-orange-600 dark:text-white"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Entrando...
</>
) : (
"Entrar"
)}
</Button>
</form>
<div className="relative">
<Separator className="my-6" />
<span className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card px-3 text-sm text-muted-foreground">
ou
</span>
</div>
<div className="text-center">
<Link href="/" className="text-sm text-orange-600 hover:text-orange-700 dark:text-orange-400 dark:hover:text-orange-500 font-medium hover:underline">
Voltar à página inicial
</Link>
</div>
</CardContent>
</Card>
</div>
)
export default function FinanceLoginPage() {
return (
// Fundo com gradiente laranja, como no seu código original
<div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-orange-50 flex items-center justify-center p-4">
<LoginForm title="Área Financeira" description="Acesse o sistema de faturamento" role="finance" themeColor="orange" redirectPath="/finance/home" />
</div>
);
}

View File

@ -1,36 +1,32 @@
import type { Metadata } from 'next'
import { GeistSans } from 'geist/font/sans'
import { GeistMono } from 'geist/font/mono'
import { Analytics } from '@vercel/analytics/next'
import './globals.css'
import { AppointmentsProvider } from './context/AppointmentsContext'
import { AccessibilityProvider } from './context/AccessibilityContext'
import { AccessibilityModal } from '@/components/accessibility-modal'
import { ThemeInitializer } from '@/components/theme-initializer'
export const metadata: Metadata = {
title: 'Clinic App',
description: 'Created with v0',
generator: 'v0.app',
}
import type { Metadata } from "next";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { Analytics } from "@vercel/analytics/next";
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,
children,
}: Readonly<{
children: React.ReactNode
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`font-sans ${GeistSans.variable} ${GeistMono.variable}`}>
<ThemeInitializer />
<AccessibilityProvider>
<AppointmentsProvider>
{children}
</AppointmentsProvider>
<AccessibilityModal />
</AccessibilityProvider>
<Analytics />
</body>
</html>
)
}
return (
<html lang="en" suppressHydrationWarning>
<body className={`font-sans ${GeistSans.variable} ${GeistMono.variable}`}>
{/* [PASSO 1.2] - Envolvendo a aplicação com o provider */}
<ThemeInitializer />
<AccessibilityProvider>
<AppointmentsProvider>{children}</AppointmentsProvider>
<AccessibilityModal />
</AccessibilityProvider>
<Analytics />
<Toaster />
</body>
</html>
);
}

View File

@ -1,154 +1,12 @@
"use client"
// Caminho: app/(manager)/login/page.tsx
import type React from "react"
import { LoginForm } from "@/components/LoginForm";
import { useState } from "react"
import { useRouter } from "next/navigation"
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 { useToast } from "@/hooks/use-toast"
import { Eye, EyeOff, Mail, Lock, Stethoscope, Loader2, IdCard } from "lucide-react"
import Link from "next/link"
interface LoginForm {
email: string
password: string
}
export default function ManagerLogin() {
const [form, setForm] = useState<LoginForm>({ email: "", password: "" })
const [showPassword, setShowPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const { toast } = useToast()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setTimeout(() => {
if (form.email && form.password) {
const managerData = {
id: "1",
name: "Arthur Cavalcante",
email: form.email,
phone: "(11) 98888-8888",
cpf: "987.654.321-00",
department: "Gerente",
permissions: ["manage_user", "manage_doctors", "create_reports"],
}
localStorage.setItem("managerData", JSON.stringify(managerData))
localStorage.setItem("userType", "manager")
toast({
title: "Login realizado com sucesso!",
description: "Bem-vindo ao sistema, " + managerData.name,
})
router.push("/manager/home")
} else {
toast({
title: "Erro no login",
description: "Por favor, preencha todos os campos.",
variant: "destructive",
})
}
setIsLoading(false)
}, 1500)
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-xl border-0 bg-card/80 backdrop-blur-sm">
<CardHeader className="text-center space-y-4 pb-8">
<div className="mx-auto w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
<IdCard className="w-8 h-8 text-blue-600 dark:text-blue-400" />
</div>
<div>
<CardTitle className="text-2xl font-bold text-foreground">Área do Gestor</CardTitle>
<CardDescription className="text-muted-foreground mt-2">Acesse o sistema de gerenciamento</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium text-foreground">
E-mail
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
id="email"
type="email"
placeholder="gestor@clinica.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="pl-10 h-11 border-border focus:border-blue-500 focus:ring-blue-500"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm font-medium text-foreground">
Senha
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Digite sua senha"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="pl-10 pr-10 h-11 border-border focus:border-blue-500 focus:ring-blue-500"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<Button
type="submit"
className="w-full h-11 bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 dark:text-white"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Entrando...
</>
) : (
"Entrar"
)}
</Button>
</form>
<div className="relative">
<Separator className="my-6" />
<span className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card px-3 text-sm text-muted-foreground">
ou
</span>
</div>
<div className="text-center">
<Link href="/" className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-500 font-medium hover:underline">
Voltar à página inicial
</Link>
</div>
</CardContent>
</Card>
</div>
)
export default function ManagerLoginPage() {
return (
// Mantemos o seu plano de fundo original
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4">
<LoginForm title="Área do Gestor" description="Acesse o sistema médico" role="manager" themeColor="blue" redirectPath="/manager/home" />
</div>
);
}

View File

@ -1,164 +1,35 @@
"use client"
// Caminho: app/(patient)/login/page.tsx
import type React from "react"
import Link from "next/link";
import { LoginForm } from "@/components/LoginForm";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Eye, EyeOff, ArrowLeft, Stethoscope, Mail, Lock } from "lucide-react"
export default function PatientLoginPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex flex-col items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="mb-8">
<Link href="/" className="inline-flex items-center text-muted-foreground hover:text-primary transition-colors duration-200 font-medium">
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar ao início
</Link>
</div>
export default function PatientLogin() {
const [showPassword, setShowPassword] = useState(false)
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
<LoginForm title="Área do Paciente" description="Acesse sua conta para gerenciar consultas" role="patient" themeColor="blue" redirectPath="/patient/dashboard">
{/* Este bloco é passado como 'children' para o LoginForm */}
<Link href="/patient/register" passHref>
<Button variant="outline" className="w-full h-12 text-base">
Criar nova conta
</Button>
</Link>
</LoginForm>
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
// Simulação de login - em produção, conectar com API real
setTimeout(() => {
if (email && password) {
// Salvar dados do usuário no localStorage para simulação
localStorage.setItem(
"patientData",
JSON.stringify({
name: "João Silva",
email: email,
phone: "(11) 99999-9999",
cpf: "123.456.789-00",
birthDate: "1990-01-01",
address: "Rua das Flores, 123 - São Paulo, SP",
}),
)
router.push("/patient/dashboard")
}
setIsLoading(false)
}, 1000)
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="mb-8">
<Link
href="/"
className="inline-flex items-center text-muted-foreground hover:text-primary transition-colors duration-200 font-medium"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar ao início
</Link>
</div>
<Card className="shadow-xl border-0 bg-card/80 backdrop-blur-sm">
<CardHeader className="text-center pb-8">
<div className="mx-auto w-16 h-16 bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center mb-4">
<Stethoscope className="w-8 h-8 text-blue-600 dark:text-blue-400" />
{/* Conteúdo e espaçamento restaurados */}
<div className="mt-8 text-center">
<p className="text-sm text-muted-foreground">Problemas para acessar? Entre em contato conosco</p>
</div>
</div>
<CardTitle className="text-3xl font-bold text-foreground mb-2">Área do Paciente</CardTitle>
<CardDescription className="text-muted-foreground text-base">
Acesse sua conta para gerenciar consultas e laudos
</CardDescription>
</CardHeader>
<CardContent className="px-8 pb-8">
<form onSubmit={handleLogin} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="email" className="text-foreground font-medium">
Email
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-5 h-5" />
<Input
id="email"
type="email"
placeholder="seu@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="pl-11 h-12 border-border focus:border-blue-500 focus:ring-blue-500"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-foreground font-medium">
Senha
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-5 h-5" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Sua senha"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="pl-11 pr-12 h-12 border-border focus:border-blue-500 focus:ring-blue-500"
required
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-2 top-1/2 transform -translate-y-1/2 h-8 w-8 p-0 hover:bg-accent"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
) : (
<Eye className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
</div>
<Button
type="submit"
className="w-full h-12 bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 dark:text-white font-semibold rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50"
disabled={isLoading}
>
{isLoading ? (
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Entrando...
</div>
) : (
"Entrar na minha conta"
)}
</Button>
</form>
<div className="mt-8 text-center">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-card text-muted-foreground">Novo por aqui?</span>
</div>
</div>
<div className="mt-4">
<Link
href="/patient/register"
className="inline-flex items-center justify-center w-full h-12 border border-border rounded-lg text-foreground hover:bg-accent transition-colors duration-200 font-medium"
>
Criar nova conta
</Link>
</div>
</div>
</CardContent>
</Card>
<div className="mt-8 text-center">
<p className="text-sm text-muted-foreground">Problemas para acessar? Entre em contato conosco</p>
</div>
</div>
</div>
)
);
}

View File

@ -1,157 +1,11 @@
"use client"
// Caminho: app/(secretary)/login/page.tsx
import type React from "react"
import { LoginForm } from "@/components/LoginForm";
import { useState } from "react"
import { useRouter } from "next/navigation"
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 { useToast } from "@/hooks/use-toast"
import { Eye, EyeOff, Mail, Lock, UserCheck, Loader2 } from "lucide-react"
import Link from "next/link"
interface LoginForm {
email: string
password: string
}
export default function SecretaryLogin() {
const [form, setForm] = useState<LoginForm>({ email: "", password: "" })
const [showPassword, setShowPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const { toast } = useToast()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
// Simular autenticação
setTimeout(() => {
if (form.email && form.password) {
// Simular dados da secretária
const secretaryData = {
id: "1",
name: "Maria Silva",
email: form.email,
phone: "(11) 99999-9999",
cpf: "123.456.789-00",
employeeId: "SEC001",
department: "Recepção",
permissions: ["manage_appointments", "view_patients", "manage_schedule"],
}
localStorage.setItem("secretaryData", JSON.stringify(secretaryData))
localStorage.setItem("userType", "secretary")
toast({
title: "Login realizado com sucesso!",
description: "Bem-vinda ao sistema, " + secretaryData.name,
})
router.push("/secretary/pacientes") // direciona para a página
} else {
toast({
title: "Erro no login",
description: "Por favor, preencha todos os campos.",
variant: "destructive",
})
}
setIsLoading(false)
}, 1500)
}
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-xl border-0 bg-card/80 backdrop-blur-sm">
<CardHeader className="text-center space-y-4 pb-8">
<div className="mx-auto w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center">
<UserCheck className="w-8 h-8 text-purple-600 dark:text-purple-400" />
</div>
<div>
<CardTitle className="text-2xl font-bold text-foreground">Área da Secretária</CardTitle>
<CardDescription className="text-muted-foreground mt-2">Acesse o sistema de gerenciamento</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-medium text-foreground">
E-mail
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
id="email"
type="email"
placeholder="seu.email@clinica.com"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="pl-10 h-11 border-border focus:border-purple-500 focus:ring-purple-500"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-sm font-medium text-foreground">
Senha
</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Digite sua senha"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="pl-10 pr-10 h-11 border-border focus:border-purple-500 focus:ring-purple-500"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<Button
type="submit"
className="w-full h-11 bg-purple-600 text-white hover:bg-purple-700 dark:bg-purple-500 dark:hover:bg-purple-600 dark:text-white"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Entrando...
</>
) : (
"Entrar"
)}
</Button>
</form>
<div className="relative">
<Separator className="my-6" />
<span className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-card px-3 text-sm text-muted-foreground">
ou
</span>
</div>
<div className="text-center">
<Link href="/" className="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-500 font-medium hover:underline">
Voltar à página inicial
</Link>
</div>
</CardContent>
</Card>
</div>
)
export default function SecretaryLoginPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-blue-50 flex items-center justify-center p-4">
<LoginForm title="Área da Secretária" description="Acesse o sistema de gerenciamento" role="secretary" themeColor="blue" redirectPath="/secretary/pacientes" />
</div>
);
}

223
components/LoginForm.tsx Normal file
View File

@ -0,0 +1,223 @@
// Caminho: components/LoginForm.tsx
"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";
// 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 { apikey } from "@/services/api.mjs";
// Hook customizado
import { useToast } from "@/hooks/use-toast";
// Ícones
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;
}
interface FormState {
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
}
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<FormState>({ 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 (
<Card className="w-full max-w-md shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<CardHeader className="text-center space-y-4 pb-8">
<div className={cn("mx-auto w-16 h-16 rounded-full flex items-center justify-center", currentTheme.iconBg)}>
<Icon className={cn("w-8 h-8", currentTheme.iconText)} />
</div>
<div>
<CardTitle className="text-2xl font-bold text-gray-900">{title}</CardTitle>
<CardDescription className="text-gray-600 mt-2">{description}</CardDescription>
</div>
</CardHeader>
<CardContent className="px-8 pb-8">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Inputs e Botão */}
<div className="space-y-2">
<Label htmlFor="email">E-mail</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input id="email" type="email" placeholder="seu.email@clinica.com" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} className={cn("pl-11 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Senha</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input id="password" type={showPassword ? "text" : "password"} placeholder="Digite sua senha" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} className={cn("pl-11 pr-12 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} />
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-2 top-1/2 -translate-y-1/2 h-8 w-8 p-0 text-gray-400 hover:text-gray-600" disabled={isLoading}>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<Button type="submit" className={cn("w-full h-12 text-base font-semibold", currentTheme.button)} disabled={isLoading}>
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : "Entrar"}
</Button>
</form>
{/* Conteúdo Extra (children) */}
<div className="mt-8">
{children ? (
<div className="space-y-4">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-white text-slate-500">Novo por aqui?</span>
</div>
</div>
{children}
</div>
) : (
<>
<div className="relative">
<Separator className="my-6" />
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white px-3 text-sm text-gray-500">ou</span>
</div>
<div className="text-center">
<Link href="/" className={cn("text-sm font-medium hover:underline", currentTheme.link)}>
Voltar à página inicial
</Link>
</div>
</>
)}
</div>
</CardContent>
</Card>
);
}

View File

@ -5,6 +5,7 @@ import { useState, useEffect } from "react"
import Link from "next/link"
import { useRouter, usePathname } from "next/navigation"
import { Button } from "@/components/ui/button"
import Cookies from "js-cookie"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
@ -66,14 +67,31 @@ export default function HospitalLayout({ children }: HospitalLayoutProps) {
}, [])
useEffect(() => {
const data = localStorage.getItem("patientData")
if (data) {
setPatientData(JSON.parse(data))
} else {
router.push("/patient/login")
}
}, [router])
// 1. Procuramos pela chave correta: 'user_info'
const userInfoString = localStorage.getItem("user_info");
// 2. Para mais segurança, verificamos também se o token de acesso existe no cookie
const token = Cookies.get("access_token");
if (userInfoString && token) {
const userInfo = JSON.parse(userInfoString);
// 3. Adaptamos os dados para a estrutura que seu layout espera (PatientData)
// Usamos os dados do objeto 'user' que a API do Supabase nos deu
setPatientData({
name: userInfo.user_metadata?.full_name || "Paciente",
email: userInfo.email || "",
// Os campos abaixo não vêm do login, então os deixamos vazios por enquanto
phone: userInfo.phone || "",
cpf: "",
birthDate: "",
address: "",
});
} else {
// Se as informações do usuário ou o token não forem encontrados, mandamos para o login.
router.push("/patient/login");
}
}, [router]);
const handleLogout = () => setShowLogoutDialog(true)
const confirmLogout = () => {

View File

@ -5,8 +5,8 @@ import {
} from "@/components/ui/dialog";
interface PatientDetailsModalProps {
patient: any;
isOpen: boolean;
patient: any;
onClose: () => void;
}

116
package-lock.json generated
View File

@ -50,8 +50,10 @@
"embla-carousel-react": "8.5.1",
"geist": "^1.3.1",
"input-otp": "1.4.1",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.454.0",
"next": "14.2.16",
"next": "^14.2.33",
"next-themes": "^0.4.6",
"react": "^18",
"react-day-picker": "^9.8.0",
@ -67,6 +69,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.9",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
@ -215,15 +218,15 @@
}
},
"node_modules/@next/env": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.16.tgz",
"integrity": "sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.33.tgz",
"integrity": "sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.16.tgz",
"integrity": "sha512-uFT34QojYkf0+nn6MEZ4gIWQ5aqGF11uIZ1HSxG+cSbj+Mg3+tYm8qXYd3dKN5jqKUm5rBVvf1PBRO/MeQ6rxw==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
"integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
"cpu": [
"arm64"
],
@ -237,9 +240,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.16.tgz",
"integrity": "sha512-mCecsFkYezem0QiZlg2bau3Xul77VxUD38b/auAjohMA22G9KTJneUYMv78vWoCCFkleFAhY1NIvbyjj1ncG9g==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
"integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
"cpu": [
"x64"
],
@ -253,9 +256,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.16.tgz",
"integrity": "sha512-yhkNA36+ECTC91KSyZcgWgKrYIyDnXZj8PqtJ+c2pMvj45xf7y/HrgI17hLdrcYamLfVt7pBaJUMxADtPaczHA==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
"integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
"cpu": [
"arm64"
],
@ -269,9 +272,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.16.tgz",
"integrity": "sha512-X2YSyu5RMys8R2lA0yLMCOCtqFOoLxrq2YbazFvcPOE4i/isubYjkh+JCpRmqYfEuCVltvlo+oGfj/b5T2pKUA==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
"integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
"cpu": [
"arm64"
],
@ -285,9 +288,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.16.tgz",
"integrity": "sha512-9AGcX7VAkGbc5zTSa+bjQ757tkjr6C/pKS7OK8cX7QEiK6MHIIezBLcQ7gQqbDW2k5yaqba2aDtaBeyyZh1i6Q==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
"integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
"cpu": [
"x64"
],
@ -301,9 +304,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.16.tgz",
"integrity": "sha512-Klgeagrdun4WWDaOizdbtIIm8khUDQJ/5cRzdpXHfkbY91LxBXeejL4kbZBrpR/nmgRrQvmz4l3OtttNVkz2Sg==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
"integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
"cpu": [
"x64"
],
@ -317,9 +320,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.16.tgz",
"integrity": "sha512-PwW8A1UC1Y0xIm83G3yFGPiOBftJK4zukTmk7DI1CebyMOoaVpd8aSy7K6GhobzhkjYvqS/QmzcfsWG2Dwizdg==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
"integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
"cpu": [
"arm64"
],
@ -333,9 +336,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.16.tgz",
"integrity": "sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [
"ia32"
],
@ -349,9 +352,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.16.tgz",
"integrity": "sha512-OA7NtfxgirCjfqt+02BqxC3MIgM/JaGjw9tOe4fyZgPsqfseNiMPnCRP44Pfs+Gpo9zPN+SXaFsgP6vk8d571A==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
"integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
"cpu": [
"x64"
],
@ -2463,6 +2466,13 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
@ -3080,12 +3090,30 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/lightningcss": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
@ -3452,12 +3480,12 @@
}
},
"node_modules/next": {
"version": "14.2.16",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.16.tgz",
"integrity": "sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==",
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.33.tgz",
"integrity": "sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==",
"license": "MIT",
"dependencies": {
"@next/env": "14.2.16",
"@next/env": "14.2.33",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
@ -3472,15 +3500,15 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.16",
"@next/swc-darwin-x64": "14.2.16",
"@next/swc-linux-arm64-gnu": "14.2.16",
"@next/swc-linux-arm64-musl": "14.2.16",
"@next/swc-linux-x64-gnu": "14.2.16",
"@next/swc-linux-x64-musl": "14.2.16",
"@next/swc-win32-arm64-msvc": "14.2.16",
"@next/swc-win32-ia32-msvc": "14.2.16",
"@next/swc-win32-x64-msvc": "14.2.16"
"@next/swc-darwin-arm64": "14.2.33",
"@next/swc-darwin-x64": "14.2.33",
"@next/swc-linux-arm64-gnu": "14.2.33",
"@next/swc-linux-arm64-musl": "14.2.33",
"@next/swc-linux-x64-gnu": "14.2.33",
"@next/swc-linux-x64-musl": "14.2.33",
"@next/swc-win32-arm64-msvc": "14.2.33",
"@next/swc-win32-ia32-msvc": "14.2.33",
"@next/swc-win32-x64-msvc": "14.2.33"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",

View File

@ -51,8 +51,10 @@
"embla-carousel-react": "8.5.1",
"geist": "^1.3.1",
"input-otp": "1.4.1",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.454.0",
"next": "14.2.16",
"next": "^14.2.33",
"next-themes": "^0.4.6",
"react": "^18",
"react-day-picker": "^9.8.0",
@ -68,6 +70,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.9",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",

View File

@ -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" }),
};
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" }),
};

View File

@ -1,125 +0,0 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}