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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,9 @@ type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
icon?: ReactNode; 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={!onChange}
readOnly value={value}
value={value || "Resultado..."} 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,58 +1,54 @@
"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";
type ThemeContextType = { type ThemeContextType = {
theme: Theme; theme: Theme;
toggleTheme: () => void; toggleTheme: () => void;
}; };
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); useEffect(() => {
setIsInitialized(true); if (theme === null) return; // ainda a carregar
}, []);
useEffect(() => { const root = document.documentElement;
if (isInitialized) { root.classList.remove("light", "dark");
localStorage.setItem("theme", theme); root.classList.add(theme);
if (theme === "dark") { localStorage.setItem("theme", theme);
document.documentElement.classList.add("dark"); }, [theme]);
} 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"));
};
return ( // Enquanto tema não carregou, não renderiza nada
<ThemeContext.Provider value={{ theme, toggleTheme }}> if (theme === null) return null;
{children}
</ThemeContext.Provider> return (
); <ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}; };
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,150 +2,170 @@ 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: "/" },
{ {
name: "Geradores", name: "Geradores",
subItems: [ subItems: [
{ name: "Gerador de Senhas", path: "/geradores/senhas" }, { name: "Gerador de Senhas", path: "/geradores/senhas" },
{ name: "Gerador de Nomes", path: "/geradores/nomes" }, { name: "Gerador de Nomes", path: "/geradores/nomes" },
], ],
}, },
{ 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 = () => setIsScrolled(window.scrollY > 10);
const onScroll = () => { window.addEventListener("scroll", onScroll);
setIsScrolled(window.scrollY > 10); return () => window.removeEventListener("scroll", onScroll);
}; }, []);
window.addEventListener("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"> >
{/* LOGO */} <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center">
<Link to="/" className="flex items-center space-x-2"> {/* LOGO */}
<img src={Logo} alt="Logo" className="h-8 w-8" /> <Link to="/" className="flex items-center space-x-2">
<span className="font-bold text-lg text-gray-800">Factory Id</span> <img src={Logo} alt="Logo" className="h-8 w-8" />
</Link> <span className="font-bold text-lg text-gray-800 dark:text-white">
Factory Id
</span>
</Link>
{/* Desktop Nav */} {/* Desktop Nav */}
<div className="hidden md:flex space-x-6 items-center"> <div className="hidden md:flex space-x-6 items-center">
{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>
<Transition <Transition
enter="transition ease-out duration-100" enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95" enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100" enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75" leave="transition ease-in duration-75"
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"
{sub.name} : "text-gray-700 dark:text-gray-200"
</Link> }`}
)} >
</Menu.Item> {sub.name}
))} </Link>
</div> )}
</Menu.Items> </Menu.Item>
</Transition> ))}
</Menu>
) : (
<NavLink
to={item.path}
key={item.name}
className={({ isActive }) => navLinkClass(isActive)}
>
{item.name}
</NavLink>
)
)}
</div> </div>
</Menu.Items>
</Transition>
</Menu>
) : (
<NavLink
to={item.path}
key={item.name}
className={({ isActive }) => navLinkClass(isActive)}
>
{item.name}
</NavLink>
)
)}
<DarkModeToggle />
</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 ? (
</Disclosure.Button> <XMarkIcon className="h-6 w-6" />
</div> ) : (
<Bars3Icon className="h-6 w-6" />
)}
</Disclosure.Button>
</div>
</div>
{/* Mobile Menu Panel */}
<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 dark:text-gray-200 mb-1">
{item.name}
</span>
<div className="pl-4 space-y-1">
{item.subItems.map((sub) => (
<NavLink
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 dark:text-gray-300"
}`
}
>
{sub.name}
</NavLink>
))}
</div> </div>
</div>
{/* Mobile Menu Panel */} ) : (
<Disclosure.Panel className="md:hidden bg-white shadow-inner"> <NavLink
<div className="px-4 pt-2 pb-4 space-y-1"> to={item.path}
{navItems.map((item) => key={item.name}
item.subItems ? ( className={({ isActive }) =>
<div key={item.name}> `block text-sm px-3 py-2 rounded-md ${
<span className="block text-sm font-medium text-gray-700 mb-1"> isActive
{item.name} ? "text-blue-600 font-semibold"
</span> : "text-gray-700 dark:text-gray-300"
<div className="pl-4 space-y-1"> }`
{item.subItems.map((sub) => ( }
<NavLink >
to={sub.path} {item.name}
key={sub.name} </NavLink>
className={({ isActive }) => )
`block text-sm px-3 py-1 rounded ${isActive ? "text-blue-600 font-semibold" : "text-gray-700" )}
}` <div className="pt-2">
} <DarkModeToggle />
> </div>
{sub.name} </div>
</NavLink> </Disclosure.Panel>
))} </header>
</div> )}
</div> </Disclosure>
) : ( );
<NavLink
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"
}`
}
>
{item.name}
</NavLink>
)
)}
</div>
</Disclosure.Panel>
</header>
)}
</Disclosure>
);
} }
export default Header; export default 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>
<App /> <ThemeProvider>
</StrictMode>, <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 { 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">
@@ -28,9 +28,9 @@ export default function Home() {
<div id="niss"> <div id="niss">
<GenerateNISS /> <GenerateNISS />
</div> </div>
<div id="cc"> <div id="cc">
<GenerateCC /> <GenerateCC />
</div> </div>
</div> </div>
</section> </section>
</div> </div>

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