Novo layout
This commit is contained in:
@@ -1 +1,4 @@
|
||||
# Geradores
|
||||
|
||||
<code> docker build -t shini89/geradoresws ..</code>
|
||||
<code> docker run --rm -p 44329:8080 shini89/geradoresws</code>
|
||||
|
||||
@@ -1 +1 @@
|
||||
VITE_API_URL=https://localhost:44329/
|
||||
VITE_API_URL=http://localhost:44329/
|
||||
@@ -19,5 +19,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="src\Api\" />
|
||||
<Folder Include="src\types\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
29
geradoresfe/src/components/Geradores/Tipos/GenerateCC.tsx
Normal file
29
geradoresfe/src/components/Geradores/Tipos/GenerateCC.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import GeradorService from "../../../Api/GeradorApi.tsx";
|
||||
import GeradorCard from "../GeradorCard.tsx";
|
||||
|
||||
const GenerateCC = () => {
|
||||
|
||||
const handleGenerateCC = async () => {
|
||||
const cc = await GeradorService.GenerateCC()
|
||||
return cc;
|
||||
};
|
||||
|
||||
const handleValidateCC = async (valor: string) => {
|
||||
// Lógica real de validação
|
||||
//const bol = await GeradorService.ValidateCC(valor);
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto p-6">
|
||||
<GeradorCard
|
||||
titulo="Cartão Cidadão"
|
||||
onGerar={handleGenerateCC}
|
||||
onValidar={handleValidateCC}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateCC;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import GeradorCard from "../GeradorCard";
|
||||
import { 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",
|
||||
|
||||
29
geradoresfe/src/components/Geradores/Tipos/GenerateNISS.tsx
Normal file
29
geradoresfe/src/components/Geradores/Tipos/GenerateNISS.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import GeradorService from "../../../Api/GeradorApi";
|
||||
import GeradorCard from "../GeradorCard.tsx";
|
||||
|
||||
const GenerateNISS = () => {
|
||||
|
||||
const handleGenerateNISS = async () => {
|
||||
const niss = await GeradorService.GenerateNISS()
|
||||
return niss;
|
||||
};
|
||||
|
||||
const handleValidateNISS = async (valor: string) => {
|
||||
// Lógica real de validação
|
||||
const bol = await GeradorService.ValidateNISS(valor);
|
||||
return bol;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto p-6">
|
||||
<GeradorCard
|
||||
titulo="NISS"
|
||||
onGerar={handleGenerateNISS}
|
||||
onValidar={handleValidateNISS}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateNISS;
|
||||
@@ -7,6 +7,9 @@ type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
icon?: ReactNode;
|
||||
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}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
27
geradoresfe/src/components/ui/DarkModeToggle.tsx
Normal file
27
geradoresfe/src/components/ui/DarkModeToggle.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { SunIcon, MoonIcon } from "@heroicons/react/24/outline";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
|
||||
export function DarkModeToggle() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
const handleToggle = () => {
|
||||
console.log("Toggle clicado, tema antes:", theme);
|
||||
toggleTheme();
|
||||
};
|
||||
|
||||
console.log("Render DarkModeToggle, theme =", theme);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className="p-2 text-gray-700 hover:text-blue-500 dark:text-gray-300"
|
||||
aria-label="Alternar modo escuro"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<SunIcon className="w-6 h-6" />
|
||||
) : (
|
||||
<MoonIcon className="w-6 h-6" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -3,29 +3,27 @@ import { CheckIcon, XIcon } from "lucide-react";
|
||||
type ResultDisplayProps = {
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>,
|
||||
)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import GeradorService from "../Api/GeradorApi";
|
||||
|
||||
|
||||
const GenerateNIF = () => {
|
||||
const [cc, setCc] = useState<string>("");
|
||||
|
||||
const fetchcc = async () => {
|
||||
const ccApi = await GeradorService.GenerateCC();
|
||||
setCc(ccApi.toString());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchcc();
|
||||
}, []);
|
||||
|
||||
const handleGenerateCC = () => {
|
||||
fetchcc();
|
||||
};
|
||||
|
||||
// Função para copiar o NIF para o clipboard
|
||||
const handleCopyToClipboard = () => {
|
||||
if (cc) {
|
||||
navigator.clipboard.writeText(cc);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
teste
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateNIF;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import GeradorService from "../Api/GeradorApi";
|
||||
|
||||
const GenerateNISS = () => {
|
||||
const [niss, setNiss] = useState<string>("");
|
||||
|
||||
const fetchNiss = async () => {
|
||||
const nissApi = await GeradorService.GenerateNISS();
|
||||
setNiss(nissApi.toString());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchNiss();
|
||||
}, []);
|
||||
const handleGenerateNISS = () => {
|
||||
fetchNiss();
|
||||
};
|
||||
|
||||
// Função para copiar o NIF para o clipboard
|
||||
const handleCopyToClipboard = () => {
|
||||
if (niss) {
|
||||
navigator.clipboard.writeText(niss);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
teste
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateNISS;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { 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">
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user