refactor: migrate from ESLint to Biome and extract SQL queries to data.ts

- Replace ESLint with Biome for linting and formatting
- Configure Biome with tabs, double quotes, and organized imports
- Move all SQL/Drizzle queries from page.tsx files to data.ts files
- Create new data.ts files for: ajustes, dashboard, relatorios/categorias
- Update existing data.ts files: extrato, fatura (add lancamentos queries)
- Remove all drizzle-orm imports from page.tsx files
- Update README.md with new tooling info

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-01-27 13:15:37 +00:00
parent 8ffe61c59b
commit a7f63fb77a
442 changed files with 66141 additions and 69292 deletions

View File

@@ -1,363 +1,365 @@
"use client";
import {
RiAlertLine,
RiCheckLine,
RiCloseLine,
RiEyeLine,
RiEyeOffLine,
} from "@remixicon/react";
import { useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { updatePasswordAction } from "@/app/(dashboard)/ajustes/actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils/ui";
import {
RiEyeLine,
RiEyeOffLine,
RiCheckLine,
RiCloseLine,
RiAlertLine,
} from "@remixicon/react";
import { useState, useTransition, useMemo } from "react";
import { toast } from "sonner";
interface PasswordValidation {
hasLowercase: boolean;
hasUppercase: boolean;
hasNumber: boolean;
hasSpecial: boolean;
hasMinLength: boolean;
hasMaxLength: boolean;
isValid: boolean;
hasLowercase: boolean;
hasUppercase: boolean;
hasNumber: boolean;
hasSpecial: boolean;
hasMinLength: boolean;
hasMaxLength: boolean;
isValid: boolean;
}
function validatePassword(password: string): PasswordValidation {
const hasLowercase = /[a-z]/.test(password);
const hasUppercase = /[A-Z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?`~]/.test(password);
const hasMinLength = password.length >= 7;
const hasMaxLength = password.length <= 23;
const hasLowercase = /[a-z]/.test(password);
const hasUppercase = /[A-Z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecial = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/.test(password);
const hasMinLength = password.length >= 7;
const hasMaxLength = password.length <= 23;
return {
hasLowercase,
hasUppercase,
hasNumber,
hasSpecial,
hasMinLength,
hasMaxLength,
isValid:
hasLowercase &&
hasUppercase &&
hasNumber &&
hasSpecial &&
hasMinLength &&
hasMaxLength,
};
return {
hasLowercase,
hasUppercase,
hasNumber,
hasSpecial,
hasMinLength,
hasMaxLength,
isValid:
hasLowercase &&
hasUppercase &&
hasNumber &&
hasSpecial &&
hasMinLength &&
hasMaxLength,
};
}
function PasswordRequirement({ met, label }: { met: boolean; label: string }) {
return (
<div
className={cn(
"flex items-center gap-1.5 text-xs transition-colors",
met ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground"
)}
>
{met ? (
<RiCheckLine className="h-3.5 w-3.5" />
) : (
<RiCloseLine className="h-3.5 w-3.5" />
)}
<span>{label}</span>
</div>
);
return (
<div
className={cn(
"flex items-center gap-1.5 text-xs transition-colors",
met
? "text-emerald-600 dark:text-emerald-400"
: "text-muted-foreground",
)}
>
{met ? (
<RiCheckLine className="h-3.5 w-3.5" />
) : (
<RiCloseLine className="h-3.5 w-3.5" />
)}
<span>{label}</span>
</div>
);
}
type UpdatePasswordFormProps = {
authProvider?: string; // 'google' | 'credential' | undefined
authProvider?: string; // 'google' | 'credential' | undefined
};
export function UpdatePasswordForm({ authProvider }: UpdatePasswordFormProps) {
const [isPending, startTransition] = useTransition();
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isPending, startTransition] = useTransition();
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
// Verificar se o usuário usa login via Google
const isGoogleAuth = authProvider === "google";
// Verificar se o usuário usa login via Google
const isGoogleAuth = authProvider === "google";
// Validação em tempo real: senhas coincidem
const passwordsMatch = useMemo(() => {
if (!confirmPassword) return null; // Não mostrar erro se campo vazio
return newPassword === confirmPassword;
}, [newPassword, confirmPassword]);
// Validação em tempo real: senhas coincidem
const passwordsMatch = useMemo(() => {
if (!confirmPassword) return null; // Não mostrar erro se campo vazio
return newPassword === confirmPassword;
}, [newPassword, confirmPassword]);
// Validação de requisitos da senha
const passwordValidation = useMemo(
() => validatePassword(newPassword),
[newPassword]
);
// Validação de requisitos da senha
const passwordValidation = useMemo(
() => validatePassword(newPassword),
[newPassword],
);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validação frontend antes de enviar
if (!passwordValidation.isValid) {
toast.error("A senha não atende aos requisitos de segurança");
return;
}
// Validação frontend antes de enviar
if (!passwordValidation.isValid) {
toast.error("A senha não atende aos requisitos de segurança");
return;
}
if (newPassword !== confirmPassword) {
toast.error("As senhas não coincidem");
return;
}
if (newPassword !== confirmPassword) {
toast.error("As senhas não coincidem");
return;
}
startTransition(async () => {
const result = await updatePasswordAction({
currentPassword,
newPassword,
confirmPassword,
});
startTransition(async () => {
const result = await updatePasswordAction({
currentPassword,
newPassword,
confirmPassword,
});
if (result.success) {
toast.success(result.message);
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} else {
toast.error(result.error);
}
});
};
if (result.success) {
toast.success(result.message);
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} else {
toast.error(result.error);
}
});
};
// Se o usuário usa Google OAuth, mostrar aviso
if (isGoogleAuth) {
return (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-900 dark:bg-amber-950/20">
<div className="flex gap-3">
<RiAlertLine className="h-5 w-5 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" />
<div>
<h3 className="font-medium text-amber-900 dark:text-amber-400">
Alteração de senha não disponível
</h3>
<p className="mt-1 text-sm text-amber-800 dark:text-amber-500">
Você fez login usando sua conta do Google. A senha é gerenciada
diretamente pelo Google e não pode ser alterada aqui. Para
modificar sua senha, acesse as configurações de segurança da sua
conta Google.
</p>
</div>
</div>
</div>
);
}
// Se o usuário usa Google OAuth, mostrar aviso
if (isGoogleAuth) {
return (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-900 dark:bg-amber-950/20">
<div className="flex gap-3">
<RiAlertLine className="h-5 w-5 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" />
<div>
<h3 className="font-medium text-amber-900 dark:text-amber-400">
Alteração de senha não disponível
</h3>
<p className="mt-1 text-sm text-amber-800 dark:text-amber-500">
Você fez login usando sua conta do Google. A senha é gerenciada
diretamente pelo Google e não pode ser alterada aqui. Para
modificar sua senha, acesse as configurações de segurança da sua
conta Google.
</p>
</div>
</div>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="flex flex-col space-y-6">
<div className="space-y-4 max-w-md">
{/* Senha atual */}
<div className="space-y-2">
<Label htmlFor="currentPassword">
Senha atual <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Input
id="currentPassword"
type={showCurrentPassword ? "text" : "password"}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
disabled={isPending}
placeholder="Digite sua senha atual"
required
aria-required="true"
aria-describedby="current-password-help"
/>
<button
type="button"
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={
showCurrentPassword
? "Ocultar senha atual"
: "Mostrar senha atual"
}
>
{showCurrentPassword ? (
<RiEyeOffLine size={20} />
) : (
<RiEyeLine size={20} />
)}
</button>
</div>
<p
id="current-password-help"
className="text-xs text-muted-foreground"
>
Por segurança, confirme sua senha atual antes de alterá-la
</p>
</div>
return (
<form onSubmit={handleSubmit} className="flex flex-col space-y-6">
<div className="space-y-4 max-w-md">
{/* Senha atual */}
<div className="space-y-2">
<Label htmlFor="currentPassword">
Senha atual <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Input
id="currentPassword"
type={showCurrentPassword ? "text" : "password"}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
disabled={isPending}
placeholder="Digite sua senha atual"
required
aria-required="true"
aria-describedby="current-password-help"
/>
<button
type="button"
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={
showCurrentPassword
? "Ocultar senha atual"
: "Mostrar senha atual"
}
>
{showCurrentPassword ? (
<RiEyeOffLine size={20} />
) : (
<RiEyeLine size={20} />
)}
</button>
</div>
<p
id="current-password-help"
className="text-xs text-muted-foreground"
>
Por segurança, confirme sua senha atual antes de alterá-la
</p>
</div>
{/* Nova senha */}
<div className="space-y-2">
<Label htmlFor="newPassword">
Nova senha <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Input
id="newPassword"
type={showNewPassword ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
disabled={isPending}
placeholder="Crie uma senha forte"
required
minLength={7}
maxLength={23}
aria-required="true"
aria-describedby="new-password-help"
aria-invalid={
newPassword.length > 0 && !passwordValidation.isValid
}
/>
<button
type="button"
onClick={() => setShowNewPassword(!showNewPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={
showNewPassword ? "Ocultar nova senha" : "Mostrar nova senha"
}
>
{showNewPassword ? (
<RiEyeOffLine size={20} />
) : (
<RiEyeLine size={20} />
)}
</button>
</div>
{/* Indicadores de requisitos da senha */}
{newPassword.length > 0 && (
<div className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<PasswordRequirement
met={passwordValidation.hasMinLength}
label="Mínimo 7 caracteres"
/>
<PasswordRequirement
met={passwordValidation.hasMaxLength}
label="Máximo 23 caracteres"
/>
<PasswordRequirement
met={passwordValidation.hasLowercase}
label="Letra minúscula"
/>
<PasswordRequirement
met={passwordValidation.hasUppercase}
label="Letra maiúscula"
/>
<PasswordRequirement
met={passwordValidation.hasNumber}
label="Número"
/>
<PasswordRequirement
met={passwordValidation.hasSpecial}
label="Caractere especial"
/>
</div>
)}
</div>
{/* Nova senha */}
<div className="space-y-2">
<Label htmlFor="newPassword">
Nova senha <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Input
id="newPassword"
type={showNewPassword ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
disabled={isPending}
placeholder="Crie uma senha forte"
required
minLength={7}
maxLength={23}
aria-required="true"
aria-describedby="new-password-help"
aria-invalid={
newPassword.length > 0 && !passwordValidation.isValid
}
/>
<button
type="button"
onClick={() => setShowNewPassword(!showNewPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={
showNewPassword ? "Ocultar nova senha" : "Mostrar nova senha"
}
>
{showNewPassword ? (
<RiEyeOffLine size={20} />
) : (
<RiEyeLine size={20} />
)}
</button>
</div>
{/* Indicadores de requisitos da senha */}
{newPassword.length > 0 && (
<div className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1">
<PasswordRequirement
met={passwordValidation.hasMinLength}
label="Mínimo 7 caracteres"
/>
<PasswordRequirement
met={passwordValidation.hasMaxLength}
label="Máximo 23 caracteres"
/>
<PasswordRequirement
met={passwordValidation.hasLowercase}
label="Letra minúscula"
/>
<PasswordRequirement
met={passwordValidation.hasUppercase}
label="Letra maiúscula"
/>
<PasswordRequirement
met={passwordValidation.hasNumber}
label="Número"
/>
<PasswordRequirement
met={passwordValidation.hasSpecial}
label="Caractere especial"
/>
</div>
)}
</div>
{/* Confirmar nova senha */}
<div className="space-y-2">
<Label htmlFor="confirmPassword">
Confirmar nova senha <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
disabled={isPending}
placeholder="Repita a senha"
required
minLength={6}
aria-required="true"
aria-describedby="confirm-password-help"
aria-invalid={passwordsMatch === false}
className={
passwordsMatch === false
? "border-red-500 focus-visible:ring-red-500"
: passwordsMatch === true
? "border-green-500 focus-visible:ring-green-500"
: ""
}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-10 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={
showConfirmPassword
? "Ocultar confirmação de senha"
: "Mostrar confirmação de senha"
}
>
{showConfirmPassword ? (
<RiEyeOffLine size={20} />
) : (
<RiEyeLine size={20} />
)}
</button>
{/* Indicador visual de match */}
{passwordsMatch !== null && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
{passwordsMatch ? (
<RiCheckLine
className="h-5 w-5 text-green-500"
aria-label="As senhas coincidem"
/>
) : (
<RiCloseLine
className="h-5 w-5 text-red-500"
aria-label="As senhas não coincidem"
/>
)}
</div>
)}
</div>
{/* Mensagem de erro em tempo real */}
{passwordsMatch === false && (
<p
id="confirm-password-help"
className="text-xs text-red-600 dark:text-red-400 flex items-center gap-1"
role="alert"
>
<RiCloseLine className="h-3.5 w-3.5" />
As senhas não coincidem
</p>
)}
{passwordsMatch === true && (
<p
id="confirm-password-help"
className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1"
>
<RiCheckLine className="h-3.5 w-3.5" />
As senhas coincidem
</p>
)}
</div>
</div>
{/* Confirmar nova senha */}
<div className="space-y-2">
<Label htmlFor="confirmPassword">
Confirmar nova senha <span className="text-destructive">*</span>
</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
disabled={isPending}
placeholder="Repita a senha"
required
minLength={6}
aria-required="true"
aria-describedby="confirm-password-help"
aria-invalid={passwordsMatch === false}
className={
passwordsMatch === false
? "border-red-500 focus-visible:ring-red-500"
: passwordsMatch === true
? "border-green-500 focus-visible:ring-green-500"
: ""
}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-10 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label={
showConfirmPassword
? "Ocultar confirmação de senha"
: "Mostrar confirmação de senha"
}
>
{showConfirmPassword ? (
<RiEyeOffLine size={20} />
) : (
<RiEyeLine size={20} />
)}
</button>
{/* Indicador visual de match */}
{passwordsMatch !== null && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
{passwordsMatch ? (
<RiCheckLine
className="h-5 w-5 text-green-500"
aria-label="As senhas coincidem"
/>
) : (
<RiCloseLine
className="h-5 w-5 text-red-500"
aria-label="As senhas não coincidem"
/>
)}
</div>
)}
</div>
{/* Mensagem de erro em tempo real */}
{passwordsMatch === false && (
<p
id="confirm-password-help"
className="text-xs text-red-600 dark:text-red-400 flex items-center gap-1"
role="alert"
>
<RiCloseLine className="h-3.5 w-3.5" />
As senhas não coincidem
</p>
)}
{passwordsMatch === true && (
<p
id="confirm-password-help"
className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1"
>
<RiCheckLine className="h-3.5 w-3.5" />
As senhas coincidem
</p>
)}
</div>
</div>
<div className="flex justify-end">
<Button
type="submit"
disabled={
isPending ||
passwordsMatch === false ||
(newPassword.length > 0 && !passwordValidation.isValid)
}
className="w-fit"
>
{isPending ? "Atualizando..." : "Atualizar senha"}
</Button>
</div>
</form>
);
<div className="flex justify-end">
<Button
type="submit"
disabled={
isPending ||
passwordsMatch === false ||
(newPassword.length > 0 && !passwordValidation.isValid)
}
className="w-fit"
>
{isPending ? "Atualizando..." : "Atualizar senha"}
</Button>
</div>
</form>
);
}