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 # 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>
<ItemGroup> <ItemGroup>
<Folder Include="src\Api\" /> <Folder Include="src\Api\" />
<Folder Include="src\types\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -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 [];
} }

View File

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

View File

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

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 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",

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; 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}
</> </>
)} )}

View File

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

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 = { 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>,
) )

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 { 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">

View File

@@ -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: [