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 +1,4 @@
# Geradores
<code> docker build -t shini89/geradoresws ..</code>
<code> docker run --rm -p 44329:8080 shini89/geradoresws</code>

View File

@@ -1 +1 @@
VITE_API_URL=https://localhost:44329/
VITE_API_URL=http://localhost:44329/

View File

@@ -19,5 +19,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="src\Api\" />
<Folder Include="src\types\" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,7 @@
import axios from 'axios';
import type { AxiosResponse } from 'axios';
import { NIFType } from '../service/api';
import { toast } from 'react-hot-toast';
const API_URL = import.meta.env.VITE_API_URL;
@@ -15,21 +16,24 @@ class GeradorService {
});
return response.data;
} catch (error) {
console.error('Error fetching NIF:', error);
toast.error('Error fetching NIF:' + error.message);
return [];
}
}
static async ValidateNIF(type: string | null): Promise<any[]> {
static async ValidateNIF(nif: string | null): Promise<any[]> {
try {
const response: AxiosResponse = await axios.get(API_URL + 'Generate/ValidateNIF',
{
params: {
type: type
nif: nif
}
});
return response.data;
} catch (error) {
toast.error('Error fetching NIF:' + error);
console.error('Error fetching NIF:', error);
return [];
}

View File

@@ -2,8 +2,11 @@ import { Routes, Route, BrowserRouter as Router } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
import Home from './pages/Home';
import { Layout } from './layout/Layout';
import { useTheme } from './context/ThemeContext';
function App() {
const { theme } = useTheme();
return (
<Router>
<Layout>
@@ -13,7 +16,14 @@ function App() {
<Route path="/NISS" />
<Route path="/CC" />
</Routes>
<Toaster position="top-right" />
<Toaster position="top-right"
toastOptions={{
style: {
background: theme === 'dark' ? '#1f2937' : '#fff',
color: theme === 'dark' ? '#fff' : '#000',
},
}}
theme={theme} />
</Layout>
</Router>
);

View File

@@ -1,21 +1,21 @@
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 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({
@@ -47,18 +47,30 @@ export default function GeradorCard({
};
const handleCopiar = () => {
if (!resultado) return;
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 (
<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 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-4">
<div className="space-y-4 mb-6">
{parametros.map((param) => {
if (param.tipo === "select") {
return (
@@ -73,7 +85,6 @@ export default function GeradorCard({
);
}
// Input normal (text, number, password)
return (
<Input
key={param.nome}
@@ -87,25 +98,42 @@ export default function GeradorCard({
</div>
)}
<div className="mb-4">
<ResultDisplay value={resultado} validated={validado} />
{/* 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>
<ButtonGroup>
<Button onClick={handleGerar} icon={<RefreshCwIcon />} variant="primary">
{/* 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>
<Button onClick={handleCopiar} icon={<CopyIcon />} variant="secondary">
Copiar
</Button>
{onValidar && (
<Button onClick={handleValidar} variant="danger">
<Button onClick={handleValidar} variant="danger" className="flex-1">
Validar
</Button>
)}
</ButtonGroup>
</div>
</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,6 +2,7 @@ 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,
@@ -9,24 +10,24 @@ const NifTypes = EnumToOptions(NIFType).map((opt) => ({
}));
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 nif = await GeradorService.GenerateNIF(tipoSelecionado);
return nif;
};
const handleValidateNIF = async (valor: string) => {
// Lógica real de validação
return valor.startsWith("NIF-");
const bol = await GeradorService.ValidateNIF(valor);
return bol;
};
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",

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" />
{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 ? (
<div className={baseClasses + " " + colors}>
<input
type="text"
readOnly
value={value || "Resultado..."}
readOnly={!onChange}
value={value}
onChange={(e) => onChange?.(e.target.value)}
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 === false && <XIcon className="w-5 h-5" />}
</div>

View File

@@ -1,7 +1,4 @@
"use client";
import type React from "react";
import { createContext, useState, useContext, useEffect } from "react";
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
type Theme = "light" | "dark";
@@ -12,36 +9,37 @@ type ThemeContextType = {
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [theme, setTheme] = useState<Theme>("light");
const [isInitialized, setIsInitialized] = useState(false);
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useState<Theme | null>(null); // null = tema ainda não carregado
useEffect(() => {
// This code will only run on the client side
const savedTheme = localStorage.getItem("theme") as Theme | null;
const initialTheme = savedTheme || "light"; // Default to light theme
const storedTheme = localStorage.getItem("theme") as Theme | null;
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const initialTheme = storedTheme ?? (prefersDark ? "dark" : "light");
setTheme(initialTheme);
setIsInitialized(true);
}, []);
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);
if (theme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
}, [theme, isInitialized]);
}, [theme]);
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 (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
@@ -49,10 +47,8 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
);
};
export const useTheme = () => {
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
};

View File

@@ -1,5 +1,3 @@
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
@custom-variant dark (&:where(.dark, .dark *));

View File

@@ -4,33 +4,33 @@ export default function Footer() {
const year = new Date().getFullYear();
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="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Marca e descrição */}
<div>
<h2 className="text-xl font-bold text-primary">FactoryiD</h2>
<p className="mt-2 text-sm text-gray-600">
<h2 className="text-xl font-bold text-gray-800 dark:text-white">FactoryiD</h2>
<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.
</p>
</div>
{/* Navegação principal */}
<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">
<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
</Link>
</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
</Link>
</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
</Link>
</li>
@@ -39,15 +39,15 @@ export default function Footer() {
{/* Outros links */}
<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">
<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
</Link>
</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
</Link>
</li>
@@ -56,10 +56,10 @@ export default function Footer() {
</div>
{/* Separador */}
<hr className="my-8 border-gray-300" />
<hr className="my-8 border-gray-300 dark:border-gray-700" />
{/* 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.
</div>
</div>

View File

@@ -2,8 +2,8 @@ import { useEffect, useState } from "react";
import { Link, NavLink, useLocation } from "react-router";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import { Bars3Icon, XMarkIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
import Logo from "../assets/logotipo32x32.png";
import { DarkModeToggle } from "../components/ui/DarkModeToggle";
const navItems = [
{ name: "Início", path: "/" },
@@ -16,35 +16,38 @@ const navItems = [
},
{ name: "Contacto", path: "/contacto" },
];
function Header() {
const location = useLocation();
const [isScrolled, setIsScrolled] = useState(false);
// Header shrink on scroll
useEffect(() => {
const onScroll = () => {
setIsScrolled(window.scrollY > 10);
};
const onScroll = () => setIsScrolled(window.scrollY > 10);
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
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 (
<Disclosure as="nav">
{({ open }) => (
<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">
{/* LOGO */}
<Link to="/" className="flex items-center space-x-2">
<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>
{/* Desktop Nav */}
@@ -52,7 +55,7 @@ function Header() {
{navItems.map((item) =>
item.subItems ? (
<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}
<ChevronDownIcon className="w-4 h-4" />
</Menu.Button>
@@ -64,14 +67,17 @@ function Header() {
leaveFrom="transform opacity-100 scale-100"
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">
{item.subItems.map((sub) => (
<Menu.Item key={sub.name}>
{({ active }) => (
<Link
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}
@@ -93,23 +99,28 @@ function Header() {
</NavLink>
)
)}
<DarkModeToggle />
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<Disclosure.Button className="text-gray-700 hover:text-blue-500">
{open ? <XMarkIcon className="h-6 w-6" /> : <Bars3Icon className="h-6 w-6" />}
<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" />
)}
</Disclosure.Button>
</div>
</div>
{/* 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">
{navItems.map((item) =>
item.subItems ? (
<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}
</span>
<div className="pl-4 space-y-1">
@@ -118,7 +129,10 @@ function Header() {
to={sub.path}
key={sub.name}
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}
key={item.name}
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>
)
)}
<div className="pt-2">
<DarkModeToggle />
</div>
</div>
</Disclosure.Panel>
</header>

View File

@@ -1,12 +1,14 @@
// components/layout/Layout.tsx
import Footer from './Footer';
import Header from './Header';
import { useTheme } from "../context/ThemeContext"; // caminho conforme a tua estrutura
export const Layout = ({ children }: { children: React.ReactNode }) => {
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 />
<main className="flex-grow">
<main className="flex-grow mx-auto w-full">
{children}
</main>
<Footer />

View File

@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { ThemeProvider } from './context/ThemeContext.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>,
)

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useLocation } from "react-router";
import GenerateNISS from "./GenerateNISS";
import GenerateCC from "./GenerateCC";
import GenerateNISS from "../components/Geradores/Tipos/GenerateNISS";
import GenerateCC from "../components/Geradores/Tipos/GenerateCC";
import GenerateNIF from "../components/Geradores/Tipos/GenerateNIF";
export default function Home() {
@@ -17,8 +17,8 @@ export default function Home() {
}, [location]);
return (
<div className="dark:bg-gray-900">
<div className="bg-gray-50 min-h-screen dark:bg-gray-900 dark:text-white">
<div >
<div >
{/* Seção de Funcionalidades */}
<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">

View File

@@ -2,7 +2,6 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [