Novo layout

This commit is contained in:
Marco Santos
2025-07-24 00:07:59 +01:00
parent 11e532ac9a
commit dec8bd3909
24 changed files with 519 additions and 425 deletions

View File

@@ -1,111 +1,139 @@
import React, { useState } from "react";
import React, { useState } from "react";
import { RefreshCwIcon, CopyIcon } from "lucide-react";
import { Button } from "../ui/Button";
import { Input } from "../ui/Input";
import { Select } from "../ui/Select"; // Import do Select moderno
import { Select } from "../ui/Select";
import { ResultDisplay } from "../ui/ResultDisplay";
import { ButtonGroup } from "../ui/ButtonGroup";
import { toast } from 'react-hot-toast';
type Parametro =
| { nome: string; tipo: "text" | "number" | "password"; placeholder?: string }
| { nome: string; tipo: "select"; options: { label: string; value: string }[] };
type Parametro =
| { nome: string; tipo: "text" | "number" | "password"; placeholder?: string }
| { nome: string; tipo: "select"; options: { label: string; value: string }[] };
type GeradorCardProps = {
titulo: string;
descricao?: string;
parametros?: Parametro[];
onGerar: (parametros?: Record<string, string>) => string | Promise<string>;
onValidar?: (valor: string) => boolean | Promise<boolean>;
readonly titulo: string;
readonly descricao?: string;
readonly parametros?: Parametro[];
readonly onGerar: (parametros?: Record<string, string>) => string | Promise<string>;
readonly onValidar?: (valor: string) => boolean | Promise<boolean>;
};
export default function GeradorCard({
titulo,
descricao,
parametros = [],
onGerar,
onValidar,
titulo,
descricao,
parametros = [],
onGerar,
onValidar,
}: GeradorCardProps) {
const [resultado, setResultado] = useState("");
const [paramInputs, setParamInputs] = useState<Record<string, string>>({});
const [validado, setValidado] = useState<boolean | null>(null);
const [resultado, setResultado] = useState("");
const [paramInputs, setParamInputs] = useState<Record<string, string>>({});
const [validado, setValidado] = useState<boolean | null>(null);
const handleInputChange = (nome: string, valor: string) => {
setParamInputs((prev) => ({ ...prev, [nome]: valor }));
};
const handleInputChange = (nome: string, valor: string) => {
setParamInputs((prev) => ({ ...prev, [nome]: valor }));
};
const handleGerar = async () => {
const gerado = await onGerar(paramInputs);
setResultado(gerado);
setValidado(null);
};
const handleGerar = async () => {
const gerado = await onGerar(paramInputs);
setResultado(gerado);
setValidado(null);
};
const handleValidar = async () => {
if (onValidar) {
const isValid = await onValidar(resultado);
setValidado(isValid);
}
};
const handleValidar = async () => {
if (onValidar) {
const isValid = await onValidar(resultado);
setValidado(isValid);
}
};
const handleCopiar = () => {
navigator.clipboard.writeText(resultado);
};
const handleCopiar = () => {
if (!resultado) return;
navigator.clipboard.writeText(resultado);
toast.success(`${titulo} copiado!`,
{
duration: 3000
});
};
return (
<div className="w-full max-w-md rounded-2xl border border-gray-200 bg-white p-6 shadow hover:shadow-md transition-all">
<div className="mb-4">
<h3 className="text-lg font-bold text-blue-700">{titulo}</h3>
{descricao && <p className="text-sm text-gray-500 mt-1">{descricao}</p>}
</div>
const handleResultadoChange = (valor: string) => {
setResultado(valor);
setValidado(null); // resetar estado de validação ao editar
};
{parametros.length > 0 && (
<div className="space-y-4 mb-4">
{parametros.map((param) => {
if (param.tipo === "select") {
return (
<Select
key={param.nome}
label={param.nome}
options={param.options}
value={paramInputs[param.nome] || ""}
onChange={(val) => handleInputChange(param.nome, val)}
placeholder={`Selecionar ${param.nome}`}
return (
<div className="w-full max-w-md rounded-2xl border border-gray-200 bg-white dark:bg-zinc-900 p-6 shadow-lg hover:shadow-xl transition-shadow duration-300">
{/* Título e descrição */}
<div className="mb-6">
<h3 className="text-xl font-bold text-blue-700 dark:text-blue-400">{titulo}</h3>
{descricao && <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{descricao}</p>}
</div>
{/* Parâmetros dinâmicos */}
{parametros.length > 0 && (
<div className="space-y-4 mb-6">
{parametros.map((param) => {
if (param.tipo === "select") {
return (
<Select
key={param.nome}
label={param.nome}
options={param.options}
value={paramInputs[param.nome] || ""}
onChange={(val) => handleInputChange(param.nome, val)}
placeholder={`Selecionar ${param.nome}`}
/>
);
}
return (
<Input
key={param.nome}
type={param.tipo}
placeholder={param.placeholder || param.nome}
value={paramInputs[param.nome] || ""}
onChange={(e) => handleInputChange(param.nome, e.target.value)}
/>
);
})}
</div>
)}
{/* Resultado + botão de copiar como ícone */}
<div className="mb-6 flex items-center gap-2 relative">
<ResultDisplay
value={resultado}
validated={validado}
onChange={handleResultadoChange}
/>
);
}
{resultado && (
<Button
onClick={handleCopiar}
icon={<CopyIcon className="w-4 h-4" />}
variant="secondary"
className="flex items-center gap-1 px-2 py-1 text-xs h-auto"
title="Copiar"
>
</Button>
)}
</div>
// Input normal (text, number, password)
return (
<Input
key={param.nome}
type={param.tipo}
placeholder={param.placeholder || param.nome}
value={paramInputs[param.nome] || ""}
onChange={(e) => handleInputChange(param.nome, e.target.value)}
/>
);
})}
{/* Botões distribuídos numa linha com preenchimento igual */}
<div className="flex w-full gap-x-2">
<Button
onClick={handleGerar}
icon={<RefreshCwIcon className="w-4 h-4" />}
variant="primary"
className="flex-1"
>
Gerar
</Button>
{onValidar && (
<Button onClick={handleValidar} variant="danger" className="flex-1">
Validar
</Button>
)}
</div>
</div>
)}
<div className="mb-4">
<ResultDisplay value={resultado} validated={validado} />
</div>
<ButtonGroup>
<Button onClick={handleGerar} icon={<RefreshCwIcon />} variant="primary">
Gerar
</Button>
<Button onClick={handleCopiar} icon={<CopyIcon />} variant="secondary">
Copiar
</Button>
{onValidar && (
<Button onClick={handleValidar} variant="danger">
Validar
</Button>
)}
</ButtonGroup>
</div>
);
);
}

View File

@@ -0,0 +1,29 @@
import GeradorService from "../../../Api/GeradorApi.tsx";
import GeradorCard from "../GeradorCard.tsx";
const GenerateCC = () => {
const handleGenerateCC = async () => {
const cc = await GeradorService.GenerateCC()
return cc;
};
const handleValidateCC = async (valor: string) => {
// Lógica real de validação
//const bol = await GeradorService.ValidateCC(valor);
return true;
};
return (
<div className="max-w-md mx-auto p-6">
<GeradorCard
titulo="Cartão Cidadão"
onGerar={handleGenerateCC}
onValidar={handleValidateCC}
/>
</div>
);
};
export default GenerateCC;

View File

@@ -2,43 +2,44 @@ import React from "react";
import GeradorCard from "../GeradorCard";
import { EnumToOptions } from "../../../library/utils";
import { NIFType } from "../../../service/api";
import GeradorService from "../../../Api/GeradorApi";
const NifTypes = EnumToOptions(NIFType).map((opt) => ({
label: opt.label,
value: opt.value,
label: opt.label,
value: opt.value,
}));
const GenerateNIF = () => {
const handleGenerateNIF = async (params: Record<string, string>) => {
const tipoSelecionado = params["Tipo de NIF"];
// Aqui chamarias a tua API real, exemplo:
// const nif = await GeradorService.GenerateNIF({ type: tipoSelecionado });
// Para simular:
return `NIF-${tipoSelecionado}-${Math.floor(Math.random() * 1000000)}`;
};
const handleValidateNIF = async (valor: string) => {
// Lógica real de validação
return valor.startsWith("NIF-");
};
const handleGenerateNIF = async (params: Record<string, string>) => {
const tipoSelecionado = params["Tipo de NIF"];
const nif = await GeradorService.GenerateNIF(tipoSelecionado);
return (
<div className="max-w-md mx-auto p-6">
<GeradorCard
titulo="NIF"
descricao="Gera um Número de Identificação Fiscal válido"
parametros={[
{
nome: "Tipo de NIF",
tipo: "select",
options: NifTypes,
},
]}
onGerar={handleGenerateNIF}
onValidar={handleValidateNIF}
/>
</div>
);
return nif;
};
const handleValidateNIF = async (valor: string) => {
// Lógica real de validação
const bol = await GeradorService.ValidateNIF(valor);
return bol;
};
return (
<div className="max-w-md mx-auto p-6">
<GeradorCard
titulo="NIF"
parametros={[
{
nome: "Tipo de NIF",
tipo: "select",
options: NifTypes,
},
]}
onGerar={handleGenerateNIF}
onValidar={handleValidateNIF}
/>
</div>
);
};
export default GenerateNIF;

View File

@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
import GeradorService from "../../../Api/GeradorApi";
import GeradorCard from "../GeradorCard.tsx";
const GenerateNISS = () => {
const handleGenerateNISS = async () => {
const niss = await GeradorService.GenerateNISS()
return niss;
};
const handleValidateNISS = async (valor: string) => {
// Lógica real de validação
const bol = await GeradorService.ValidateNISS(valor);
return bol;
};
return (
<div className="max-w-md mx-auto p-6">
<GeradorCard
titulo="NISS"
onGerar={handleGenerateNISS}
onValidar={handleValidateNISS}
/>
</div>
);
};
export default GenerateNISS;

View File

@@ -7,6 +7,9 @@ type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
icon?: ReactNode;
variant?: "primary" | "secondary" | "ghost" | "danger";
loading?: boolean;
loadingText?: string;
fullWidth?: boolean;
rounded?: boolean | "md" | "lg" | "xl" | "2xl" | "full";
};
export function Button({
@@ -16,19 +19,27 @@ export function Button({
className = "",
type = "button",
loading = false,
loadingText,
disabled,
fullWidth = false,
rounded = "2xl",
...rest
}: ButtonProps) {
const base =
"inline-flex items-center justify-center gap-2 rounded-2xl px-5 py-2.5 font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md";
const base = twMerge(
"inline-flex items-center justify-center gap-2 px-5 py-2.5 font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md",
fullWidth && "w-full",
typeof rounded === "string" ? `rounded-${rounded}` : rounded === true ? "rounded-md" : "rounded-2xl"
);
const variants = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
primary:
"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 dark:bg-blue-600 dark:hover:bg-blue-500",
secondary:
"bg-zinc-100 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600 focus:ring-zinc-400",
"bg-zinc-100 text-zinc-900 hover:bg-zinc-200 focus:ring-zinc-400 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600",
ghost:
"bg-transparent text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800 focus:ring-zinc-300",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
"bg-transparent text-zinc-700 hover:bg-zinc-100 focus:ring-zinc-300 dark:text-zinc-200 dark:hover:bg-zinc-800",
danger:
"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 dark:bg-red-600 dark:hover:bg-red-500",
};
return (
@@ -36,13 +47,17 @@ export function Button({
type={type}
className={twMerge(base, variants[variant], className)}
disabled={disabled || loading}
aria-busy={loading}
{...rest}
>
{loading ? (
<Loader2 className="animate-spin w-4 h-4" />
<>
<Loader2 className="animate-spin w-4 h-4" />
{loadingText && <span>{loadingText}</span>}
</>
) : (
<>
{icon && <span className="w-4 h-4">{icon}</span>}
{icon && <span className="flex items-center">{icon}</span>}
{children}
</>
)}

View File

@@ -1,5 +1,5 @@
import { ReactNode } from "react";
export function ButtonGroup({ children }: { children: ReactNode }) {
return <div className="flex flex-wrap gap-2">{children}</div>;
return <div className="flex flex-wrap w-full gap-x-2">{children}</div>;
}

View File

@@ -0,0 +1,27 @@
import { SunIcon, MoonIcon } from "@heroicons/react/24/outline";
import { useTheme } from "../../context/ThemeContext";
export function DarkModeToggle() {
const { theme, toggleTheme } = useTheme();
const handleToggle = () => {
console.log("Toggle clicado, tema antes:", theme);
toggleTheme();
};
console.log("Render DarkModeToggle, theme =", theme);
return (
<button
onClick={handleToggle}
className="p-2 text-gray-700 hover:text-blue-500 dark:text-gray-300"
aria-label="Alternar modo escuro"
>
{theme === "dark" ? (
<SunIcon className="w-6 h-6" />
) : (
<MoonIcon className="w-6 h-6" />
)}
</button>
);
}

View File

@@ -3,29 +3,27 @@ import { CheckIcon, XIcon } from "lucide-react";
type ResultDisplayProps = {
value: string;
validated: boolean | null;
onChange?: (value: string) => void;
};
export function ResultDisplay({ value, validated }: ResultDisplayProps) {
const baseClasses = "flex items-center gap-2 px-3 py-2 border rounded-md text-sm ";
export function ResultDisplay({ value, validated, onChange }: ResultDisplayProps) {
const baseClasses = "flex items-center gap-2 px-3 py-2 border rounded-md text-sm w-full";
const colors =
validated === true
? "border-green-500 text-green-700 bg-green-50"
? "border-green-500 text-green-700 bg-green-50 dark:text-green-400 dark:bg-green-950 dark:border-green-700"
: validated === false
? "border-red-500 text-red-700 bg-red-50"
: "border-gray-300 text-gray-700 bg-gray-100";
? "border-red-500 text-red-700 bg-red-50 dark:text-red-400 dark:bg-red-950 dark:border-red-700"
: "border-gray-300 text-gray-700 bg-gray-100 dark:text-gray-300 dark:bg-zinc-800 dark:border-gray-600";
return (
<div className={baseClasses + colors}>
{validated !== null ? (
<input
type="text"
readOnly
value={value || "Resultado..."}
className="flex-1 bg-transparent outline-none font-mono truncate"
/>
) : (
<span className="font-mono flex-1 truncate">{value || "Resultado..."}</span>
)}
<div className={baseClasses + " " + colors}>
<input
type="text"
readOnly={!onChange}
value={value}
onChange={(e) => onChange?.(e.target.value)}
className="flex-1 bg-transparent outline-none font-mono truncate"
/>
{validated === true && <CheckIcon className="w-5 h-5" />}
{validated === false && <XIcon className="w-5 h-5" />}
</div>