+
+
Factory Id
+
+ Geradores e validadores de dados portugueses para testes de software.
+
- {/* Separador */}
-
-
- {/* Copyright */}
-
- © {year} FactoryiD. Todos os direitos reservados.
+
+
+
+
Aviso
+
+ Todos os dados são fictícios e gerados aleatoriamente. Destinam-se exclusivamente a testes;
+ qualquer coincidência com dados reais é acidental.
+
+
+
+
+ © {year} Factory Id
);
diff --git a/geradoresfe/src/layout/Header.tsx b/geradoresfe/src/layout/Header.tsx
index 2aeb0d9..df03c4b 100644
--- a/geradoresfe/src/layout/Header.tsx
+++ b/geradoresfe/src/layout/Header.tsx
@@ -1,172 +1,105 @@
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 { Link, NavLink } from "react-router";
+import {
+ Disclosure, DisclosureButton, DisclosurePanel, Menu, MenuButton, MenuItem, MenuItems,
+} from "@headlessui/react";
+import { ChevronDownIcon, MenuIcon, XIcon } from "lucide-react";
import Logo from "../assets/logotipo32x32.png";
import { DarkModeToggle } from "../components/ui/DarkModeToggle";
+import { useGenerators } from "../lib/useGenerators";
+import { docsUrl } from "../lib/api";
-const navItems = [
- { name: "Início", path: "/" },
- {
- name: "Geradores",
- subItems: [
- { name: "Gerador de NIFs", path: "/NIF", },
- { name: "Gerador de NISS", path: "/NISS" },
- { name: "Gerador de CC", path: "/CC" },
- ],
- },
- { name: "Contacto", path: "/contacto" },
-];
+const linkClass = ({ isActive }: { isActive: boolean }) =>
+ `rounded-md px-3 py-2 text-sm font-medium transition-colors ${isActive
+ ? "text-blue-600 dark:text-blue-400"
+ : "text-gray-700 hover:text-blue-600 dark:text-gray-300 dark:hover:text-blue-400"}`;
-function Header() {
- const location = useLocation();
- const [isScrolled, setIsScrolled] = useState(false);
+export default function Header() {
+ const { byCategory } = useGenerators();
+ const [isScrolled, setIsScrolled] = useState(false);
- useEffect(() => {
- const onScroll = () => setIsScrolled(window.scrollY > 10);
- window.addEventListener("scroll", onScroll);
- return () => window.removeEventListener("scroll", onScroll);
- }, []);
+ useEffect(() => {
+ const onScroll = () => setIsScrolled(window.scrollY > 10);
+ window.addEventListener("scroll", onScroll, { passive: true });
+ 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 dark:text-gray-300 dark:hover:text-blue-400"
- }`;
-
- return (
-
- {({ open }) => (
-
+ );
+}
diff --git a/geradoresfe/src/layout/Layout.tsx b/geradoresfe/src/layout/Layout.tsx
index b6c8eae..394f8c3 100644
--- a/geradoresfe/src/layout/Layout.tsx
+++ b/geradoresfe/src/layout/Layout.tsx
@@ -1,16 +1,13 @@
-// components/layout/Layout.tsx
-import Footer from './Footer';
-import Header from './Header';
-
-export const Layout = ({ children }: { children: React.ReactNode }) => {
+import type { ReactNode } from "react";
+import Footer from "./Footer";
+import Header from "./Header";
+export function Layout({ children }: { children: ReactNode }) {
return (
-
-
-
- {children}
-
+
+
+ {children}
);
-};
+}
diff --git a/geradoresfe/src/lib/api.ts b/geradoresfe/src/lib/api.ts
new file mode 100644
index 0000000..5a2e784
--- /dev/null
+++ b/geradoresfe/src/lib/api.ts
@@ -0,0 +1,84 @@
+// Cliente da API. Por omissão usa caminhos relativos (/api/v1): em dev o Vite faz proxy,
+// em produção o nginx encaminha /api para o backend. VITE_API_URL permite apontar para outro host.
+const BASE = `${(import.meta.env.VITE_API_URL ?? "").replace(/\/$/, "")}/api/v1`;
+
+export type OptionChoice = { value: string; label: string };
+
+export type GeneratorOption = {
+ key: string;
+ label: string;
+ choices: OptionChoice[];
+ defaultValue: string;
+};
+
+export type Generator = {
+ id: string;
+ name: string;
+ category: string;
+ description: string;
+ hasChecksum: boolean;
+ canValidate: boolean;
+ options: GeneratorOption[];
+};
+
+export type GeneratedItem = {
+ value: string;
+ fields?: Record
| null;
+};
+
+export type ValidationResult = {
+ isValid: boolean;
+ formatMatch: boolean;
+ message: string;
+ normalized?: string | null;
+ details?: string[] | null;
+};
+
+export type DetectMatch = {
+ generatorId: string;
+ generatorName: string;
+ result: ValidationResult;
+};
+
+export class ApiError extends Error {}
+
+async function get(path: string, params?: Record): Promise {
+ const query = new URLSearchParams();
+ for (const [k, v] of Object.entries(params ?? {}))
+ if (v !== undefined && v !== "") query.set(k, String(v));
+ const qs = query.toString();
+
+ let response: Response;
+ try {
+ response = await fetch(`${BASE}${path}${qs ? `?${qs}` : ""}`);
+ } catch {
+ throw new ApiError("Não foi possível contactar o servidor.");
+ }
+
+ if (!response.ok) {
+ const problem = await response.json().catch(() => null);
+ const firstError = problem?.errors ? Object.values(problem.errors).flat()[0] : null;
+ throw new ApiError(String(firstError ?? problem?.title ?? `Erro ${response.status}`));
+ }
+ return response.json() as Promise;
+}
+
+export const api = {
+ listGenerators: () => get("/generators"),
+
+ generate: (id: string, options: Record, count = 1) =>
+ get<{ generator: string; items: GeneratedItem[] }>(`/generators/${encodeURIComponent(id)}/generate`, { ...options, count })
+ .then((r) => r.items),
+
+ validate: (id: string, value: string) =>
+ get(`/generators/${encodeURIComponent(id)}/validate`, { value }),
+
+ detect: (value: string) =>
+ get<{ value: string; matches: DetectMatch[] }>("/validate", { value }).then((r) => r.matches),
+};
+
+export function defaultOptions(generator: Generator): Record {
+ return Object.fromEntries(generator.options.map((o) => [o.key, o.defaultValue]));
+}
+
+export const docsUrl =`${(import.meta.env.VITE_API_URL ?? "").replace(/\/$/, "")}/docs`;
diff --git a/geradoresfe/src/lib/clipboard.ts b/geradoresfe/src/lib/clipboard.ts
new file mode 100644
index 0000000..bbef268
--- /dev/null
+++ b/geradoresfe/src/lib/clipboard.ts
@@ -0,0 +1,10 @@
+import { toast } from "react-hot-toast";
+
+export async function copyToClipboard(text: string, label = "Valor") {
+ try {
+ await navigator.clipboard.writeText(text);
+ toast.success(`${label} copiado!`, { duration: 1500 });
+ } catch {
+ toast.error("Não foi possível copiar para a área de transferência.");
+ }
+}
diff --git a/geradoresfe/src/lib/export.test.ts b/geradoresfe/src/lib/export.test.ts
new file mode 100644
index 0000000..ede882c
--- /dev/null
+++ b/geradoresfe/src/lib/export.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from "vitest";
+import { columnsOf, toCsv, toJson, toText } from "./export";
+
+const simple = [{ value: "123456789" }, { value: "500000000" }];
+const composite = [
+ { value: "Ana Silva", fields: { Nome: "Ana Silva", Morada: 'Rua "A"; 1' } },
+ { value: "Rui Costa", fields: { Nome: "Rui Costa", Morada: "Rua B" } },
+];
+
+describe("export", () => {
+ it("usa a coluna Valor para geradores simples", () => {
+ expect(columnsOf(simple)).toEqual(["Valor"]);
+ });
+
+ it("usa os campos como colunas para geradores compostos", () => {
+ expect(columnsOf(composite)).toEqual(["Nome", "Morada"]);
+ });
+
+ it("gera CSV com BOM, ';' e aspas escapadas", () => {
+ const csv = toCsv(composite);
+ expect(csv.startsWith("")).toBe(true);
+ expect(csv).toBe('Nome;Morada\r\nAna Silva;"Rua ""A""; 1"\r\nRui Costa;Rua B');
+ });
+
+ it("gera JSON com valores ou objectos", () => {
+ expect(JSON.parse(toJson(simple))).toEqual(["123456789", "500000000"]);
+ expect(JSON.parse(toJson(composite))[1]).toEqual({ Nome: "Rui Costa", Morada: "Rua B" });
+ });
+
+ it("gera texto com um valor por linha", () => {
+ expect(toText(simple)).toBe("123456789\n500000000");
+ });
+});
diff --git a/geradoresfe/src/lib/export.ts b/geradoresfe/src/lib/export.ts
new file mode 100644
index 0000000..0a98239
--- /dev/null
+++ b/geradoresfe/src/lib/export.ts
@@ -0,0 +1,40 @@
+import type { GeneratedItem } from "./api";
+
+/** Colunas da tabela/exportação: "Valor" para geradores simples, ou os campos dos compostos. */
+export function columnsOf(items: GeneratedItem[]): string[] {
+ const first = items.find((i) => i.fields);
+ return first?.fields ? Object.keys(first.fields) : ["Valor"];
+}
+
+export function rowOf(item: GeneratedItem, columns: string[]): string[] {
+ return item.fields ? columns.map((c) => item.fields?.[c] ?? "") : [item.value];
+}
+
+function csvCell(value: string): string {
+ return /[";\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
+}
+
+/** CSV com separador ";" (o Excel em PT usa ";") e BOM para os acentos abrirem bem. */
+export function toCsv(items: GeneratedItem[]): string {
+ const columns = columnsOf(items);
+ const lines = [columns, ...items.map((i) => rowOf(i, columns))].map((row) => row.map(csvCell).join(";"));
+ return "" + lines.join("\r\n");
+}
+
+export function toJson(items: GeneratedItem[]): string {
+ const data = items.map((i) => i.fields ?? i.value);
+ return JSON.stringify(data, null, 2);
+}
+
+export function toText(items: GeneratedItem[]): string {
+ return items.map((i) => i.value).join("\n");
+}
+
+export function download(content: string, filename: string, mime: string) {
+ const url = URL.createObjectURL(new Blob([content], { type: mime }));
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(url);
+}
diff --git a/geradoresfe/src/lib/slug.ts b/geradoresfe/src/lib/slug.ts
new file mode 100644
index 0000000..5329df9
--- /dev/null
+++ b/geradoresfe/src/lib/slug.ts
@@ -0,0 +1,8 @@
+export function slugify(text: string) {
+ return text
+ .normalize("NFD")
+ .replace(/[̀-ͯ]/g, "")
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-|-$/g, "");
+}
diff --git a/geradoresfe/src/lib/useGenerators.tsx b/geradoresfe/src/lib/useGenerators.tsx
new file mode 100644
index 0000000..0828ad2
--- /dev/null
+++ b/geradoresfe/src/lib/useGenerators.tsx
@@ -0,0 +1,47 @@
+import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
+import { api, type Generator } from "./api";
+
+type GeneratorsState = {
+ generators: Generator[];
+ loading: boolean;
+ error: string | null;
+ byCategory: [string, Generator[]][];
+ find: (id: string) => Generator | undefined;
+};
+
+const GeneratorsContext = createContext(undefined);
+
+/** Carrega a lista de geradores uma vez; toda a UI é construída a partir destes metadados. */
+export function GeneratorsProvider({ children }: { children: ReactNode }) {
+ const [generators, setGenerators] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ api.listGenerators()
+ .then(setGenerators)
+ .catch((e: Error) => setError(e.message))
+ .finally(() => setLoading(false));
+ }, []);
+
+ const value = useMemo(() => {
+ const groups = new Map();
+ for (const g of generators) groups.set(g.category, [...(groups.get(g.category) ?? []), g]);
+ return {
+ generators,
+ loading,
+ error,
+ byCategory: [...groups.entries()],
+ find: (id) => generators.find((g) => g.id.toLowerCase() === id.toLowerCase()),
+ };
+ }, [generators, loading, error]);
+
+ return {children};
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export function useGenerators() {
+ const ctx = useContext(GeneratorsContext);
+ if (!ctx) throw new Error("useGenerators tem de estar dentro de GeneratorsProvider");
+ return ctx;
+}
diff --git a/geradoresfe/src/lib/useTitle.ts b/geradoresfe/src/lib/useTitle.ts
new file mode 100644
index 0000000..a13ad4b
--- /dev/null
+++ b/geradoresfe/src/lib/useTitle.ts
@@ -0,0 +1,7 @@
+import { useEffect } from "react";
+
+export function useTitle(title?: string) {
+ useEffect(() => {
+ document.title = title ? `${title} · Factory Id` : "Factory Id — Geradores e validadores";
+ }, [title]);
+}
diff --git a/geradoresfe/src/library/utils.tsx b/geradoresfe/src/library/utils.tsx
deleted file mode 100644
index b3a9a43..0000000
--- a/geradoresfe/src/library/utils.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-
-const Utils = {
- EnumToOptions,
- // Outras fun��es podem ser adicionadas aqui
-};
-
-export function EnumToOptions>(enumObj: T): any[] {
- return Object.entries(enumObj)
- .filter(([key]) => isNaN(Number(key))) // Ignora entradas num�ricas, caso existam
- .map(([key, value]) => ({
- value: key, // A chave do enum como `value`
- label: value, // O valor do enum como `label`
- }));
-}
-
-export function getEnumKeyByValue>(
- enumObj: T,
- value: string
-): string | undefined {
- return Object.keys(enumObj).find((key) => enumObj[key as keyof T] === value);
-}
-
-
-
-export default Utils;
\ No newline at end of file
diff --git a/geradoresfe/src/main.tsx b/geradoresfe/src/main.tsx
index 468d7d3..07ff6bd 100644
--- a/geradoresfe/src/main.tsx
+++ b/geradoresfe/src/main.tsx
@@ -1,13 +1,13 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
-import { ThemeProvider } from './context/ThemeContext.tsx'
+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(
+createRoot(document.getElementById("root")!).render(
,
-)
+);
diff --git a/geradoresfe/src/pages/Blank.tsx b/geradoresfe/src/pages/Blank.tsx
deleted file mode 100644
index f084490..0000000
--- a/geradoresfe/src/pages/Blank.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-export default function Blank() {
- console.log("inicalizar componente");
- return (
-
-
-
-
-
- Card Title Here
-
-
-
- Start putting content on grids or panels, you can also use different
- combinations of grids.Please check out the dashboard and other pages
-
-
-
-
- );
-}
diff --git a/geradoresfe/src/pages/CC.tsx b/geradoresfe/src/pages/CC.tsx
deleted file mode 100644
index a5ece76..0000000
--- a/geradoresfe/src/pages/CC.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useEffect } from "react";
-import { useLocation } from "react-router";
-import GenerateCC from "../components/Geradores/Tipos/GenerateCC";
-
-export default function CC() {
- const location = useLocation();
-
- // Efeito para rolar até a seção correspondente ao path
- useEffect(() => {
- const featuresSection = document.getElementById(location.hash.substring(1));
- if (featuresSection) {
- featuresSection.scrollIntoView({ behavior: "smooth" });
- }
-
- }, [location]);
-
- return (
-
-
- {/* Seção de Funcionalidades */}
-
-
-
- );
-}
diff --git a/geradoresfe/src/pages/GeneratorPage.tsx b/geradoresfe/src/pages/GeneratorPage.tsx
new file mode 100644
index 0000000..4ad9cad
--- /dev/null
+++ b/geradoresfe/src/pages/GeneratorPage.tsx
@@ -0,0 +1,267 @@
+import { useEffect, useState, type FormEvent } from "react";
+import { Link, useParams, useSearchParams } from "react-router";
+import { toast } from "react-hot-toast";
+import {
+ ArrowLeftIcon, CopyIcon, FileJsonIcon, FileSpreadsheetIcon, FileTextIcon, RefreshCwIcon, SearchCheckIcon, Trash2Icon,
+} from "lucide-react";
+import { api, defaultOptions, type GeneratedItem, type Generator, type ValidationResult } from "../lib/api";
+import { columnsOf, download, rowOf, toCsv, toJson, toText } from "../lib/export";
+import { copyToClipboard } from "../lib/clipboard";
+import { useGenerators } from "../lib/useGenerators";
+import { useTitle } from "../lib/useTitle";
+import { OptionsForm } from "../components/OptionsForm";
+import { ChecksumBadge } from "../components/ChecksumBadge";
+import { LoadState } from "../components/LoadState";
+import { ValidationResultView } from "../components/ValidationResultView";
+import { Button } from "../components/ui/Button";
+import { Input } from "../components/ui/Input";
+import NotFoundPage from "./NotFoundPage";
+
+const MAX = 1000;
+const QUICK_COUNTS = [1, 10, 100, 1000];
+
+export default function GeneratorPage() {
+ const { id = "" } = useParams();
+ const { find, loading } = useGenerators();
+ const generator = find(id);
+
+ if (!loading && !generator) return ;
+
+ return (
+
+
+
Todos os geradores
+
+
{generator && }
+
+ );
+}
+
+function GeneratorWorkspace({ generator }: { generator: Generator }) {
+ useTitle(`Gerador de ${generator.name}`);
+
+ // As opções e a quantidade ficam no URL para se poder partilhar a configuração.
+ const [searchParams, setSearchParams] = useSearchParams();
+ const options = { ...defaultOptions(generator) };
+ for (const o of generator.options) {
+ const v = searchParams.get(o.key);
+ if (v && o.choices.some((c) => c.value === v)) options[o.key] = v;
+ }
+ const count = Math.min(MAX, Math.max(1, Number(searchParams.get("n")) || 10));
+
+ const update = (patch: Record) =>
+ setSearchParams((prev) => {
+ const next = new URLSearchParams(prev);
+ for (const [k, v] of Object.entries(patch)) next.set(k, v);
+ return next;
+ }, { replace: true });
+
+ const [items, setItems] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ const generate = async () => {
+ setLoading(true);
+ try {
+ setItems(await api.generate(generator.id, options, count));
+ } catch (e) {
+ toast.error((e as Error).message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Mostrar logo resultados ao abrir a página.
+ useEffect(() => {
+ void generate();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const filename =`${generator.id}-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "")}`;
+ const apiExample = `/api/v1/generators/${generator.id}/generate?${new URLSearchParams({ count: String(count), ...options })}`;
+
+ return (
+
+
+
+
+ {items.length === 0 ? (
+
+
+
Escolha as opções e carregue em Gerar.
+
+ ) : (
+ setItems([])}
+ onCsv={() => download(toCsv(items), `${filename}.csv`, "text/csv;charset=utf-8")}
+ onJson={() => download(toJson(items), `${filename}.json`, "application/json")}
+ onTxt={() => download(toText(items), `${filename}.txt`, "text/plain;charset=utf-8")}
+ />
+ )}
+
+
+ );
+}
+
+type ResultsProps = {
+ generator: Generator;
+ items: GeneratedItem[];
+ onClear: () => void;
+ onCsv: () => void;
+ onJson: () => void;
+ onTxt: () => void;
+};
+
+function Results({ generator, items, onClear, onCsv, onJson, onTxt }: ResultsProps) {
+ const columns = columnsOf(items);
+
+ return (
+
+
+
+ {items.length} {items.length === 1 ? "valor gerado" : "valores gerados"}
+
+
+
}>CSV
+
}>JSON
+
}>TXT
+
} aria-label="Limpar" title="Limpar" />
+
+
+
+
+
+
+ | # |
+ {columns.map((c) => (
+ {c} |
+ ))}
+ Copiar |
+
+
+
+ {items.map((item, i) => (
+
+ | {i + 1} |
+ {rowOf(item, columns).map((cell, j) => (
+ {cell} |
+ ))}
+
+
+ |
+
+ ))}
+
+
+
+
+ );
+}
+
+function ValidateBox({ generator }: { generator: Generator }) {
+ const [value, setValue] = useState("");
+ const [result, setResult] = useState(null);
+ const [loading, setLoading] = useState(false);
+
+ const submit = async (e: FormEvent) => {
+ e.preventDefault();
+ if (!value.trim()) return;
+ setLoading(true);
+ try {
+ setResult(await api.validate(generator.id, value));
+ } catch (err) {
+ toast.error((err as Error).message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/geradoresfe/src/pages/Home.tsx b/geradoresfe/src/pages/Home.tsx
deleted file mode 100644
index da07126..0000000
--- a/geradoresfe/src/pages/Home.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { useEffect } from "react";
-import { useLocation } from "react-router";
-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() {
- const location = useLocation(); // Pegando a rota atual
-
- // Efeito para rolar até a seção correspondente ao path
- useEffect(() => {
- const featuresSection = document.getElementById(location.hash.substring(1));
- if (featuresSection) {
- featuresSection.scrollIntoView({ behavior: "smooth" });
- }
-
- }, [location]);
-
- return (
-
-
- {/* Seção de Funcionalidades */}
-
-
-
- );
-}
diff --git a/geradoresfe/src/pages/HomePage.tsx b/geradoresfe/src/pages/HomePage.tsx
new file mode 100644
index 0000000..7bb53fc
--- /dev/null
+++ b/geradoresfe/src/pages/HomePage.tsx
@@ -0,0 +1,40 @@
+import { GeneratorCard } from "../components/GeneratorCard";
+import { Validator } from "../components/Validator";
+import { LoadState } from "../components/LoadState";
+import { useGenerators } from "../lib/useGenerators";
+import { useTitle } from "../lib/useTitle";
+import { slugify } from "../lib/slug";
+
+export default function HomePage() {
+ useTitle();
+ const { byCategory } = useGenerators();
+
+ return (
+
+
+
+ Dados portugueses válidos para testes
+
+
+ Gere e valide NIF, NISS, Cartão de Cidadão, IBAN, cartões, matrículas e muito mais — um a um ou aos milhares.
+
+
+
+
+
+
+
+ {byCategory.map(([category, generators]) => (
+
+ {category}
+
+ {generators.map((g) => (
+
+ ))}
+
+
+ ))}
+
+
+ );
+}
diff --git a/geradoresfe/src/pages/NIF.tsx b/geradoresfe/src/pages/NIF.tsx
deleted file mode 100644
index 122fcb5..0000000
--- a/geradoresfe/src/pages/NIF.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useEffect } from "react";
-import { useLocation } from "react-router";
-import GenerateNIF from "../components/Geradores/Tipos/GenerateNIF";
-
-export default function NIF() {
- const location = useLocation();
-
- // Efeito para rolar até a seção correspondente ao path
- useEffect(() => {
- const featuresSection = document.getElementById(location.hash.substring(1));
- if (featuresSection) {
- featuresSection.scrollIntoView({ behavior: "smooth" });
- }
-
- }, [location]);
-
- return (
-
-
- {/* Seção de Funcionalidades */}
-
-
-
- );
-}
diff --git a/geradoresfe/src/pages/NISS.tsx b/geradoresfe/src/pages/NISS.tsx
deleted file mode 100644
index 8f28f8d..0000000
--- a/geradoresfe/src/pages/NISS.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useEffect } from "react";
-import { useLocation } from "react-router";
-import GenerateNISS from "../components/Geradores/Tipos/GenerateNISS";
-
-export default function NISS() {
- const location = useLocation();
-
- // Efeito para rolar até a seção correspondente ao path
- useEffect(() => {
- const featuresSection = document.getElementById(location.hash.substring(1));
- if (featuresSection) {
- featuresSection.scrollIntoView({ behavior: "smooth" });
- }
-
- }, [location]);
-
- return (
-
-
- {/* Seção de Funcionalidades */}
-
-
-
- );
-}
diff --git a/geradoresfe/src/pages/NotFoundPage.tsx b/geradoresfe/src/pages/NotFoundPage.tsx
new file mode 100644
index 0000000..b7dd0ad
--- /dev/null
+++ b/geradoresfe/src/pages/NotFoundPage.tsx
@@ -0,0 +1,16 @@
+import { Link } from "react-router";
+import { useTitle } from "../lib/useTitle";
+
+export default function NotFoundPage() {
+ useTitle("Página não encontrada");
+ return (
+
+
404
+
Página não encontrada
+
O endereço que procura não existe.
+
+ Voltar ao início
+
+
+ );
+}
diff --git a/geradoresfe/src/pages/ValidatorPage.tsx b/geradoresfe/src/pages/ValidatorPage.tsx
new file mode 100644
index 0000000..df94454
--- /dev/null
+++ b/geradoresfe/src/pages/ValidatorPage.tsx
@@ -0,0 +1,24 @@
+import { Validator } from "../components/Validator";
+import { useGenerators } from "../lib/useGenerators";
+import { useTitle } from "../lib/useTitle";
+
+export default function ValidatorPage() {
+ useTitle("Validador");
+ const { generators } = useGenerators();
+ const supported = generators.filter((g) => g.canValidate).map((g) => g.name);
+
+ return (
+
+
Validador
+
+ Cole qualquer valor: identificamos o tipo automaticamente e explicamos se é válido — e, se não for, porquê.
+
+
+
+
+ {supported.length > 0 && (
+
Tipos suportados: {supported.join(", ")}.
+ )}
+
+ );
+}
diff --git a/geradoresfe/src/service/api.ts b/geradoresfe/src/service/api.ts
deleted file mode 100644
index 702a3c5..0000000
--- a/geradoresfe/src/service/api.ts
+++ /dev/null
@@ -1,333 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-/*
- * ---------------------------------------------------------------
- * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
- * ## ##
- * ## AUTHOR: acacode ##
- * ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
- * ---------------------------------------------------------------
- */
-
-export enum NIFType {
- /** Pessoa singular (1) */
- PessoaSingular1 = "Pessoa singular (1)",
- /** Pessoa singular (2) */
- PessoaSingular2 = "Pessoa singular (2)",
- /** Pessoa singular (3; novo em 2019) */
- PessoaSingular3 = "Pessoa singular (3; novo em 2019)",
- /** Pessoa colectiva (5) */
- PessoaColectiva = "Pessoa colectiva (5)",
- /** Pessoa colectiva pública (6) */
- PessoaColectivaPublica = "Pessoa colectiva pública (6)",
- /** Empresário em nome individual (8) */
- EmpresarioIndividual = "Empresário em nome individual (8)",
- /** Pessoa colectiva irregular ou número provisório (9) */
- PessoaColectivaIrregular = "Pessoa colectiva irregular ou número provisório (9)",
-}
-
-export interface WeatherForecast {
- /** @format date */
- date?: string;
- /** @format int32 */
- temperatureC?: number;
- /** @format int32 */
- temperatureF?: number;
- summary?: string | null;
-}
-
-import type {
- AxiosInstance,
- AxiosRequestConfig,
- AxiosResponse,
- HeadersDefaults,
- ResponseType,
-} from "axios";
-import axios from "axios";
-
-export type QueryParamsType = Record;
-
-export interface FullRequestParams
- extends Omit {
- /** set parameter to `true` for call `securityWorker` for this request */
- secure?: boolean;
- /** request path */
- path: string;
- /** content type of request body */
- type?: ContentType;
- /** query params */
- query?: QueryParamsType;
- /** format of response (i.e. response.json() -> format: "json") */
- format?: ResponseType;
- /** request body */
- body?: unknown;
-}
-
-export type RequestParams = Omit<
- FullRequestParams,
- "body" | "method" | "query" | "path"
->;
-
-export interface ApiConfig
- extends Omit {
- securityWorker?: (
- securityData: SecurityDataType | null,
- ) => Promise | AxiosRequestConfig | void;
- secure?: boolean;
- format?: ResponseType;
-}
-
-export enum ContentType {
- Json = "application/json",
- JsonApi = "application/vnd.api+json",
- FormData = "multipart/form-data",
- UrlEncoded = "application/x-www-form-urlencoded",
- Text = "text/plain",
-}
-
-export class HttpClient {
- public instance: AxiosInstance;
- private securityData: SecurityDataType | null = null;
- private securityWorker?: ApiConfig["securityWorker"];
- private secure?: boolean;
- private format?: ResponseType;
-
- constructor({
- securityWorker,
- secure,
- format,
- ...axiosConfig
- }: ApiConfig = {}) {
- this.instance = axios.create({
- ...axiosConfig,
- baseURL: axiosConfig.baseURL || "",
- });
- this.secure = secure;
- this.format = format;
- this.securityWorker = securityWorker;
- }
-
- public setSecurityData = (data: SecurityDataType | null) => {
- this.securityData = data;
- };
-
- protected mergeRequestParams(
- params1: AxiosRequestConfig,
- params2?: AxiosRequestConfig,
- ): AxiosRequestConfig {
- const method = params1.method || (params2 && params2.method);
-
- return {
- ...this.instance.defaults,
- ...params1,
- ...(params2 || {}),
- headers: {
- ...((method &&
- this.instance.defaults.headers[
- method.toLowerCase() as keyof HeadersDefaults
- ]) ||
- {}),
- ...(params1.headers || {}),
- ...((params2 && params2.headers) || {}),
- },
- };
- }
-
- protected stringifyFormItem(formItem: unknown) {
- if (typeof formItem === "object" && formItem !== null) {
- return JSON.stringify(formItem);
- } else {
- return `${formItem}`;
- }
- }
-
- protected createFormData(input: Record): FormData {
- if (input instanceof FormData) {
- return input;
- }
- return Object.keys(input || {}).reduce((formData, key) => {
- const property = input[key];
- const propertyContent: any[] =
- property instanceof Array ? property : [property];
-
- for (const formItem of propertyContent) {
- const isFileType = formItem instanceof Blob || formItem instanceof File;
- formData.append(
- key,
- isFileType ? formItem : this.stringifyFormItem(formItem),
- );
- }
-
- return formData;
- }, new FormData());
- }
-
- public request = async ({
- secure,
- path,
- type,
- query,
- format,
- body,
- ...params
- }: FullRequestParams): Promise> => {
- const secureParams =
- ((typeof secure === "boolean" ? secure : this.secure) &&
- this.securityWorker &&
- (await this.securityWorker(this.securityData))) ||
- {};
- const requestParams = this.mergeRequestParams(params, secureParams);
- const responseFormat = format || this.format || undefined;
-
- if (
- type === ContentType.FormData &&
- body &&
- body !== null &&
- typeof body === "object"
- ) {
- body = this.createFormData(body as Record);
- }
-
- if (
- type === ContentType.Text &&
- body &&
- body !== null &&
- typeof body !== "string"
- ) {
- body = JSON.stringify(body);
- }
-
- return this.instance.request({
- ...requestParams,
- headers: {
- ...(requestParams.headers || {}),
- ...(type ? { "Content-Type": type } : {}),
- },
- params: query,
- responseType: responseFormat,
- data: body,
- url: path,
- });
- };
-}
-
-/**
- * @title FactoryId
- * @version v1
- * @contact Marco Santos
- */
-export class Api<
- SecurityDataType extends unknown,
-> extends HttpClient {
- generate = {
- /**
- * No description
- *
- * @tags Generate
- * @name GenerateNifList
- * @request GET:/Generate/GenerateNIF
- */
- generateNifList: (
- query?: {
- type?: NIFType;
- },
- params: RequestParams = {},
- ) =>
- this.request({
- path: `/Generate/GenerateNIF`,
- method: "GET",
- query: query,
- format: "json",
- ...params,
- }),
-
- /**
- * No description
- *
- * @tags Generate
- * @name ValidateNifList
- * @request GET:/Generate/ValidateNIF
- */
- validateNifList: (
- query?: {
- nif?: string;
- },
- params: RequestParams = {},
- ) =>
- this.request({
- path: `/Generate/ValidateNIF`,
- method: "GET",
- query: query,
- format: "json",
- ...params,
- }),
-
- /**
- * No description
- *
- * @tags Generate
- * @name GenerateNissList
- * @request GET:/Generate/GenerateNISS
- */
- generateNissList: (params: RequestParams = {}) =>
- this.request({
- path: `/Generate/GenerateNISS`,
- method: "GET",
- format: "json",
- ...params,
- }),
-
- /**
- * No description
- *
- * @tags Generate
- * @name ValidateNissList
- * @request GET:/Generate/ValidateNISS
- */
- validateNissList: (
- query?: {
- nif?: string;
- },
- params: RequestParams = {},
- ) =>
- this.request({
- path: `/Generate/ValidateNISS`,
- method: "GET",
- query: query,
- format: "json",
- ...params,
- }),
-
- /**
- * No description
- *
- * @tags Generate
- * @name GenerateCcList
- * @request GET:/Generate/GenerateCC
- */
- generateCcList: (params: RequestParams = {}) =>
- this.request({
- path: `/Generate/GenerateCC`,
- method: "GET",
- format: "json",
- ...params,
- }),
- };
- weatherForecast = {
- /**
- * No description
- *
- * @tags WeatherForecast
- * @name GetWeatherForecast
- * @request GET:/WeatherForecast
- */
- getWeatherForecast: (params: RequestParams = {}) =>
- this.request({
- path: `/WeatherForecast`,
- method: "GET",
- format: "json",
- ...params,
- }),
- };
-}
diff --git a/geradoresfe/tailwind.config.js b/geradoresfe/tailwind.config.js
deleted file mode 100644
index 0d90afd..0000000
--- a/geradoresfe/tailwind.config.js
+++ /dev/null
@@ -1,196 +0,0 @@
-const defaultTheme = require("tailwindcss/defaultTheme");
-// @type {import('tailwindcss').Config}
-module.exports = {
- content: [
- "./index.html",
- "./src/**/*.{js,jsx,ts,tsx}"],
- darkMode: "class",
- theme: {
- fontFamily: {
- outfit: ["Outfit", "sans-serif"],
- },
- screens: {
- "2xsm": "375px",
- xsm: "425px",
- "3xl": "2000px",
- ...defaultTheme.screens,
- },
- extend: {
- fontSize: {
- "title-2xl": ["72px", "90px"],
- "title-xl": ["60px", "72px"],
- "title-lg": ["48px", "60px"],
- "title-md": ["36px", "44px"],
- "title-sm": ["30px", "38px"],
- "theme-xl": ["20px", "30px"],
- "theme-sm": ["14px", "20px"],
- "theme-xs": ["12px", "18px"],
- },
- colors: {
- primary: "#2175d9",
- "primary-dark": "#1c5fb3",
- current: "currentColor",
- transparent: "transparent",
- white: "#FFFFFF",
- black: "#101828",
- brand: {
- 25: "#F2F7FF",
- 50: "#ECF3FF",
- 100: "#DDE9FF",
- 200: "#C2D6FF",
- 300: "#9CB9FF",
- 400: "#7592FF",
- 500: "#465FFF",
- 600: "#3641F5",
- 700: "#2A31D8",
- 800: "#252DAE",
- 900: "#262E89",
- 950: "#161950",
- },
- "blue-light": {
- 25: "#F5FBFF",
- 50: "#F0F9FF",
- 100: "#E0F2FE",
- 200: "#B9E6FE",
- 300: "#7CD4FD",
- 400: "#36BFFA",
- 500: "#0BA5EC",
- 600: "#0086C9",
- 700: "#026AA2",
- 800: "#065986",
- 900: "#0B4A6F",
- 950: "#062C41",
- },
- gray: {
- dark: "#1A2231",
- 25: "#FCFCFD",
- 50: "#F9FAFB",
- 100: "#F2F4F7",
- 200: "#E4E7EC",
- 300: "#D0D5DD",
- 400: "#98A2B3",
- 500: "#667085",
- 600: "#475467",
- 700: "#344054",
- 800: "#1D2939",
- 900: "#101828",
- 950: "#0C111D",
- },
- orange: {
- 25: "#FFFAF5",
- 50: "#FFF6ED",
- 100: "#FFEAD5",
- 200: "#FDDCAB",
- 300: "#FEB273",
- 400: "#FD853A",
- 500: "#FB6514",
- 600: "#EC4A0A",
- 700: "#C4320A",
- 800: "#9C2A10",
- 900: "#7E2410",
- 950: "#511C10",
- },
- success: {
- 25: "#F6FEF9",
- 50: "#ECFDF3",
- 100: "#D1FADF",
- 200: "#A6F4C5",
- 300: "#6CE9A6",
- 400: "#32D583",
- 500: "#12B76A",
- 600: "#039855",
- 700: "#027A48",
- 800: "#05603A",
- 900: "#054F31",
- 950: "#053321",
- },
- error: {
- 25: "#FFFBFA",
- 50: "#FEF3F2",
- 100: "#FEE4E2",
- 200: "#FECDCA",
- 300: "#FDA29B",
- 400: "#F97066",
- 500: "#F04438",
- 600: "#D92D20",
- 700: "#B42318",
- 800: "#912018",
- 900: "#7A271A",
- 950: "#55160C",
- },
- warning: {
- 25: "#FFFCF5",
- 50: "#FFFAEB",
- 100: "#FEF0C7",
- 200: "#FEDF89",
- 300: "#FEC84B",
- 400: "#FDB022",
- 500: "#F79009",
- 600: "#DC6803",
- 700: "#B54708",
- 800: "#93370D",
- 900: "#7A2E0E",
- 950: "#4E1D09",
- },
- "theme-pink": {
- 500: "#EE46BC",
- },
- "theme-purple": {
- 500: "#7A5AF8",
- },
- },
- boxShadow: {
- "theme-md":
- "0px 4px 8px -2px rgba(16, 24, 40, 0.10), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)",
- "theme-lg":
- "0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)",
-
- "theme-sm":
- "0px 1px 3px 0px rgba(16, 24, 40, 0.10), 0px 1px 2px 0px rgba(16, 24, 40, 0.06)",
- "theme-xs": "0px 1px 2px 0px rgba(16, 24, 40, 0.05)",
- "theme-xl":
- "0px 20px 24px -4px rgba(16, 24, 40, 0.08), 0px 8px 8px -4px rgba(16, 24, 40, 0.03)",
- datepicker: "-5px 0 0 #262d3c, 5px 0 0 #262d3c",
- "focus-ring": "0px 0px 0px 4px rgba(70, 95, 255, 0.12)",
- "slider-navigation":
- "0px 1px 2px 0px rgba(16, 24, 40, 0.10), 0px 1px 3px 0px rgba(16, 24, 40, 0.10)",
- tooltip:
- "0px 4px 6px -2px rgba(16, 24, 40, 0.05), -8px 0px 20px 8px rgba(16, 24, 40, 0.05)",
- },
- dropShadow: {
- "4xl": [
- "0 35px 35px rgba(0, 0, 0, 0.25)",
- "0 45px 65px rgba(0, 0, 0, 0.15)",
- ],
- },
- zIndex: {
- 999999: "999999",
- 99999: "99999",
- 9999: "9999",
- 999: "999",
- 99: "99",
- 9: "9",
- 1: "1",
- },
- spacing: {
- 4.5: "1.125rem",
- 5.5: "1.375rem",
- 6.5: "1.625rem",
- 7.5: "1.875rem",
- 8.5: "2.125rem",
- 9.5: "2.375rem",
- 10.5: "2.625rem",
- 11.5: "2.875rem",
- 12.5: "3.125rem",
- 13: "3.25rem",
- 13.5: "3.375rem",
- 14.5: "3.625rem",
- 15: "3.75rem",
- },
- },
- },
- plugins: [
- require("@tailwindcss/forms"),
- require('tailwindcss-animate')
- ],
-};
diff --git a/geradoresfe/vite.config.ts b/geradoresfe/vite.config.ts
index f29bd0d..66a5269 100644
--- a/geradoresfe/vite.config.ts
+++ b/geradoresfe/vite.config.ts
@@ -1,11 +1,22 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-import tailwindcss from '@tailwindcss/vite'
+///
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
+
+// Em dev, /api e /docs vão para o backend (dotnet run → http://localhost:5053).
+const apiTarget = process.env.API_PROXY_TARGET ?? "http://localhost:5053";
-// https://vite.dev/config/
export default defineConfig({
- plugins: [
- react(),
- tailwindcss(),
- ],
-})
+ plugins: [react(), tailwindcss()],
+ server: {
+ port: 3000,
+ proxy: {
+ "/api": apiTarget,
+ "/docs": apiTarget,
+ "/openapi": apiTarget,
+ },
+ },
+ test: {
+ environment: "node",
+ },
+});
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 6cb6b54..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Geradores",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {}
-}
diff --git a/package.json b/package.json
deleted file mode 100644
index 0967ef4..0000000
--- a/package.json
+++ /dev/null
@@ -1 +0,0 @@
-{}