From b80c5c54a2fa390c6c943ad8111e2ffddc6dcefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= <69174195+fonteeboa@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:28:08 -0300 Subject: [PATCH 1/2] front: melhorando estrutura e visual para algo mais limpo ao usuario --- .gitignore | 3 + apps/web/app/globals.css | 68 ++++ apps/web/app/layout.tsx | 28 +- apps/web/app/page.tsx | 286 +++++++------- apps/web/components/CustomLoading.tsx | 117 ++++++ apps/web/components/EthicsPage.tsx | 55 +++ apps/web/components/LoadingScreen.tsx | 25 ++ apps/web/components/MindmapPreview.tsx | 501 +++++++++++++++++++++---- apps/web/components/ResultsScreen.tsx | 22 ++ apps/web/components/SearchScreen.tsx | 46 +++ apps/web/components/theme-provider.tsx | 67 ++++ apps/web/lib/constants.ts | 4 +- apps/web/lib/utils.ts | 4 + apps/web/next-env.d.ts | 1 - apps/web/package.json | 15 +- apps/web/tsconfig.json | 30 +- apps/web/types/index.ts | 79 ++++ 17 files changed, 1112 insertions(+), 239 deletions(-) create mode 100644 apps/web/app/globals.css create mode 100644 apps/web/components/CustomLoading.tsx create mode 100644 apps/web/components/EthicsPage.tsx create mode 100644 apps/web/components/LoadingScreen.tsx create mode 100644 apps/web/components/ResultsScreen.tsx create mode 100644 apps/web/components/SearchScreen.tsx create mode 100644 apps/web/components/theme-provider.tsx create mode 100644 apps/web/lib/utils.ts create mode 100644 apps/web/types/index.ts diff --git a/.gitignore b/.gitignore index ca31811..6241a42 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ dist/ build/ .vercel/ .cache/ + +# Build +apps/web/.next/ \ No newline at end of file diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css new file mode 100644 index 0000000..18323ca --- /dev/null +++ b/apps/web/app/globals.css @@ -0,0 +1,68 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55% + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 5a083cb..028fc71 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,30 +1,16 @@ -import type { ReactNode } from 'react'; -import Link from 'next/link'; - +import { AppThemeProvider } from "@/components/theme-provider"; +import type { RootLayoutProps } from "../types"; export const metadata = { - title: 'AliasMap', - description: 'OSINT username mapping with ethical, public-only collection.' + title: "AliasMap", + description: "OSINT username mapping with ethical, public-only collection.", }; -export default function RootLayout({ children }: { children: ReactNode }) { +export default function RootLayout({ children }: RootLayoutProps) { return ( - -
- -
-
{children}
- + + {children} ); } - diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 5a99e84..dfc1d67 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,159 +1,185 @@ -'use client'; - -import { useMemo, useState } from 'react'; -import Link from 'next/link'; -import { EthicsBanner } from '../components/EthicsBanner'; -import { buildMindmap } from '../lib/mindmap'; -import { platformLabel } from '../lib/platforms'; -import { MindmapPreview } from '../components/MindmapPreview'; - -type SiteEvent = - | { type: 'site_start'; id: string } - | { type: 'site_result'; id: string; status: string; url?: string; latencyMs?: number; reason?: string; evidence?: { kind: string; value: string }[]; heuristic?: boolean } - | { type: 'site_error'; id: string; reason: string } - | { type: 'progress'; done: number; total: number } - | { type: 'done'; summary: { done: number; total: number } }; - -export default function Home() { - const [username, setUsername] = useState(''); - const [events, setEvents] = useState([]); - const [running, setRunning] = useState(false); - // Show only truly found; do not coerce 'inconclusive' to 'found' - - const canStart = useMemo(() => { - return !running && username.trim().length > 0; - }, [running, username]); - - const identityStatus = (s: string) => s; - - // Progress is intentionally hidden from the UI per request. - - const latestByPlatform = useMemo(() => { - type SiteResultEvent = Extract; - type UiResult = (SiteResultEvent & { platform: string; rawStatus: SiteResultEvent['status']; heuristic?: boolean }) & { - status: 'found' | 'not_found' | 'inconclusive' | string; - }; - const map = new Map(); +"use client"; + +import * as React from "react"; +import Image from "next/image"; +import logo from "./icon.svg"; +import { Box, Container, Paper, Stack, Typography, IconButton, Tooltip } from "@mui/material"; +import RestartAltIcon from "@mui/icons-material/RestartAlt"; +import GavelIcon from "@mui/icons-material/Gavel"; +import SearchScreen from "@/components/SearchScreen"; +import Loading from "@/components/LoadingScreen"; +import ResultsScreen from "@/components/ResultsScreen"; +import EthicsPage from "@/components/EthicsPage"; +import type { SiteEvent, Status, MindmapItem } from "../types"; + +const normalizeStatus = (s: string): Status => { + const v = s.toLowerCase(); + if (v.includes("found") || v === "200") return "found"; + if (v.includes("not") || v === "404") return "not_found"; + if (v.includes("timeout") || v.includes("error")) return "inconclusive"; + return s as Status; +}; + +export default function HomeStepsSimple() { + const [step, setStep] = React.useState<0 | 1 | 2 | 3>(0); + const [username, setUsername] = React.useState(""); + const [running, setRunning] = React.useState(false); + const [error, setError] = React.useState(null); + const [events, setEvents] = React.useState([]); + const [progress, setProgress] = React.useState<{ done: number; total: number }>({ + done: 0, + total: 0, + }); + const esRef = React.useRef(null); + + const foundOnly: MindmapItem[] = React.useMemo(() => { + const latestByPlatform = new Map>(); for (const e of events) { - if (e.type === 'site_result') { - const label = platformLabel(e.id); - map.set(e.id, { ...e, rawStatus: e.status, status: identityStatus(e.status), platform: label }); - } + if (e.type === "site_result") latestByPlatform.set(e.id, e); } - return Array.from(map.values()); + return Array.from(latestByPlatform.values()) + .filter((e) => normalizeStatus(e.status) === "found") + .map((e) => ({ + platform: e.id, + status: "found" as Status, + rawStatus: e.status as Status, + heuristic: e.heuristic, + url: e.url, + })); }, [events]); - const startScan = () => { - if (!username) return; + const hasData = foundOnly.length > 0; + + const startScan = (name: string) => { + if (!name) return; + const isValid = /^[a-zA-Z0-9._-]{3,32}$/.test(name); + if (!isValid) { + setError("Username inválido. Use 3–32 caracteres: letras, números, ponto, traço e sublinhado."); + return; + } + const normalizedUsername = name.trim().toLowerCase(); + setUsername(normalizedUsername); + + if (esRef.current) { + esRef.current.close(); + esRef.current = null; + } + setStep(1); setEvents([]); + setError(null); + setProgress({ done: 0, total: 0 }); setRunning(true); - const es = new EventSource(`/api/scan?username=${encodeURIComponent(username)}&tier=all`); + + const es = new EventSource(`/api/scan?username=${encodeURIComponent(normalizedUsername)}&tier=all`); + esRef.current = es; es.onmessage = (msg) => { try { const data = JSON.parse(msg.data) as SiteEvent; - setEvents(prev => [...prev, data]); - if (data.type === 'done') { + setEvents((prev) => [...prev, data]); + if (data.type === "progress") { + setProgress({ done: data.done, total: data.total }); + } + + if (data.type === "done") { es.close(); + esRef.current = null; setRunning(false); + setStep(2); } - } catch (e) { - // ignore parse errors in stub + } catch (error) { + console.error("Erro ao processar evento SSE:", error); } }; es.onerror = () => { es.close(); + esRef.current = null; setRunning(false); + setError("Falha na conexão de varredura (SSE). Tente novamente."); }; }; - const exportJson = () => { - const mindmap = buildMindmap(username, events); - const header = { - tool: 'AliasMap', - note: - 'Dados coletados de fontes públicas. Uso responsável. Nenhuma persistência no servidor. Consulte o Aviso e Uso Ético.' - }; - const payload = { header, username, events, mindmap }; - const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `aliasmap-${username || 'resultado'}.json`; - a.click(); - URL.revokeObjectURL(url); + const resetAll = () => { + setStep(0); + setUsername(""); + setEvents([]); + setError(null); + setProgress({ done: 0, total: 0 }); + setRunning(false); }; - const exportCsv = () => { - const header = 'tool=AliasMap;note=Dados coletados de fontes públicas; etica=/ethics\n'; - const rows = [['platform', 'status', 'url', 'latencyMs', 'reason', 'heuristic']]; - for (const e of events) { - if (e.type === 'site_result') { - const st = e.status; - rows.push([ - e.id, - st, - e.url ?? '', - String(e.latencyMs ?? ''), - e.reason ?? '', - e.heuristic ? 'yes' : '' - ]); - } - } - const csv = header + rows.map(r => r.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(',')).join('\n'); - const blob = new Blob([csv], { type: 'text/csv' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; a.download = `aliasmap-${username || 'resultado'}.csv`; a.click(); - URL.revokeObjectURL(url); + const handleEthicsClick = () => { + setStep(3); }; return ( -
- -

AliasMap

-

Mapeie possíveis perfis por username em plataformas públicas. Coleta ética e sem persistência.

-
- setUsername(e.target.value)} - onKeyDown={(e) => { if (e.key === 'Enter' && canStart) startScan(); }} - disabled={running} - style={{ padding: 8, border: '1px solid #d1d5db', borderRadius: 6 }} - /> - - - - Aviso e Uso Ético -
- - {/* Progress hidden */} - - {running ? ( -
- - Varredura em andamento… -
- ) : null} - -
-

Mindmap (pré-visualização)

- {/* Filter removed: always showing only found results */} -
- e.status === 'found')} - /> -
-
-
+ + + + + AliasMap logo + + AliasMap + + + + {step > 0 && ( + + + + + + )} + + {step === 0 && ( + + + + + + )} + + + + {step === 0 && } + {step === 1 && } + {step === 2 && ( + + )} + {step === 3 && } + + + ); } diff --git a/apps/web/components/CustomLoading.tsx b/apps/web/components/CustomLoading.tsx new file mode 100644 index 0000000..f8e8f2f --- /dev/null +++ b/apps/web/components/CustomLoading.tsx @@ -0,0 +1,117 @@ +"use client"; + +import * as React from "react"; +import Box from "@mui/material/Box"; +import { useTheme, alpha, lighten, darken } from "@mui/material/styles"; +import { keyframes } from "@mui/system"; +import type { CustomLoadingProps } from "../types"; + +const barAnim = keyframes` + 0% { background-position: left; } + 100% { background-position: right; } +`; +const searchAnim = keyframes` + 0% { transform: translateX(0%) rotate(70deg); } + 100% { transform: translateX(100px) rotate(10deg); } +`; + +export default function CustomLoadingScreen({ width = 130, barHeight = 8, ariaLabel = "Carregando" }: CustomLoadingProps) { + const theme = useTheme(); + const primary = theme.palette.primary.main; + const primaryLight = theme.palette.primary.light ?? lighten(primary, 0.35); + const primaryDark = theme.palette.primary.dark ?? darken(primary, 0.3); + const primarySoft = alpha(primary, 0.22); + + return ( + + + + + + + + + + ); +} diff --git a/apps/web/components/EthicsPage.tsx b/apps/web/components/EthicsPage.tsx new file mode 100644 index 0000000..74084d7 --- /dev/null +++ b/apps/web/components/EthicsPage.tsx @@ -0,0 +1,55 @@ +import { Container, Paper, Typography, Box, Alert, useTheme } from "@mui/material"; +import { AlertTriangle, Info, CheckCircle } from "lucide-react"; +import { disclaimerPt } from "../lib/constants"; + +export const metadata = { title: "Aviso e Uso Ético — AliasMap" }; + +export default function EthicsPage() { + const theme = useTheme(); + return ( + + } sx={{ borderRadius: 2, background: "transparent" }}> + + Leia atentamente todos os termos antes de utilizar nossos serviços + + + + + + + Aviso e Uso Ético + + + + {disclaimerPt} + + + + + + + Ao utilizar nossos serviços, você concorda com estes termos + + + + + ); +} diff --git a/apps/web/components/LoadingScreen.tsx b/apps/web/components/LoadingScreen.tsx new file mode 100644 index 0000000..317e0d8 --- /dev/null +++ b/apps/web/components/LoadingScreen.tsx @@ -0,0 +1,25 @@ +"use client"; + +import * as React from "react"; +import { Stack, Typography, LinearProgress, Alert } from "@mui/material"; +import CustomLoadingScreen from "./CustomLoading"; +import type { LoadingProps } from "../types"; + +export default function LoadingScreen({ progress, error }: LoadingProps) { + return ( + + Varredura em andamento + + + + + + {!!error && {error}} + + ); +} diff --git a/apps/web/components/MindmapPreview.tsx b/apps/web/components/MindmapPreview.tsx index c5edb49..ae3b7ba 100644 --- a/apps/web/components/MindmapPreview.tsx +++ b/apps/web/components/MindmapPreview.tsx @@ -1,88 +1,435 @@ -import React, { useMemo } from 'react'; - -type Item = { - platform: string; - status: 'found' | 'not_found' | 'inconclusive' | string; - rawStatus?: 'found' | 'not_found' | 'inconclusive' | string; - heuristic?: boolean; - url?: string; -}; - -export function MindmapPreview({ username, items }: { username: string; items: Item[] }) { - const width = 600; - const height = 420; - const cx = width / 2; - const cy = height / 2; - const radius = 140; - - const nodes = useMemo(() => { +"use client"; + +import * as React from "react"; +import { ExternalLink, Info, Dot, ShieldCheck } from "lucide-react"; +import { + Card, + CardHeader, + CardContent, + Divider, + Box, + Stack, + Typography, + Chip, + Tooltip, + useTheme, + Button, +} from "@mui/material"; +import DownloadIcon from "@mui/icons-material/Download"; +import type { MindmapPreviewProps, EdgeProps, LegendProps, SiteEvent } from "../types"; + +/* ===== Constantes/Utils ===== */ +const STATUS_COLORS = { + found: "#10B981", + inconclusive: "#F59E0B", + not_found: "#9CA3AF", + unknown: "#EF4444", +} as const; + +const NODE_R = 20; +const CORE_R = 34; + +function colorFor(status: string): string { + if (status === "found") return STATUS_COLORS.found; + if (status === "inconclusive") return STATUS_COLORS.inconclusive; + if (status === "not_found") return STATUS_COLORS.not_found; + return STATUS_COLORS.unknown; +} + +function isSafeHttpUrl(u?: string): u is string { + if (!u) return false; + try { + const url = new URL(u); + return url.protocol === "http:" || url.protocol === "https:"; + } catch { + return false; + } +} + +function truncate(s: string, max = 12) { + return s.length > max ? s.slice(0, max - 1) + "…" : s; +} + +function useMeasure() { + const ref = React.useRef(null); + const [rect, setRect] = React.useState<{ width: number; height: number }>({ width: 640, height: 420 }); + + React.useLayoutEffect(() => { + const el = ref.current; + if (!el) return; + const ro = new ResizeObserver((entries) => { + for (const e of entries) { + const cr = e.contentRect; + setRect({ width: Math.max(320, cr.width), height: Math.max(280, cr.height) }); + } + }); + ro.observe(el); + const r = el.getBoundingClientRect(); + setRect({ width: Math.max(320, r.width), height: Math.max(280, r.height) }); + return () => ro.disconnect(); + }, []); + + return { ref, rect }; +} + +export function MindmapPreview({ + username, + items, + width, + height, + radius = 150, + className, + maxLabel = 12, + events, + exportData = true, +}: MindmapPreviewProps) { + const theme = useTheme(); + const prefersReduced = + typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches; + + const { ref, rect } = useMeasure(); + const idPrefix = React.useId(); + const W = width ?? rect.width; + const H = height ?? rect.height; + const cx = W / 2; + const cy = H / 2; + const effectiveRadius = Math.max(80, Math.min(radius, Math.min(W, H) * 0.42 - 10)); + const nodes = React.useMemo(() => { const N = Math.max(1, items.length); return items.map((it, i) => { const angle = (i / N) * Math.PI * 2 - Math.PI / 2; - const x = cx + radius * Math.cos(angle); - const y = cy + radius * Math.sin(angle); - return { ...it, x, y }; + const x = cx + effectiveRadius * Math.cos(angle); + const y = cy + effectiveRadius * Math.sin(angle); + return { ...it, x, y, angle, safeUrl: isSafeHttpUrl(it.url) ? it.url : undefined }; }); - }, [items, cx, cy, radius]); + }, [items, cx, cy, effectiveRadius]); - const colorFor = (status: string) => { - if (status === 'found') return '#10B981'; // green - if (status === 'inconclusive') return '#F59E0B'; // amber - if (status === 'not_found') return '#9CA3AF'; // gray - return '#EF4444'; // red for errors/unknown - }; + const gradCoreId = `${idPrefix}-mm-core`; + const shadowId = `${idPrefix}-mm-shadow`; + const ariaLabel = `Mapa de evidências para ${username || "alvo"} com ${items.length} plataformas`; + + function exportJson(username: string, events: SiteEvent[]) { + const payload = { username, events }; + const blob = new Blob([JSON.stringify(payload, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `aliasmap-${username || "resultado"}.json`; + a.click(); + URL.revokeObjectURL(url); + } + + function exportCsv(username: string, events: SiteEvent[]) { + const header = `user=${username}\n`; + const rows = [["platform", "status", "url"]]; + for (const e of events) { + if (e.type === "site_result") { + rows.push([e.id, e.status, e.url ?? ""]); + } + } + const csv = header + rows.map((r) => r.map((v) => `"${v}"`).join(",")).join("\n"); + const blob = new Blob([csv], { type: "text/csv" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `aliasmap-${username || "resultado"}.csv`; + a.click(); + URL.revokeObjectURL(url); + } + + if (!items || items.length === 0) { + return ( + + + + + + + Mapa de Evidências + + + } + subheader={ + + Sem dados para exibir + + } + /> + + ); + } return ( -
- - - - - - - - {/* Root node */} - - - {username || 'alvo'} - - - {/* Edges */} - {nodes.map((n) => ( - - ))} - - {/* Platform nodes */} - {nodes.map((n) => { - const content = ( - - - - {n.platform} + + + + + + + + Mapa de Evidências + + + {username || "alvo"} — {items.length} plataformas + + + + } + action={ + + Cada nó representa uma plataforma. Cores indicam o status. Tracejado = heurístico. Clique/Enter abre a + URL quando disponível. + + } + > + + + Ajuda + + + } + sx={{ pb: 1.5 }} + /> + + + + + + + + + + + + + {ariaLabel} + Visual de nós por plataforma conectados ao identificador central. + + + + + + + + + + + + + + + + + + {truncate(username || "alvo", 18)} - {n.heuristic && ( - - heurístico - - )} - ); - return n.url ? ( - - {content} - - ) : content; - })} - -
+ + {nodes.map((n, idx) => ( + + ))} + + {/* nós */} + {nodes.map((n, idx) => { + const stroke = colorFor(n.rawStatus ?? n.status); + const isHeuristic = Boolean(n.heuristic); + const label = truncate(n.platform, maxLabel); + + const group = ( + { + if (!n.safeUrl) return; + if (e.key === "Enter" || e.key === " ") { + window.open(n.safeUrl, "_blank", "noopener,noreferrer"); + e.preventDefault(); + } + }} + > + + {`${n.platform} — ${n.rawStatus ?? n.status}${isHeuristic ? " (heurístico)" : ""}${ + n.safeUrl ? ` — ${n.safeUrl}` : "" + }`} + + + + {label} + + {isHeuristic && ( + + heurístico + + )} + + ); + + if (!n.safeUrl) return group; + + return ( + + + + + {n.platform} + + + {isHeuristic && ( + + )} + + + {n.safeUrl} + + + + Abrir + + + } + > + + {group} + + + ); + })} + +
+ {exportData && ( + + + + + )} + + + ); +} + +function Edge({ idx, x1, y1, x2, y2, animate, prefix }: EdgeProps) { + const gradId = `${prefix}-edge-grad-${idx}`; + return ( + + + + + + + {animate ? ( + + ) : null} + + + ); +} + +function Legend({ label, color }: LegendProps) { + return ( + + + {label} + + } + sx={{ "& .MuiChip-label": { display: "flex", alignItems: "center", py: 0.5, px: 1 } }} + /> ); } diff --git a/apps/web/components/ResultsScreen.tsx b/apps/web/components/ResultsScreen.tsx new file mode 100644 index 0000000..2ef6dfe --- /dev/null +++ b/apps/web/components/ResultsScreen.tsx @@ -0,0 +1,22 @@ +"use client"; + +import * as React from "react"; +import { Stack, Typography, Alert } from "@mui/material"; +import { MindmapPreview } from "@/components/MindmapPreview"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import type { ResultsScreenProps } from "../types"; + +export default function ResultsScreen({ hasData, trimmed, foundOnly, events }: ResultsScreenProps) { + return ( + + Resultado + {!hasData ? ( + }> + Nenhuma evidência confirmada. + + ) : ( + + )} + + ); +} diff --git a/apps/web/components/SearchScreen.tsx b/apps/web/components/SearchScreen.tsx new file mode 100644 index 0000000..2583a19 --- /dev/null +++ b/apps/web/components/SearchScreen.tsx @@ -0,0 +1,46 @@ +"use client"; + +import * as React from "react"; +import { Stack, TextField, Button } from "@mui/material"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import type { SearchScreenProps } from "../types"; + +export default function SearchScreen({ onStart, loading = false }: SearchScreenProps) { + const [username, setUsername] = React.useState(""); + const trimmed = username.trim(); + const isValid = trimmed.length >= 3 && /^[a-zA-Z0-9._-]{3,32}$/.test(trimmed); + + return ( + + setUsername(e.target.value)} + disabled={loading} + onKeyDown={(e) => { + if (e.key === "Enter" && isValid) onStart(trimmed); + }} + inputProps={{ "aria-label": "username" }} + /> + + + + + + ); +} diff --git a/apps/web/components/theme-provider.tsx b/apps/web/components/theme-provider.tsx new file mode 100644 index 0000000..94f7ca0 --- /dev/null +++ b/apps/web/components/theme-provider.tsx @@ -0,0 +1,67 @@ +"use client"; + +import * as React from "react"; +import { createTheme, ThemeProvider, CssBaseline } from "@mui/material"; +import type { NodeProps } from "../types"; + +const LIGHT = { + primary: { main: "#0B1B34", light: "#4A90E2", dark: "#003B5C" }, + secondary: { main: "#16C5C0" }, + background: { default: "#f0f0f0", paper: "#FFFFFF" }, + text: { primary: "#0B1B34", secondary: "#374151" }, + error: { main: "#EF4444" }, + found: "#10B981", + inconclusive: "#F59E0B", + not_found: "#6B7280", + unknown: "#EF4444", +} as const; + +const DARK = { + primary: { main: "#0D2D3A", light: "#4A90E2", dark: "#003B5C" }, + secondary: { main: "#F9FAFB" }, + background: { default: "#0B1B34", paper: "#111827" }, + text: { primary: "#FFFFFF", secondary: "#9CA3AF" }, + error: { main: "#EF4444" }, + found: "#10B981", + inconclusive: "#F59E0B", + not_found: "#9CA3AF", + unknown: "#EF4444", +} as const; + +export function AppThemeProvider({ children }: NodeProps) { + const [mode, setMode] = React.useState<"light" | "dark">("dark"); + React.useEffect(() => { + if (typeof window === "undefined" || !window.matchMedia) return; + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const apply = (e: MediaQueryListEvent | MediaQueryList) => { + const isDark = "matches" in e ? e.matches : mq.matches; + setMode(isDark ? "dark" : "light"); + }; + apply(mq); + mq.addEventListener("change", apply); + return () => { + mq.removeEventListener("change", apply); + }; + }, []); + + const theme = React.useMemo( + () => + createTheme({ + palette: { + mode, + ...(mode === "light" ? (LIGHT as any) : (DARK as any)), + }, + typography: { + fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial", + }, + }), + [mode] + ); + + return ( + + + {children} + + ); +} diff --git a/apps/web/lib/constants.ts b/apps/web/lib/constants.ts index b3d86dd..5963348 100644 --- a/apps/web/lib/constants.ts +++ b/apps/web/lib/constants.ts @@ -1,6 +1,4 @@ -export const disclaimerPt = `# Aviso e Uso Ético - -Este projeto (AliasMap) é não-oficial e não afiliado ao Sherlock Project. Destina-se a fins educacionais e investigações legítimas/autorizadas. Ao continuar, você declara que entende e concorda com os termos abaixo: +export const disclaimerPt = `Este projeto (AliasMap) é não-oficial e não afiliado ao Sherlock Project. Destina-se a fins educacionais e investigações legítimas/autorizadas. Ao continuar, você declara que entende e concorda com os termos abaixo: - Coletamos somente informações publicamente acessíveis. Não realizamos login, não contornamos controles e não interagimos com fluxos de recuperação além da página inicial (apenas 1 tentativa e parsing estático). - Não persistimos dados no servidor. Resultados são exibidos apenas na sua sessão; export é local e sob sua responsabilidade. diff --git a/apps/web/lib/utils.ts b/apps/web/lib/utils.ts new file mode 100644 index 0000000..0f7074a --- /dev/null +++ b/apps/web/lib/utils.ts @@ -0,0 +1,4 @@ +// Se usar shadcn: ajuste para o seu caminho util de "cn" +export function cn(...classes: (string | undefined | false)[]) { + return classes.filter(Boolean).join(" "); +} \ No newline at end of file diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 709e66b..4f11a03 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -3,4 +3,3 @@ // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. - diff --git a/apps/web/package.json b/apps/web/package.json index cb82fda..703441c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,14 @@ "e2e": "echo 'e2e offline placeholder' && exit 0" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/icons-material": "^5.18.0", + "@mui/material": "^5.18.0", + "@mui/system": "^5.18.0", + "@mui/utils": "^5.17.1", + "@tailwindcss/postcss": "^4.1.13", + "lucide-react": "^0.542.0", "next": "14.2.5", "react": "18.3.1", "react-dom": "18.3.1" @@ -22,10 +30,13 @@ "@types/node": "20.12.12", "@types/react": "18.2.66", "@types/react-dom": "18.2.22", + "@vitest/coverage-v8": "1.6.0", + "autoprefixer": "^10.4.19", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "eslint": "8.57.0", "eslint-config-next": "14.2.5", "typescript": "5.5.3", - "vitest": "1.6.0", - "@vitest/coverage-v8": "1.6.0" + "vitest": "1.6.0" } } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 82edaa8..12beaf7 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2020", - "lib": ["dom", "dom.iterable", "es2020"], + "lib": [ + "dom", + "dom.iterable", + "es2020" + ], "allowJs": false, "skipLibCheck": true, "strict": true, @@ -13,9 +17,25 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, - "types": ["node"] + "types": [ + "node" + ], + "paths": { + "@/*": ["./*"] + }, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } - diff --git a/apps/web/types/index.ts b/apps/web/types/index.ts new file mode 100644 index 0000000..c90c2cf --- /dev/null +++ b/apps/web/types/index.ts @@ -0,0 +1,79 @@ +import type { ReactNode } from "react"; + +export type NodeProps = Readonly<{ children: ReactNode }>; + +export type RootLayoutProps = Readonly<{ + children: ReactNode; +}>; + +export type CustomLoadingProps = Readonly<{ + width?: number; + barHeight?: number; + ariaLabel?: string; +}>; + +export type LoadingProps = Readonly<{ + progress: { done: number; total: number }; + error?: string; +}>; + +export type LegendProps = Readonly<{ label: string; color: string }>; + +export type EdgeProps = Readonly<{ + idx: number; + x1: number; + y1: number; + x2: number; + y2: number; + animate?: boolean; + prefix: string; +}>; + +export type Status = "found" | "not_found" | "inconclusive"; + +export type MindmapItem = Readonly<{ + platform: string; + status: Status; + rawStatus?: Status; + heuristic?: boolean; + url?: string; +}>; + +export type MindmapPreviewProps = Readonly<{ + username: string; + items: MindmapItem[]; + width?: number; + height?: number; + radius?: number; + className?: string; + maxLabel?: number; + events: SiteEvent[]; + exportData?: boolean; +}>; + +export type SiteEvent = + | { type: "site_start"; id: string } + | { + type: "site_result"; + id: string; + status: string; + url?: string; + latencyMs?: number; + reason?: string; + heuristic?: boolean; + } + | { type: "site_error"; id: string; reason: string } + | { type: "progress"; done: number; total: number } + | { type: "done"; summary: { done: number; total: number } }; + +export type ResultsScreenProps = Readonly<{ + events: SiteEvent[]; + hasData: boolean; + trimmed: string; + foundOnly: MindmapItem[]; +}>; + +export type SearchScreenProps = Readonly<{ + onStart: (username: string) => void; + loading?: boolean; +}>; From 292d406d93e22c3a917b46b33f68f75c750046d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= <69174195+fonteeboa@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:51:01 -0300 Subject: [PATCH 2/2] github actions: ajustando verificacao de secret a pedido do maintainer --- .github/workflows/ci.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62b7dd9..a6d2b8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,17 +2,17 @@ name: CI on: push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ "**" ] + branches: ["**"] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: - NODE_VERSION: '20' - PNPM_VERSION: '8' + NODE_VERSION: "20" + PNPM_VERSION: "8" jobs: lint: @@ -231,12 +231,18 @@ jobs: secrets_scan: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Gitleaks uses: gitleaks/gitleaks-action@v2 - with: - args: detect --redact --no-banner + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_VERSION: 8.24.3 build: runs-on: ubuntu-latest