feat: implement Footer component and integrate it into the layout and updated header component

This commit is contained in:
2025-09-03 23:45:36 -03:00
parent 5af2d72526
commit ff9ca32d4f
4 changed files with 171 additions and 54 deletions

View File

@@ -0,0 +1,53 @@
import { Link } from "@tanstack/react-router";
export default function Footer() {
return (
<footer className="relative mt-8 mb-8">
<div className="container mx-auto max-w-7xl px-4">
<div className="relative overflow-hidden rounded-3xl border border-white/10 bg-white/5 p-5 backdrop-blur sm:p-6">
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
<div
className={[
"flex",
"items-center",
"gap-2",
"text-xs",
"text-muted-foreground",
].join(" ")}
>
<span
aria-hidden
className="inline-block h-4 w-4 rounded bg-gradient-to-br from-purple-500 to-fuchsia-500"
/>
<span className="font-medium">Reflecto</span>
<span className="hidden sm:inline"></span>
<span className="hidden text-muted-foreground sm:inline">
Private by default
</span>
</div>
<nav
className={[
"flex",
"items-center",
"gap-4",
"text-xs",
"text-muted-foreground",
].join(" ")}
>
<Link to="/">Home</Link>
<Link to="/dashboard">Dashboard</Link>
</nav>
<div className={["text-xs", "text-muted-foreground"].join(" ")}>
<span className="lining-nums tabular-nums">
© {new Date().getFullYear()}
</span>{" "}
Reflecto
</div>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -9,23 +9,105 @@ export default function Header() {
] as const; ] as const;
return ( return (
<div> <header className="sticky top-0 z-50">
<div className="flex flex-row items-center justify-between px-2 py-1"> {/* Subtle gradient ribbon behind the header for depth */}
<nav className="flex gap-4 text-lg"> <div className="absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-black/30 to-transparent" />
{links.map(({ to, label }) => {
return ( <div className="relative mx-auto w-full max-w-7xl px-3">
<Link key={to} to={to}> <div className="mt-3 rounded-2xl border border-white/10 bg-white/5 backdrop-blur supports-[backdrop-filter]:bg-white/5">
{label} <div className="relative flex items-center justify-between px-4 py-2">
{/* Logo + primary nav */}
<div className="flex items-center gap-4">
<Link className="inline-flex items-center gap-2" to="/">
<span
aria-hidden
className="inline-block h-6 w-6 rounded bg-gradient-to-br from-purple-500 to-fuchsia-500"
/>
<span className="font-semibold tracking-tight">Reflecto</span>
</Link> </Link>
);
})} <nav className="hidden items-center gap-2 sm:flex">
</nav> {links.map(({ to, label }) => (
<div className="flex items-center gap-2"> <Link
<ModeToggle /> activeProps={{ className: "text-white" }}
<UserMenu /> className={[
"rounded-lg",
"px-3",
"py-1.5",
"text-sm",
"text-muted-foreground",
"transition-colors",
"hover:text-white",
].join(" ")}
key={to}
to={to}
>
{label}
</Link>
))}
</nav>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
<ModeToggle />
<UserMenu />
</div>
{/* Mobile nav (compact) */}
<nav className="flex items-center gap-2 sm:hidden">
{links.map(({ to, label }) => (
<Link
className={[
"rounded-lg",
"px-2",
"py-1",
"text-sm",
"text-muted-foreground",
"hover:text-white",
].join(" ")}
key={to}
to={to}
>
{label}
</Link>
))}
</nav>
</div>
</div> </div>
</div> </div>
<hr />
</div> {/* Decorative blobs for subtle flair matching landing visuals */}
<div
className={[
"absolute",
"-z-10",
"left-0",
"top-0",
"h-40",
"w-40",
"rounded-full",
"bg-gradient-to-br",
"from-purple-500/30",
"to-fuchsia-500/20",
"blur-3xl",
].join(" ")}
/>
<div
className={[
"absolute",
"-z-10",
"right-10",
"top-0",
"h-56",
"w-56",
"rounded-full",
"bg-gradient-to-br",
"from-cyan-400/25",
"to-emerald-400/15",
"blur-3xl",
].join(" ")}
/>
</header>
); );
} }

View File

@@ -7,6 +7,7 @@ import {
useRouterState, useRouterState,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import Footer from "@/components/footer";
import Header from "@/components/header"; import Header from "@/components/header";
import Loader from "@/components/loader"; import Loader from "@/components/loader";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
@@ -14,10 +15,10 @@ import { Toaster } from "@/components/ui/sonner";
import type { trpc } from "@/utils/trpc"; import type { trpc } from "@/utils/trpc";
import "../index.css"; import "../index.css";
export interface RouterAppContext { export type RouterAppContext = {
trpc: typeof trpc; trpc: typeof trpc;
queryClient: QueryClient; queryClient: QueryClient;
} };
export const Route = createRootRouteWithContext<RouterAppContext>()({ export const Route = createRootRouteWithContext<RouterAppContext>()({
component: RootComponent, component: RootComponent,
@@ -54,9 +55,10 @@ function RootComponent() {
disableTransitionOnChange disableTransitionOnChange
storageKey="vite-ui-theme" storageKey="vite-ui-theme"
> >
<div className="grid h-svh grid-rows-[auto_1fr]"> <div className="grid min-h-svh grid-rows-[auto_1fr_auto]">
<Header /> <Header />
{isFetching ? <Loader /> : <Outlet />} {isFetching ? <Loader /> : <Outlet />}
<Footer />
</div> </div>
<Toaster richColors /> <Toaster richColors />
</ThemeProvider> </ThemeProvider>

View File

@@ -21,16 +21,19 @@ export const Route = createFileRoute("/")({
function HealthBadge() { function HealthBadge() {
const healthCheck = useQuery(trpc.healthCheck.queryOptions()); const healthCheck = useQuery(trpc.healthCheck.queryOptions());
const status = healthCheck.isLoading let status = "Offline";
? "Checking" if (healthCheck.isLoading) {
: healthCheck.data status = "Checking";
? "Online" } else if (healthCheck.data) {
: "Offline"; status = "Online";
const color = healthCheck.isLoading }
? "bg-yellow-500" // flatten ternaries for linter by using simple logic
: healthCheck.data let color = "bg-red-500";
? "bg-emerald-500" if (healthCheck.isLoading) {
: "bg-red-500"; color = "bg-yellow-500";
} else if (healthCheck.data) {
color = "bg-emerald-500";
}
return ( return (
<div className="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-muted-foreground text-xs backdrop-blur"> <div className="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-muted-foreground text-xs backdrop-blur">
<span className={`h-2 w-2 rounded-full ${color}`} /> <span className={`h-2 w-2 rounded-full ${color}`} />
@@ -309,12 +312,9 @@ function HomeComponent() {
</Button> </Button>
</div> </div>
<div className="mt-8 flex items-center gap-3 text-muted-foreground text-xs"> <div className="mt-8 flex items-center gap-3 text-muted-foreground text-xs">
<img <span
alt="Reflecto" aria-hidden
className="h-5 w-5 rounded-sm" className="inline-block h-5 w-5 rounded-sm bg-gradient-to-br from-purple-500 to-fuchsia-500"
height="20"
src="/logo.png"
width="20"
/> />
<span>Private by default Powered by lightweight AI</span> <span>Private by default Powered by lightweight AI</span>
</div> </div>
@@ -395,26 +395,6 @@ function HomeComponent() {
</div> </div>
</div> </div>
</section> </section>
{/* Footer */}
<footer className="container mx-auto max-w-7xl px-4 pt-4 pb-10 text-muted-foreground text-xs">
<div className="flex flex-col items-center justify-between gap-2 sm:flex-row">
<div className="flex items-center gap-2">
<img
alt="Reflecto"
className="h-4 w-4 rounded-sm"
height="16"
src="/logo.png"
width="16"
/>
<span>Reflecto</span>
</div>
<div className="flex items-center gap-4">
<Link to="/">Home</Link>
<Link to="/dashboard">Dashboard</Link>
</div>
</div>
</footer>
</div> </div>
</> </>
); );