Novo layout
This commit is contained in:
@@ -1 +1,4 @@
|
|||||||
# Geradores
|
# Geradores
|
||||||
|
|
||||||
|
<code> docker build -t shini89/geradoresws ..</code>
|
||||||
|
<code> docker run --rm -p 44329:8080 shini89/geradoresws</code>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
VITE_API_URL=https://localhost:44329/
|
VITE_API_URL=http://localhost:44329/
|
||||||
@@ -19,5 +19,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="src\Api\" />
|
<Folder Include="src\Api\" />
|
||||||
|
<Folder Include="src\types\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { AxiosResponse } from 'axios';
|
import type { AxiosResponse } from 'axios';
|
||||||
import { NIFType } from '../service/api';
|
import { NIFType } from '../service/api';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL;
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
@@ -15,21 +16,24 @@ class GeradorService {
|
|||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching NIF:', error);
|
|
||||||
|
toast.error('Error fetching NIF:' + error.message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async ValidateNIF(type: string | null): Promise<any[]> {
|
static async ValidateNIF(nif: string | null): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse = await axios.get(API_URL + 'Generate/ValidateNIF',
|
const response: AxiosResponse = await axios.get(API_URL + 'Generate/ValidateNIF',
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
type: type
|
nif: nif
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
toast.error('Error fetching NIF:' + error);
|
||||||
|
|
||||||
console.error('Error fetching NIF:', error);
|
console.error('Error fetching NIF:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import { Routes, Route, BrowserRouter as Router } from 'react-router-dom';
|
|||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import { Layout } from './layout/Layout';
|
import { Layout } from './layout/Layout';
|
||||||
|
import { useTheme } from './context/ThemeContext';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Layout>
|
<Layout>
|
||||||
@@ -13,7 +16,14 @@ function App() {
|
|||||||
<Route path="/NISS" />
|
<Route path="/NISS" />
|
||||||
<Route path="/CC" />
|
<Route path="/CC" />
|
||||||
</Routes>
|
</Routes>
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right"
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
background: theme === 'dark' ? '#1f2937' : '#fff',
|
||||||
|
color: theme === 'dark' ? '#fff' : '#000',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
theme={theme} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { RefreshCwIcon, CopyIcon } from "lucide-react";
|
import { RefreshCwIcon, CopyIcon } from "lucide-react";
|
||||||
import { Button } from "../ui/Button";
|
import { Button } from "../ui/Button";
|
||||||
import { Input } from "../ui/Input";
|
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 { ResultDisplay } from "../ui/ResultDisplay";
|
||||||
import { ButtonGroup } from "../ui/ButtonGroup";
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
type Parametro =
|
type Parametro =
|
||||||
| { nome: string; tipo: "text" | "number" | "password"; placeholder?: string }
|
| { nome: string; tipo: "text" | "number" | "password"; placeholder?: string }
|
||||||
| { nome: string; tipo: "select"; options: { label: string; value: string }[] };
|
| { nome: string; tipo: "select"; options: { label: string; value: string }[] };
|
||||||
|
|
||||||
type GeradorCardProps = {
|
type GeradorCardProps = {
|
||||||
titulo: string;
|
readonly titulo: string;
|
||||||
descricao?: string;
|
readonly descricao?: string;
|
||||||
parametros?: Parametro[];
|
readonly parametros?: Parametro[];
|
||||||
onGerar: (parametros?: Record<string, string>) => string | Promise<string>;
|
readonly onGerar: (parametros?: Record<string, string>) => string | Promise<string>;
|
||||||
onValidar?: (valor: string) => boolean | Promise<boolean>;
|
readonly onValidar?: (valor: string) => boolean | Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function GeradorCard({
|
export default function GeradorCard({
|
||||||
@@ -47,18 +47,30 @@ export default function GeradorCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopiar = () => {
|
const handleCopiar = () => {
|
||||||
|
if (!resultado) return;
|
||||||
navigator.clipboard.writeText(resultado);
|
navigator.clipboard.writeText(resultado);
|
||||||
|
toast.success(`${titulo} copiado!`,
|
||||||
|
{
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResultadoChange = (valor: string) => {
|
||||||
|
setResultado(valor);
|
||||||
|
setValidado(null); // resetar estado de validação ao editar
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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="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">
|
||||||
<div className="mb-4">
|
{/* Título e descrição */}
|
||||||
<h3 className="text-lg font-bold text-blue-700">{titulo}</h3>
|
<div className="mb-6">
|
||||||
{descricao && <p className="text-sm text-gray-500 mt-1">{descricao}</p>}
|
<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>
|
</div>
|
||||||
|
|
||||||
|
{/* Parâmetros dinâmicos */}
|
||||||
{parametros.length > 0 && (
|
{parametros.length > 0 && (
|
||||||
<div className="space-y-4 mb-4">
|
<div className="space-y-4 mb-6">
|
||||||
{parametros.map((param) => {
|
{parametros.map((param) => {
|
||||||
if (param.tipo === "select") {
|
if (param.tipo === "select") {
|
||||||
return (
|
return (
|
||||||
@@ -73,7 +85,6 @@ export default function GeradorCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input normal (text, number, password)
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
key={param.nome}
|
key={param.nome}
|
||||||
@@ -87,25 +98,42 @@ export default function GeradorCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-4">
|
{/* Resultado + botão de copiar como ícone */}
|
||||||
<ResultDisplay value={resultado} validated={validado} />
|
<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>
|
</div>
|
||||||
|
|
||||||
<ButtonGroup>
|
{/* Botões distribuídos numa linha com preenchimento igual */}
|
||||||
<Button onClick={handleGerar} icon={<RefreshCwIcon />} variant="primary">
|
<div className="flex w-full gap-x-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleGerar}
|
||||||
|
icon={<RefreshCwIcon className="w-4 h-4" />}
|
||||||
|
variant="primary"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
Gerar
|
Gerar
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={handleCopiar} icon={<CopyIcon />} variant="secondary">
|
|
||||||
Copiar
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{onValidar && (
|
{onValidar && (
|
||||||
<Button onClick={handleValidar} variant="danger">
|
<Button onClick={handleValidar} variant="danger" className="flex-1">
|
||||||
Validar
|
Validar
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ButtonGroup>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
29
geradoresfe/src/components/Geradores/Tipos/GenerateCC.tsx
Normal file
29
geradoresfe/src/components/Geradores/Tipos/GenerateCC.tsx
Normal 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;
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import GeradorCard from "../GeradorCard";
|
import GeradorCard from "../GeradorCard";
|
||||||
import { EnumToOptions } from "../../../library/utils";
|
import { EnumToOptions } from "../../../library/utils";
|
||||||
import { NIFType } from "../../../service/api";
|
import { NIFType } from "../../../service/api";
|
||||||
|
import GeradorService from "../../../Api/GeradorApi";
|
||||||
|
|
||||||
const NifTypes = EnumToOptions(NIFType).map((opt) => ({
|
const NifTypes = EnumToOptions(NIFType).map((opt) => ({
|
||||||
label: opt.label,
|
label: opt.label,
|
||||||
@@ -9,24 +10,24 @@ const NifTypes = EnumToOptions(NIFType).map((opt) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const GenerateNIF = () => {
|
const GenerateNIF = () => {
|
||||||
|
|
||||||
const handleGenerateNIF = async (params: Record<string, string>) => {
|
const handleGenerateNIF = async (params: Record<string, string>) => {
|
||||||
const tipoSelecionado = params["Tipo de NIF"];
|
const tipoSelecionado = params["Tipo de NIF"];
|
||||||
// Aqui chamarias a tua API real, exemplo:
|
const nif = await GeradorService.GenerateNIF(tipoSelecionado);
|
||||||
// const nif = await GeradorService.GenerateNIF({ type: tipoSelecionado });
|
|
||||||
// Para simular:
|
return nif;
|
||||||
return `NIF-${tipoSelecionado}-${Math.floor(Math.random() * 1000000)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleValidateNIF = async (valor: string) => {
|
const handleValidateNIF = async (valor: string) => {
|
||||||
// Lógica real de validação
|
// Lógica real de validação
|
||||||
return valor.startsWith("NIF-");
|
const bol = await GeradorService.ValidateNIF(valor);
|
||||||
|
return bol;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md mx-auto p-6">
|
<div className="max-w-md mx-auto p-6">
|
||||||
<GeradorCard
|
<GeradorCard
|
||||||
titulo="NIF"
|
titulo="NIF"
|
||||||
descricao="Gera um Número de Identificação Fiscal válido"
|
|
||||||
parametros={[
|
parametros={[
|
||||||
{
|
{
|
||||||
nome: "Tipo de NIF",
|
nome: "Tipo de NIF",
|
||||||
|
|||||||
29
geradoresfe/src/components/Geradores/Tipos/GenerateNISS.tsx
Normal file
29
geradoresfe/src/components/Geradores/Tipos/GenerateNISS.tsx
Normal 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;
|
||||||
@@ -7,6 +7,9 @@ type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
variant?: "primary" | "secondary" | "ghost" | "danger";
|
variant?: "primary" | "secondary" | "ghost" | "danger";
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
loadingText?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
rounded?: boolean | "md" | "lg" | "xl" | "2xl" | "full";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
@@ -16,19 +19,27 @@ export function Button({
|
|||||||
className = "",
|
className = "",
|
||||||
type = "button",
|
type = "button",
|
||||||
loading = false,
|
loading = false,
|
||||||
|
loadingText,
|
||||||
disabled,
|
disabled,
|
||||||
|
fullWidth = false,
|
||||||
|
rounded = "2xl",
|
||||||
...rest
|
...rest
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const base =
|
const base = twMerge(
|
||||||
"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";
|
"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 = {
|
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:
|
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:
|
ghost:
|
||||||
"bg-transparent text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800 focus:ring-zinc-300",
|
"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",
|
danger:
|
||||||
|
"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 dark:bg-red-600 dark:hover:bg-red-500",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,13 +47,17 @@ export function Button({
|
|||||||
type={type}
|
type={type}
|
||||||
className={twMerge(base, variants[variant], className)}
|
className={twMerge(base, variants[variant], className)}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
|
aria-busy={loading}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{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}
|
{children}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
export function ButtonGroup({ children }: { children: ReactNode }) {
|
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>;
|
||||||
}
|
}
|
||||||
|
|||||||
27
geradoresfe/src/components/ui/DarkModeToggle.tsx
Normal file
27
geradoresfe/src/components/ui/DarkModeToggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,29 +3,27 @@ import { CheckIcon, XIcon } from "lucide-react";
|
|||||||
type ResultDisplayProps = {
|
type ResultDisplayProps = {
|
||||||
value: string;
|
value: string;
|
||||||
validated: boolean | null;
|
validated: boolean | null;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ResultDisplay({ value, validated }: ResultDisplayProps) {
|
export function ResultDisplay({ value, validated, onChange }: ResultDisplayProps) {
|
||||||
const baseClasses = "flex items-center gap-2 px-3 py-2 border rounded-md text-sm ";
|
const baseClasses = "flex items-center gap-2 px-3 py-2 border rounded-md text-sm w-full";
|
||||||
const colors =
|
const colors =
|
||||||
validated === true
|
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
|
: validated === false
|
||||||
? "border-red-500 text-red-700 bg-red-50"
|
? "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";
|
: "border-gray-300 text-gray-700 bg-gray-100 dark:text-gray-300 dark:bg-zinc-800 dark:border-gray-600";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={baseClasses + colors}>
|
<div className={baseClasses + " " + colors}>
|
||||||
{validated !== null ? (
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
readOnly
|
readOnly={!onChange}
|
||||||
value={value || "Resultado..."}
|
value={value}
|
||||||
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
className="flex-1 bg-transparent outline-none font-mono truncate"
|
className="flex-1 bg-transparent outline-none font-mono truncate"
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<span className="font-mono flex-1 truncate">{value || "Resultado..."}</span>
|
|
||||||
)}
|
|
||||||
{validated === true && <CheckIcon className="w-5 h-5" />}
|
{validated === true && <CheckIcon className="w-5 h-5" />}
|
||||||
{validated === false && <XIcon className="w-5 h-5" />}
|
{validated === false && <XIcon className="w-5 h-5" />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
"use client";
|
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
|
||||||
|
|
||||||
import type React from "react";
|
|
||||||
import { createContext, useState, useContext, useEffect } from "react";
|
|
||||||
|
|
||||||
type Theme = "light" | "dark";
|
type Theme = "light" | "dark";
|
||||||
|
|
||||||
@@ -12,36 +9,37 @@ type ThemeContextType = {
|
|||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
|
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||||
children,
|
const [theme, setTheme] = useState<Theme | null>(null); // null = tema ainda não carregado
|
||||||
}) => {
|
|
||||||
const [theme, setTheme] = useState<Theme>("light");
|
|
||||||
const [isInitialized, setIsInitialized] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This code will only run on the client side
|
const storedTheme = localStorage.getItem("theme") as Theme | null;
|
||||||
const savedTheme = localStorage.getItem("theme") as Theme | null;
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
const initialTheme = savedTheme || "light"; // Default to light theme
|
const initialTheme = storedTheme ?? (prefersDark ? "dark" : "light");
|
||||||
|
|
||||||
setTheme(initialTheme);
|
setTheme(initialTheme);
|
||||||
setIsInitialized(true);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInitialized) {
|
if (theme === null) return; // ainda a carregar
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.classList.remove("light", "dark");
|
||||||
|
root.classList.add(theme);
|
||||||
localStorage.setItem("theme", theme);
|
localStorage.setItem("theme", theme);
|
||||||
if (theme === "dark") {
|
}, [theme]);
|
||||||
document.documentElement.classList.add("dark");
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove("dark");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [theme, isInitialized]);
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
|
setTheme((prev) => {
|
||||||
|
const nextTheme = prev === "dark" ? "light" : "dark";
|
||||||
|
console.log("ToggleTheme:", prev, "->", nextTheme);
|
||||||
|
return nextTheme;
|
||||||
|
});
|
||||||
|
//setTheme((prev) => (prev === "dark" ? "light" : "dark"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Enquanto tema não carregou, não renderiza nada
|
||||||
|
if (theme === null) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||||
{children}
|
{children}
|
||||||
@@ -49,10 +47,8 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTheme = () => {
|
export const useTheme = (): ThemeContextType => {
|
||||||
const context = useContext(ThemeContext);
|
const context = useContext(ThemeContext);
|
||||||
if (context === undefined) {
|
if (!context) throw new Error("useTheme must be used within ThemeProvider");
|
||||||
throw new Error("useTheme must be used within a ThemeProvider");
|
|
||||||
}
|
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@tailwind base;
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
@@ -4,33 +4,33 @@ export default function Footer() {
|
|||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-gray-100 text-gray-700 mt-20">
|
<footer className="bg-gray-100 dark:bg-zinc-900 text-gray-700 dark:text-gray-300 mt-20">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
{/* Marca e descrição */}
|
{/* Marca e descrição */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-primary">FactoryiD</h2>
|
<h2 className="text-xl font-bold text-gray-800 dark:text-white">FactoryiD</h2>
|
||||||
<p className="mt-2 text-sm text-gray-600">
|
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||||
Geradores inteligentes para profissionais modernos. Soluções simples, rápidas e acessíveis.
|
Geradores inteligentes para profissionais modernos. Soluções simples, rápidas e acessíveis.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navegação principal */}
|
{/* Navegação principal */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">Navegação</h3>
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">Navegação</h3>
|
||||||
<ul className="space-y-1 text-sm">
|
<ul className="space-y-1 text-sm">
|
||||||
<li>
|
<li>
|
||||||
<Link to="/" className="hover:text-primary transition-colors duration-200">
|
<Link to="/" className="hover:text-blue-500 dark:hover:text-blue-400 transition-colors duration-200">
|
||||||
Início
|
Início
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/geradores" className="hover:text-primary transition-colors duration-200">
|
<Link to="/geradores" className="hover:text-blue-500 dark:hover:text-blue-400 transition-colors duration-200">
|
||||||
Geradores
|
Geradores
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/contacto" className="hover:text-primary transition-colors duration-200">
|
<Link to="/contacto" className="hover:text-blue-500 dark:hover:text-blue-400 transition-colors duration-200">
|
||||||
Contacto
|
Contacto
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -39,15 +39,15 @@ export default function Footer() {
|
|||||||
|
|
||||||
{/* Outros links */}
|
{/* Outros links */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">Outros</h3>
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">Outros</h3>
|
||||||
<ul className="space-y-1 text-sm">
|
<ul className="space-y-1 text-sm">
|
||||||
<li>
|
<li>
|
||||||
<Link to="/termos" className="hover:text-primary transition-colors duration-200">
|
<Link to="/termos" className="hover:text-blue-500 dark:hover:text-blue-400 transition-colors duration-200">
|
||||||
Termos de Utilização
|
Termos de Utilização
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/privacidade" className="hover:text-primary transition-colors duration-200">
|
<Link to="/privacidade" className="hover:text-blue-500 dark:hover:text-blue-400 transition-colors duration-200">
|
||||||
Política de Privacidade
|
Política de Privacidade
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -56,10 +56,10 @@ export default function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Separador */}
|
{/* Separador */}
|
||||||
<hr className="my-8 border-gray-300" />
|
<hr className="my-8 border-gray-300 dark:border-gray-700" />
|
||||||
|
|
||||||
{/* Copyright */}
|
{/* Copyright */}
|
||||||
<div className="text-center text-sm text-gray-500">
|
<div className="text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
© {year} FactoryiD. Todos os direitos reservados.
|
© {year} FactoryiD. Todos os direitos reservados.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useEffect, useState } from "react";
|
|||||||
import { Link, NavLink, useLocation } from "react-router";
|
import { Link, NavLink, useLocation } from "react-router";
|
||||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||||
import { Bars3Icon, XMarkIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
import { Bars3Icon, XMarkIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import Logo from "../assets/logotipo32x32.png";
|
import Logo from "../assets/logotipo32x32.png";
|
||||||
|
import { DarkModeToggle } from "../components/ui/DarkModeToggle";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: "Início", path: "/" },
|
{ name: "Início", path: "/" },
|
||||||
@@ -16,35 +16,38 @@ const navItems = [
|
|||||||
},
|
},
|
||||||
{ name: "Contacto", path: "/contacto" },
|
{ name: "Contacto", path: "/contacto" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
// Header shrink on scroll
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onScroll = () => {
|
const onScroll = () => setIsScrolled(window.scrollY > 10);
|
||||||
setIsScrolled(window.scrollY > 10);
|
|
||||||
};
|
|
||||||
window.addEventListener("scroll", onScroll);
|
window.addEventListener("scroll", onScroll);
|
||||||
return () => window.removeEventListener("scroll", onScroll);
|
return () => window.removeEventListener("scroll", onScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const navLinkClass = (isActive: boolean) =>
|
const navLinkClass = (isActive: boolean) =>
|
||||||
`px-3 py-2 rounded-md text-sm font-medium transition ${isActive ? "text-blue-600 font-semibold" : "text-gray-700 hover:text-blue-500"
|
`px-3 py-2 rounded-md text-sm font-medium transition ${isActive
|
||||||
|
? "text-blue-600 font-semibold"
|
||||||
|
: "text-gray-700 hover:text-blue-500 dark:text-gray-300 dark:hover:text-blue-400"
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure as="nav">
|
<Disclosure as="nav">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<header
|
<header
|
||||||
className={`fixed top-0 w-full z-50 bg-white shadow transition-all duration-300 ${isScrolled ? "py-2 shadow-md" : "py-4"
|
className={`fixed top-0 w-full z-50 bg-white dark:bg-zinc-900 shadow transition-all duration-300 ${
|
||||||
|
isScrolled ? "py-2 shadow-md" : "py-4"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center">
|
||||||
{/* LOGO */}
|
{/* LOGO */}
|
||||||
<Link to="/" className="flex items-center space-x-2">
|
<Link to="/" className="flex items-center space-x-2">
|
||||||
<img src={Logo} alt="Logo" className="h-8 w-8" />
|
<img src={Logo} alt="Logo" className="h-8 w-8" />
|
||||||
<span className="font-bold text-lg text-gray-800">Factory Id</span>
|
<span className="font-bold text-lg text-gray-800 dark:text-white">
|
||||||
|
Factory Id
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Nav */}
|
{/* Desktop Nav */}
|
||||||
@@ -52,7 +55,7 @@ function Header() {
|
|||||||
{navItems.map((item) =>
|
{navItems.map((item) =>
|
||||||
item.subItems ? (
|
item.subItems ? (
|
||||||
<Menu as="div" className="relative" key={item.name}>
|
<Menu as="div" className="relative" key={item.name}>
|
||||||
<Menu.Button className="flex items-center gap-1 text-gray-700 hover:text-blue-500 text-sm font-medium">
|
<Menu.Button className="flex items-center gap-1 text-gray-700 hover:text-blue-500 dark:text-gray-300 dark:hover:text-blue-400 text-sm font-medium">
|
||||||
{item.name}
|
{item.name}
|
||||||
<ChevronDownIcon className="w-4 h-4" />
|
<ChevronDownIcon className="w-4 h-4" />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
@@ -64,14 +67,17 @@ function Header() {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="absolute mt-2 w-56 rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-none z-10">
|
<Menu.Items className="absolute mt-2 w-56 rounded-md bg-white dark:bg-zinc-800 shadow-lg ring-1 ring-black/5 focus:outline-none z-10">
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{item.subItems.map((sub) => (
|
{item.subItems.map((sub) => (
|
||||||
<Menu.Item key={sub.name}>
|
<Menu.Item key={sub.name}>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<Link
|
<Link
|
||||||
to={sub.path}
|
to={sub.path}
|
||||||
className={`block px-4 py-2 text-sm ${active ? "bg-gray-100 text-blue-600" : "text-gray-700"
|
className={`block px-4 py-2 text-sm ${
|
||||||
|
active
|
||||||
|
? "bg-gray-100 dark:bg-zinc-700 text-blue-600"
|
||||||
|
: "text-gray-700 dark:text-gray-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{sub.name}
|
{sub.name}
|
||||||
@@ -93,23 +99,28 @@ function Header() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
<DarkModeToggle />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu button */}
|
{/* Mobile menu button */}
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<Disclosure.Button className="text-gray-700 hover:text-blue-500">
|
<Disclosure.Button className="text-gray-700 hover:text-blue-500 dark:text-gray-300">
|
||||||
{open ? <XMarkIcon className="h-6 w-6" /> : <Bars3Icon className="h-6 w-6" />}
|
{open ? (
|
||||||
|
<XMarkIcon className="h-6 w-6" />
|
||||||
|
) : (
|
||||||
|
<Bars3Icon className="h-6 w-6" />
|
||||||
|
)}
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Menu Panel */}
|
{/* Mobile Menu Panel */}
|
||||||
<Disclosure.Panel className="md:hidden bg-white shadow-inner">
|
<Disclosure.Panel className="md:hidden bg-white dark:bg-zinc-900 shadow-inner">
|
||||||
<div className="px-4 pt-2 pb-4 space-y-1">
|
<div className="px-4 pt-2 pb-4 space-y-1">
|
||||||
{navItems.map((item) =>
|
{navItems.map((item) =>
|
||||||
item.subItems ? (
|
item.subItems ? (
|
||||||
<div key={item.name}>
|
<div key={item.name}>
|
||||||
<span className="block text-sm font-medium text-gray-700 mb-1">
|
<span className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
<div className="pl-4 space-y-1">
|
<div className="pl-4 space-y-1">
|
||||||
@@ -118,7 +129,10 @@ function Header() {
|
|||||||
to={sub.path}
|
to={sub.path}
|
||||||
key={sub.name}
|
key={sub.name}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`block text-sm px-3 py-1 rounded ${isActive ? "text-blue-600 font-semibold" : "text-gray-700"
|
`block text-sm px-3 py-1 rounded ${
|
||||||
|
isActive
|
||||||
|
? "text-blue-600 font-semibold"
|
||||||
|
: "text-gray-700 dark:text-gray-300"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -132,7 +146,10 @@ function Header() {
|
|||||||
to={item.path}
|
to={item.path}
|
||||||
key={item.name}
|
key={item.name}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`block text-sm px-3 py-2 rounded-md ${isActive ? "text-blue-600 font-semibold" : "text-gray-700"
|
`block text-sm px-3 py-2 rounded-md ${
|
||||||
|
isActive
|
||||||
|
? "text-blue-600 font-semibold"
|
||||||
|
: "text-gray-700 dark:text-gray-300"
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -140,6 +157,9 @@ function Header() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
<div className="pt-2">
|
||||||
|
<DarkModeToggle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
// components/layout/Layout.tsx
|
// components/layout/Layout.tsx
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
|
import { useTheme } from "../context/ThemeContext"; // caminho conforme a tua estrutura
|
||||||
|
|
||||||
export const Layout = ({ children }: { children: React.ReactNode }) => {
|
export const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100">
|
<div className="flex flex-col min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 transition-colors duration-300">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="flex-grow">
|
<main className="flex-grow mx-auto w-full">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import { ThemeProvider } from './context/ThemeContext.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<ThemeProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import GeradorService from "../Api/GeradorApi";
|
|
||||||
|
|
||||||
|
|
||||||
const GenerateNIF = () => {
|
|
||||||
const [cc, setCc] = useState<string>("");
|
|
||||||
|
|
||||||
const fetchcc = async () => {
|
|
||||||
const ccApi = await GeradorService.GenerateCC();
|
|
||||||
setCc(ccApi.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchcc();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleGenerateCC = () => {
|
|
||||||
fetchcc();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Função para copiar o NIF para o clipboard
|
|
||||||
const handleCopyToClipboard = () => {
|
|
||||||
if (cc) {
|
|
||||||
navigator.clipboard.writeText(cc);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
teste
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GenerateNIF;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import GeradorService from "../Api/GeradorApi";
|
|
||||||
|
|
||||||
const GenerateNISS = () => {
|
|
||||||
const [niss, setNiss] = useState<string>("");
|
|
||||||
|
|
||||||
const fetchNiss = async () => {
|
|
||||||
const nissApi = await GeradorService.GenerateNISS();
|
|
||||||
setNiss(nissApi.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchNiss();
|
|
||||||
}, []);
|
|
||||||
const handleGenerateNISS = () => {
|
|
||||||
fetchNiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Função para copiar o NIF para o clipboard
|
|
||||||
const handleCopyToClipboard = () => {
|
|
||||||
if (niss) {
|
|
||||||
navigator.clipboard.writeText(niss);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
teste
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GenerateNISS;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
import GenerateNISS from "./GenerateNISS";
|
import GenerateNISS from "../components/Geradores/Tipos/GenerateNISS";
|
||||||
import GenerateCC from "./GenerateCC";
|
import GenerateCC from "../components/Geradores/Tipos/GenerateCC";
|
||||||
import GenerateNIF from "../components/Geradores/Tipos/GenerateNIF";
|
import GenerateNIF from "../components/Geradores/Tipos/GenerateNIF";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -17,8 +17,8 @@ export default function Home() {
|
|||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-gray-900">
|
<div >
|
||||||
<div className="bg-gray-50 min-h-screen dark:bg-gray-900 dark:text-white">
|
<div >
|
||||||
{/* Seção de Funcionalidades */}
|
{/* Seção de Funcionalidades */}
|
||||||
<section id="features" className="py-16 px-4 md:px-8">
|
<section id="features" className="py-16 px-4 md:px-8">
|
||||||
<div className="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
|
<div className="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { defineConfig } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
Reference in New Issue
Block a user