From b1d90827017107ceb5056fc3abf87372f284a592 Mon Sep 17 00:00:00 2001 From: Erik Silva Date: Wed, 21 Jan 2026 01:23:48 -0300 Subject: [PATCH] feat: add dynamic favicon, dynamic titles and public portal page --- src/app/dashboard/DashboardClient.tsx | 4 + src/app/documento/[id]/DocumentViewClient.tsx | 4 + src/app/portal/PortalClient.tsx | 219 ++++++++++++++++++ src/app/portal/page.tsx | 53 +++++ .../pasta/[id]/FolderViewClient.tsx | 4 + src/components/DynamicFavicon.tsx | 56 +++++ src/components/DynamicTitle.tsx | 17 ++ src/components/LoginClient.tsx | 4 + src/components/Sidebar.tsx | 25 ++ 9 files changed, 386 insertions(+) create mode 100644 src/app/portal/PortalClient.tsx create mode 100644 src/app/portal/page.tsx create mode 100644 src/components/DynamicFavicon.tsx create mode 100644 src/components/DynamicTitle.tsx diff --git a/src/app/dashboard/DashboardClient.tsx b/src/app/dashboard/DashboardClient.tsx index 2072814..2c64df9 100644 --- a/src/app/dashboard/DashboardClient.tsx +++ b/src/app/dashboard/DashboardClient.tsx @@ -15,6 +15,8 @@ import { import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Sidebar } from "@/components/Sidebar"; +import { DynamicFavicon } from "@/components/DynamicFavicon"; +import { DynamicTitle } from "@/components/DynamicTitle"; type Organization = { id: string; @@ -57,6 +59,8 @@ export default function DashboardClient({ return (
+ + {/* Main Content Area */} diff --git a/src/app/documento/[id]/DocumentViewClient.tsx b/src/app/documento/[id]/DocumentViewClient.tsx index 718cd17..d9da866 100644 --- a/src/app/documento/[id]/DocumentViewClient.tsx +++ b/src/app/documento/[id]/DocumentViewClient.tsx @@ -21,6 +21,8 @@ import "react-pdf/dist/Page/AnnotationLayer.css"; import "react-pdf/dist/Page/TextLayer.css"; import { incrementViewCount, incrementDownloadCount } from "@/app/actions/documents"; +import { DynamicFavicon } from "@/components/DynamicFavicon"; +import { DynamicTitle } from "@/components/DynamicTitle"; // Configure PDF.js worker pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; @@ -165,6 +167,8 @@ export default function DocumentViewClient({ className="min-h-screen bg-slate-50" onContextMenu={!doc.isDownloadable ? (e) => e.preventDefault() : undefined} > + + {/* Header */}
+ folder.name.toLowerCase().includes(searchTerm.toLowerCase()) || + folder.description?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const formatDate = (date: Date) => { + return new Date(date).toLocaleDateString("pt-BR", { + day: "2-digit", + month: "short", + year: "numeric", + }); + }; + + return ( +
+ + + + {/* Header */} +
+
+
+ {organization.logoUrl ? ( + {organization.name} + ) : ( +
+ {organization.name[0]} +
+ )} +
+

{organization.name}

+

Portal da Transparência

+
+
+ + Ambiente Seguro + +
+
+ +
+
+ {/* Portal Hero */} +
+
+
+ + +
+
+

+ Portal da Transparência +

+

+ Central de documentos públicos e projetos de {organization.name} +

+
+
+ +
+
+ + {/* Search & Stats */} +
+
+
+ +
+ setSearchTerm(e.target.value)} + className="h-10 sm:h-12 pl-10 sm:pl-12 pr-4 bg-white border-slate-200 rounded-xl text-sm font-medium focus:ring-0 focus:border-slate-300 transition-all shadow-none" + /> +
+ +
+ Projetos + {filteredFolders.length} +
+ +
+
+ + {/* Projects Grid */} +
+ {filteredFolders.map((folder, idx) => ( + + +
+ {folder.imageUrl ? ( +
+ {folder.name} +
+ ) : ( +
+ +
+ )} +
+

+ {folder.name} +

+

+ {folder.description || "Projeto público com documentos oficiais."} +

+
+
+ +
+
+ + + {folder.documentsCount + folder.foldersCount} itens + + + + {formatDate(folder.createdAt)} + +
+ +
+ +
+ ))} + + {filteredFolders.length === 0 && ( +
+
+ +
+

Nenhum projeto encontrado

+

+ Não existem projetos públicos disponíveis no momento ou nenhum corresponde à sua busca. +

+
+ )} +
+
+
+ + {/* Footer */} +
+
+ {organization.logoUrl ? ( + {organization.name} + ) : ( +
+
+ {organization.name[0]} +
+ {organization.name} +
+ )} +
+
+
+ ); +} diff --git a/src/app/portal/page.tsx b/src/app/portal/page.tsx new file mode 100644 index 0000000..fabc181 --- /dev/null +++ b/src/app/portal/page.tsx @@ -0,0 +1,53 @@ +import { prisma } from "@/lib/prisma"; +import { redirect } from "next/navigation"; +import PortalClient from "./PortalClient"; + +export default async function PortalPage() { + const organization = await prisma.organization.findFirst(); + + if (!organization) { + redirect("/setup"); + } + + // Get all public root folders (projects) + const publicFolders = await prisma.folder.findMany({ + where: { + organizationId: organization.id, + parentId: null, + isPublished: true, + }, + orderBy: { createdAt: "desc" }, + include: { + _count: { + select: { + documents: { + where: { isPublished: true } + }, + children: { + where: { isPublished: true } + } + } + } + } + }); + + return ( + ({ + id: f.id, + name: f.name, + description: f.description, + color: f.color, + imageUrl: f.imageUrl, + documentsCount: f._count.documents, + foldersCount: f._count.children, + createdAt: f.createdAt, + }))} + /> + ); +} diff --git a/src/app/visualizar/pasta/[id]/FolderViewClient.tsx b/src/app/visualizar/pasta/[id]/FolderViewClient.tsx index 4770c13..4456088 100644 --- a/src/app/visualizar/pasta/[id]/FolderViewClient.tsx +++ b/src/app/visualizar/pasta/[id]/FolderViewClient.tsx @@ -16,6 +16,8 @@ import Link from "next/link"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +import { DynamicFavicon } from "@/components/DynamicFavicon"; +import { DynamicTitle } from "@/components/DynamicTitle"; type Organization = { name: string; @@ -98,6 +100,8 @@ export default function FolderViewClient({ return (
+ + {/* Header */}
diff --git a/src/components/DynamicFavicon.tsx b/src/components/DynamicFavicon.tsx new file mode 100644 index 0000000..62f2b06 --- /dev/null +++ b/src/components/DynamicFavicon.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useEffect } from "react"; + +interface DynamicFaviconProps { + logoUrl: string | null; + orgName: string; + primaryColor: string; +} + +export function DynamicFavicon({ logoUrl, orgName, primaryColor }: DynamicFaviconProps) { + useEffect(() => { + // If there's a logo URL, use it as favicon + if (logoUrl) { + // Remove any existing favicon links + const existingLinks = document.querySelectorAll("link[rel*='icon']"); + existingLinks.forEach(link => link.remove()); + + // Create new favicon link + const link = document.createElement("link"); + link.rel = "icon"; + link.type = "image/png"; + link.href = logoUrl; + document.head.appendChild(link); + + // Also add apple touch icon + const appleLink = document.createElement("link"); + appleLink.rel = "apple-touch-icon"; + appleLink.href = logoUrl; + document.head.appendChild(appleLink); + } else { + // Generate a simple SVG favicon with the first letter + const svg = ` + + + + ${orgName.charAt(0).toUpperCase()} + + + `; + const blob = new Blob([svg], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + + const existingLinks = document.querySelectorAll("link[rel*='icon']"); + existingLinks.forEach(link => link.remove()); + + const link = document.createElement("link"); + link.rel = "icon"; + link.type = "image/svg+xml"; + link.href = url; + document.head.appendChild(link); + } + }, [logoUrl, orgName, primaryColor]); + + return null; +} diff --git a/src/components/DynamicTitle.tsx b/src/components/DynamicTitle.tsx new file mode 100644 index 0000000..a9a49a5 --- /dev/null +++ b/src/components/DynamicTitle.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { useEffect } from "react"; + +interface DynamicTitleProps { + title: string; + orgName?: string; +} + +export function DynamicTitle({ title, orgName }: DynamicTitleProps) { + useEffect(() => { + const fullTitle = orgName ? `${title} | ${orgName}` : title; + document.title = fullTitle; + }, [title, orgName]); + + return null; +} diff --git a/src/components/LoginClient.tsx b/src/components/LoginClient.tsx index 8da8667..a40d2ff 100644 --- a/src/components/LoginClient.tsx +++ b/src/components/LoginClient.tsx @@ -9,6 +9,8 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { DynamicFavicon } from "@/components/DynamicFavicon"; +import { DynamicTitle } from "@/components/DynamicTitle"; type Organization = { id: string; @@ -42,6 +44,8 @@ export default function LoginClient({ organization }: { organization: Organizati return (
+ + {/* Left Side: Illustration & Info - Hidden on Mobile */}