tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ | [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/app/routes/__root.tsx b/app/routes/__root.tsx
index a29571cc..29d62a26 100644
--- a/app/routes/__root.tsx
+++ b/app/routes/__root.tsx
@@ -132,24 +132,23 @@ function NotFoundComponent() {
);
}
+function shouldHidePublicChrome(pathname: string): boolean {
+ // Routes that bring their own chrome and shouldn't render the public Navbar/Footer.
+ return (
+ pathname === "/dados/ciclodados" ||
+ pathname === "/admin" ||
+ pathname.startsWith("/admin/")
+ );
+}
+
function ConditionalNavbar() {
const location = useRouterState({ select: (s) => s.location });
- const isCicloDadosPage = location.pathname === "/dados/ciclodados";
-
- if (isCicloDadosPage) {
- return null;
- }
-
+ if (shouldHidePublicChrome(location.pathname)) return null;
return ;
}
function ConditionalFooter() {
const location = useRouterState({ select: (s) => s.location });
- const isCicloDadosPage = location.pathname === "/dados/ciclodados";
-
- if (isCicloDadosPage) {
- return null;
- }
-
+ if (shouldHidePublicChrome(location.pathname)) return null;
return ;
}
diff --git a/app/routes/admin.contagens.index.tsx b/app/routes/admin.contagens.index.tsx
new file mode 100644
index 00000000..f16f4812
--- /dev/null
+++ b/app/routes/admin.contagens.index.tsx
@@ -0,0 +1,49 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { useSuspenseQuery } from "@tanstack/react-query";
+import { AdminTopbar } from "~/components/Admin/AdminTopbar";
+import { ContagensTable, type ContagemRow } from "~/components/Admin/ContagensTable";
+import { Button } from "~/components/ui/button";
+import { Plus } from "lucide-react";
+import { contagensQueryOptions } from "~/queries/dados.contagens";
+import { RouteLoading, RouteErrorBoundary } from "~/components/Commom/RouteBoundaries";
+import { seo } from "~/utils/seo";
+
+export const Route = createFileRoute("/admin/contagens/")({
+ loader: ({ context: { queryClient } }) =>
+ queryClient.ensureQueryData(contagensQueryOptions()),
+ head: () =>
+ seo({
+ title: "Admin · Contagens - Ameciclo",
+ description: "Gestão das contagens de ciclistas registradas pela Ameciclo.",
+ pathname: "/admin/contagens",
+ noindex: true,
+ }),
+ component: AdminContagens,
+ pendingComponent: () => ,
+ pendingMs: 500,
+ pendingMinMs: 800,
+ errorComponent: RouteErrorBoundary,
+});
+
+function AdminContagens() {
+ const { data: { summaryData } } = useSuspenseQuery(contagensQueryOptions());
+ const rows = summaryData.countsData as ContagemRow[];
+
+ return (
+ <>
+
+
+ Nova contagem
+
+ }
+ />
+
+
+
+ >
+ );
+}
diff --git a/app/routes/admin.index.tsx b/app/routes/admin.index.tsx
new file mode 100644
index 00000000..d40397f8
--- /dev/null
+++ b/app/routes/admin.index.tsx
@@ -0,0 +1,73 @@
+import { createFileRoute, Link } from "@tanstack/react-router";
+import { useSuspenseQuery } from "@tanstack/react-query";
+import { Users2, ArrowRight } from "lucide-react";
+import { AdminTopbar } from "~/components/Admin/AdminTopbar";
+import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
+import { Button } from "~/components/ui/button";
+import { contagensQueryOptions } from "~/queries/dados.contagens";
+import { RouteLoading, RouteErrorBoundary } from "~/components/Commom/RouteBoundaries";
+
+export const Route = createFileRoute("/admin/")({
+ loader: ({ context: { queryClient } }) =>
+ queryClient.ensureQueryData(contagensQueryOptions()),
+ component: AdminDashboard,
+ pendingComponent: () => ,
+ pendingMs: 500,
+ pendingMinMs: 800,
+ errorComponent: RouteErrorBoundary,
+});
+
+function AdminDashboard() {
+ const { data: { summaryData } } = useSuspenseQuery(contagensQueryOptions());
+ const { summaryData: stats, countsData } = summaryData;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ Contagens de ciclistas
+
+
+
+
+
+ {countsData.length.toLocaleString("pt-BR")} registros disponíveis para
+ consulta e edição.
+
+
+
+
+ >
+ );
+}
+
+function StatCard({ label, value }: { label: string; value: number | string }) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ );
+}
diff --git a/app/routes/admin.route.tsx b/app/routes/admin.route.tsx
new file mode 100644
index 00000000..66863e7c
--- /dev/null
+++ b/app/routes/admin.route.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute, Outlet } from "@tanstack/react-router";
+import { AdminSidebar } from "~/components/Admin/AdminSidebar";
+import { seo } from "~/utils/seo";
+
+export const Route = createFileRoute("/admin")({
+ head: () =>
+ seo({
+ title: "Admin - Ameciclo",
+ description: "Área administrativa para gestão de dados da Ameciclo.",
+ pathname: "/admin",
+ // Admin pages must not be indexed; robots.txt also disallows /admin.
+ noindex: true,
+ }),
+ component: AdminLayout,
+});
+
+function AdminLayout() {
+ return (
+
+ );
+}
diff --git a/app/routes/robots[.]txt.ts b/app/routes/robots[.]txt.ts
index 4fd63f97..c3835321 100644
--- a/app/routes/robots[.]txt.ts
+++ b/app/routes/robots[.]txt.ts
@@ -10,6 +10,7 @@ export const Route = createFileRoute("/robots.txt")({
const body = [
"User-agent: *",
"Allow: /",
+ "Disallow: /admin",
"Disallow: /documentacao",
"",
`Sitemap: ${siteUrl}/sitemap.xml`,
|