feat: update global styles and layout with new font and theme support; enhance dashboard header with theme toggle

This commit is contained in:
asepharyana
2026-07-23 15:53:56 +07:00
parent 6b725f31b4
commit 4dbaba7529
5 changed files with 193 additions and 158 deletions
+76 -65
View File
@@ -7,9 +7,9 @@
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-sans); --font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
--font-heading: var(--font-sans); --font-heading: var(--font-geist-sans);
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -49,72 +49,72 @@
} }
:root { :root {
--background: oklch(1 0 0); --background: oklch(0.985 0.005 85);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.15 0.025 265);
--card: oklch(1 0 0); --card: oklch(0.99 0.003 85);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.15 0.025 265);
--popover: oklch(1 0 0); --popover: oklch(0.99 0.003 85);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.15 0.025 265);
--primary: oklch(0.205 0 0); --primary: oklch(0.45 0.13 265);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0.005 85);
--secondary: oklch(0.97 0 0); --secondary: oklch(0.93 0.02 265);
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: oklch(0.25 0.03 265);
--muted: oklch(0.97 0 0); --muted: oklch(0.95 0.01 265);
--muted-foreground: oklch(0.556 0 0); --muted-foreground: oklch(0.55 0.02 265);
--accent: oklch(0.97 0 0); --accent: oklch(0.7 0.15 75);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.2 0.02 265);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.6 0.2 25);
--border: oklch(0.922 0 0); --border: oklch(0.88 0.015 265);
--input: oklch(0.922 0 0); --input: oklch(0.88 0.015 265);
--ring: oklch(0.708 0 0); --ring: oklch(0.45 0.13 265);
--chart-1: oklch(0.87 0 0); --chart-1: oklch(0.7 0.15 75);
--chart-2: oklch(0.556 0 0); --chart-2: oklch(0.45 0.13 265);
--chart-3: oklch(0.439 0 0); --chart-3: oklch(0.6 0.12 195);
--chart-4: oklch(0.371 0 0); --chart-4: oklch(0.5 0.18 330);
--chart-5: oklch(0.269 0 0); --chart-5: oklch(0.75 0.1 155);
--radius: 0.625rem; --radius: 0.5rem;
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.98 0.005 85);
--sidebar-foreground: oklch(0.145 0 0); --sidebar-foreground: oklch(0.15 0.025 265);
--sidebar-primary: oklch(0.205 0 0); --sidebar-primary: oklch(0.45 0.13 265);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.985 0.005 85);
--sidebar-accent: oklch(0.97 0 0); --sidebar-accent: oklch(0.95 0.01 265);
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: oklch(0.15 0.025 265);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.88 0.015 265);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.45 0.13 265);
} }
.dark { .dark {
--background: oklch(0.145 0 0); --background: oklch(0.13 0.02 265);
--foreground: oklch(0.985 0 0); --foreground: oklch(0.95 0.008 85);
--card: oklch(0.205 0 0); --card: oklch(0.17 0.025 265);
--card-foreground: oklch(0.985 0 0); --card-foreground: oklch(0.95 0.008 85);
--popover: oklch(0.205 0 0); --popover: oklch(0.17 0.025 265);
--popover-foreground: oklch(0.985 0 0); --popover-foreground: oklch(0.95 0.008 85);
--primary: oklch(0.922 0 0); --primary: oklch(0.7 0.15 75);
--primary-foreground: oklch(0.205 0 0); --primary-foreground: oklch(0.15 0.02 265);
--secondary: oklch(0.269 0 0); --secondary: oklch(0.22 0.03 265);
--secondary-foreground: oklch(0.985 0 0); --secondary-foreground: oklch(0.9 0.01 85);
--muted: oklch(0.269 0 0); --muted: oklch(0.2 0.02 265);
--muted-foreground: oklch(0.708 0 0); --muted-foreground: oklch(0.6 0.02 265);
--accent: oklch(0.269 0 0); --accent: oklch(0.6 0.12 195);
--accent-foreground: oklch(0.985 0 0); --accent-foreground: oklch(0.95 0.008 85);
--destructive: oklch(0.704 0.191 22.216); --destructive: oklch(0.65 0.2 25);
--border: oklch(1 0 0 / 10%); --border: oklch(0.25 0.03 265);
--input: oklch(1 0 0 / 15%); --input: oklch(0.25 0.03 265);
--ring: oklch(0.556 0 0); --ring: oklch(0.7 0.15 75);
--chart-1: oklch(0.87 0 0); --chart-1: oklch(0.7 0.15 75);
--chart-2: oklch(0.556 0 0); --chart-2: oklch(0.65 0.12 265);
--chart-3: oklch(0.439 0 0); --chart-3: oklch(0.6 0.12 195);
--chart-4: oklch(0.371 0 0); --chart-4: oklch(0.65 0.15 330);
--chart-5: oklch(0.269 0 0); --chart-5: oklch(0.7 0.1 155);
--sidebar: oklch(0.205 0 0); --sidebar: oklch(0.15 0.02 265);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.95 0.008 85);
--sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary: oklch(0.7 0.15 75);
--sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-primary-foreground: oklch(0.15 0.02 265);
--sidebar-accent: oklch(0.269 0 0); --sidebar-accent: oklch(0.2 0.02 265);
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.9 0.01 85);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(0.25 0.03 265);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.7 0.15 75);
} }
@layer base { @layer base {
@@ -128,3 +128,14 @@
@apply font-sans; @apply font-sans;
} }
} }
/* Signature: glowing "server LED" focus ring */
.dark *:focus-visible {
@apply ring-[3px];
outline: none;
box-shadow: 0 0 8px oklch(0.7 0.15 75 / 0.3), 0 0 0 3px oklch(0.7 0.15 75 / 0.5);
}
html:not(.dark) *:focus-visible {
@apply ring-[3px];
outline: none;
}
+7 -3
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import { ThemeProvider } from "next-themes";
import { DashboardHeader } from "@/components/dashboard/header"; import { DashboardHeader } from "@/components/dashboard/header";
import "./globals.css"; import "./globals.css";
@@ -16,7 +17,7 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Asep Haryana Saputra", title: "Asep Haryana Saputra",
description: description:
"Backend & infrastructure engineer — portfolio, projects, and live monitoring dashboard by Asep Haryana Saputra.", "Teknik Informatika student exploring backend, infrastructure & DevOps — portfolio and projects by Asep Haryana Saputra.",
}; };
export default function RootLayout({ export default function RootLayout({
@@ -28,10 +29,13 @@ export default function RootLayout({
<html <html
lang="en" lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
suppressHydrationWarning
> >
<body className="flex min-h-full flex-col bg-background text-foreground"> <body className="flex min-h-full flex-col bg-background text-foreground">
<DashboardHeader /> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children} <DashboardHeader />
{children}
</ThemeProvider>
</body> </body>
</html> </html>
); );
+96 -89
View File
@@ -1,6 +1,5 @@
import { import {
ArrowRight, ArrowRight,
BookOpen,
Container, Container,
Cpu, Cpu,
Database, Database,
@@ -8,6 +7,7 @@ import {
GitBranch, GitBranch,
GitFork, GitFork,
HardDrive, HardDrive,
Layers,
} from "lucide-react"; } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -15,13 +15,14 @@ import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// ponytail: static data, fine for a personal portfolio. Move to CMS/content layer if > 50 items.
const skills = [ const skills = [
{ icon: HardDrive, label: "Backend (Bun, Elysia, Axum)" }, { icon: HardDrive, label: "Backend (Bun, Elysia, Axum)" },
{ icon: Container, label: "Docker & Traefik" }, { icon: Container, label: "Docker & Traefik" },
{ icon: Database, label: "PostgreSQL, Redis, NATS" }, { icon: Database, label: "PostgreSQL, Redis, NATS" },
{ icon: Cpu, label: "Rust, TypeScript, Python" }, { icon: Cpu, label: "Rust, TypeScript, Python" },
{ icon: GitBranch, label: "CI/CD, GitHub Actions" }, { icon: GitBranch, label: "CI/CD, GitHub Actions" },
{ icon: BookOpen, label: "Dapr, Prometheus, Jaeger" }, { icon: Layers, label: "Dapr, Prometheus, Jaeger" },
]; ];
const techTags = [ const techTags = [
@@ -50,19 +51,25 @@ const techTags = [
export default function Home() { export default function Home() {
return ( return (
<div className="flex flex-1 flex-col"> <div className="flex flex-1 flex-col">
{/* Hero */} {/* Hero — terminal-inspired */}
<section className="flex flex-col items-center justify-center px-6 py-24 text-center sm:py-32"> <section className="relative flex flex-col items-center justify-center overflow-hidden px-6 py-28 text-center sm:py-36">
<div className="mb-6 flex size-14 items-center justify-center rounded-2xl bg-foreground text-xl font-bold text-background"> <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,oklch(0.45_0.13_265_/_0.06)_0%,transparent_70%)] dark:bg-[radial-gradient(ellipse_at_center,oklch(0.7_0.15_75_/_0.06)_0%,transparent_70%)]" />
AH <div className="relative">
<p className="mb-2 font-mono text-xs text-muted-foreground">
<span className="text-primary dark:text-primary">~</span> whoami
</p>
<h1 className="font-mono text-3xl font-bold tracking-tight sm:text-5xl">
<span className="text-muted-foreground">$ </span>
Asep Haryana Saputra
<span className="inline-block size-2.5 animate-pulse bg-foreground ml-1 rounded-none" />
</h1>
<p className="mx-auto mt-3 max-w-lg font-mono text-xs text-muted-foreground sm:text-sm">
<span className="text-muted-foreground/50"># </span>
Teknik Informatika student exploring backend systems, distributed
infrastructure, and DevOps still learning, still building.
</p>
</div> </div>
<h1 className="max-w-2xl text-4xl font-semibold tracking-tight sm:text-5xl"> <div className="relative mt-10 flex flex-wrap items-center justify-center gap-3">
Asep Haryana Saputra
</h1>
<p className="mt-3 max-w-lg text-lg text-muted-foreground">
Backend &amp; infrastructure engineer who builds distributed systems,
RESTful APIs, and production-grade observability stacks.
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<a href="#projects" className={cn(buttonVariants({ size: "lg" }))}> <a href="#projects" className={cn(buttonVariants({ size: "lg" }))}>
View Projects View Projects
<ArrowRight className="size-4" /> <ArrowRight className="size-4" />
@@ -79,75 +86,75 @@ export default function Home() {
</div> </div>
</section> </section>
{/* About */} {/* Content — staggered sections with alternating visual weight */}
<section className="mx-auto flex w-full max-w-3xl flex-col gap-8 px-6 pb-24"> <section className="mx-auto flex w-full max-w-3xl flex-col gap-12 px-6 pb-28">
<Card> {/* About — minimal, just text */}
<CardHeader> <div>
<CardTitle>About</CardTitle> <p className="mb-2 font-mono text-xs text-muted-foreground">
</CardHeader> <span className="text-primary dark:text-primary">~</span> about
<CardContent className="text-muted-foreground"> </p>
<div className="space-y-3 leading-relaxed text-muted-foreground">
<p> <p>
I build backend systems and infrastructure for personal projects, I&rsquo;m a Teknik Informatika student passionate about backend
research collaborations, and production deployments. My work spans engineering, distributed systems, and infrastructure. I spend my
RESTful API design, container orchestration with Docker &amp; time building RESTful APIs, learning container orchestration with
Traefik, distributed tracing with OpenTelemetry &amp; Jaeger, and Docker &amp; Traefik, and exploring observability with OpenTelemetry
event-driven architectures on Dapr &amp; NATS. &amp; Jaeger.
</p> </p>
<p className="mt-3"> <p>
I was the Back-End lead on the ZeaVis Edu capstone team (&quot;AI I was the Back-End lead on the ZeaVis Edu capstone team (&ldquo;AI
for Smart Education&quot; by Pijak &times; IBM SkillsBuild), where for Smart Education&rdquo; by Pijak &times; IBM SkillsBuild),
I designed the system architecture, API contracts, and deployment a 5-person team with task division across Back-End, Front-End,
pipeline for a computer vision-based corn leaf disease detection and Machine Learning. I handled the system architecture, API
platform. design, RESTful API development, and Docker deployment on VPS.
</p> </p>
</CardContent> </div>
</Card> </div>
{/* Skills */} {/* Skills & Tools — grid */}
<Card> <div>
<CardHeader> <p className="mb-2 font-mono text-xs text-muted-foreground">
<CardTitle>Skills &amp; Tools</CardTitle> <span className="text-primary dark:text-primary">~</span> skills
</CardHeader> </p>
<CardContent> <div className="mb-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
<div className="mb-4 grid grid-cols-1 gap-2 sm:grid-cols-2"> {skills.map((s) => {
{skills.map((s) => { const Icon = s.icon;
const Icon = s.icon; return (
return ( <div
<div key={s.label}
key={s.label} className="flex items-center gap-3 rounded-lg border border-border bg-card px-3.5 py-2.5 text-sm"
className="flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-sm"
>
<Icon className="size-4 shrink-0 text-muted-foreground" />
<span>{s.label}</span>
</div>
);
})}
</div>
<div className="flex flex-wrap gap-1.5">
{techTags.map((t) => (
<Badge
key={t}
variant="secondary"
className="text-[11px] font-normal"
> >
{t} <Icon className="size-4 shrink-0 text-primary dark:text-primary" />
</Badge> <span>{s.label}</span>
))} </div>
</div> );
</CardContent> })}
</Card> </div>
<div className="flex flex-wrap gap-1.5">
{techTags.map((t) => (
<Badge
key={t}
variant="outline"
className="text-[11px] font-normal"
>
{t}
</Badge>
))}
</div>
</div>
{/* Projects */} {/* Featured Project — elevated */}
<div id="projects" className="scroll-mt-20"> <div id="projects" className="scroll-mt-24">
<h2 className="mb-4 text-xl font-semibold tracking-tight"> <p className="mb-2 font-mono text-xs text-muted-foreground">
Featured Project <span className="text-primary dark:text-primary">~</span> featured
</h2> project
<Card> </p>
<Card className="relative overflow-hidden">
<CardHeader> <CardHeader>
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div> <div>
<CardTitle>ZeaVis Edu</CardTitle> <CardTitle className="text-lg">ZeaVis Edu</CardTitle>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-0.5 text-sm text-muted-foreground">
Asisten Edukasi Interaktif untuk Deteksi Penyakit Daun Asisten Edukasi Interaktif untuk Deteksi Penyakit Daun
Jagung Jagung
</p> </p>
@@ -179,19 +186,19 @@ export default function Home() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="text-sm text-muted-foreground"> <CardContent className="text-sm text-muted-foreground">
<p> <p className="mb-4">
A Computer Vision-powered educational platform that helps A Computer Vision-powered educational platform that helps
farmers, agricultural students, and field extension officers farmers, agricultural students, and field extension officers
identify corn leaf diseases by uploading a photo. Built as a identify corn leaf diseases by uploading a photo. Built as a
capstone project for the Pijak &times; IBM SkillsBuild program. capstone project for the Pijak &times; IBM SkillsBuild program.
</p> </p>
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div> <div className="rounded-lg border border-border bg-muted/30 p-3">
<p className="mb-1.5 text-xs font-semibold uppercase tracking-wider"> <p className="mb-2 text-xs font-semibold uppercase tracking-wider text-foreground">
What it does What it does
</p> </p>
<ul className="list-inside list-disc space-y-1 text-muted-foreground"> <ul className="list-inside list-disc space-y-1 text-xs">
<li>Upload a photo of a corn leaf</li> <li>Upload a photo of a corn leaf</li>
<li> <li>
AI classifies the disease &mdash; Blight, Rust, Leaf Spot, AI classifies the disease &mdash; Blight, Rust, Leaf Spot,
@@ -200,14 +207,14 @@ export default function Home() {
<li>Get treatment &amp; prevention recommendations</li> <li>Get treatment &amp; prevention recommendations</li>
</ul> </ul>
</div> </div>
<div> <div className="rounded-lg border border-border bg-muted/30 p-3">
<p className="mb-1.5 text-xs font-semibold uppercase tracking-wider"> <p className="mb-2 text-xs font-semibold uppercase tracking-wider text-foreground">
My role My role
</p> </p>
<ul className="list-inside list-disc space-y-1 text-muted-foreground"> <ul className="list-inside list-disc space-y-1 text-xs">
<li>System architecture &amp; API design</li> <li>System architecture &amp; API design</li>
<li>RESTful API (Bun + Elysia + Drizzle + PostgreSQL)</li> <li>RESTful API (Bun + Elysia + Drizzle + PostgreSQL)</li>
<li>Rust inference engine (Axum + ONNX Runtime)</li> <li>Secure upload handling</li>
<li> <li>
Docker deployment on VPS with Traefik &amp; Tailscale Docker deployment on VPS with Traefik &amp; Tailscale
</li> </li>
@@ -234,7 +241,7 @@ export default function Home() {
].map((t) => ( ].map((t) => (
<Badge <Badge
key={t} key={t}
variant="outline" variant="secondary"
className="text-[10px] font-normal" className="text-[10px] font-normal"
> >
{t} {t}
@@ -245,16 +252,16 @@ export default function Home() {
</Card> </Card>
</div> </div>
{/* Infrastructure CTA */} {/* Infrastructure CTA — clean card */}
<Card> <Card className="text-center">
<CardContent className="flex flex-col items-center gap-4 py-8 text-center"> <CardContent className="flex flex-col items-center gap-4 py-10">
<p className="text-base font-medium"> <p className="text-base font-medium">
This site doubles as a live infrastructure monitor. This site doubles as a live infrastructure monitor.
</p> </p>
<p className="max-w-md text-sm text-muted-foreground"> <p className="max-w-md text-sm text-muted-foreground">
The dashboard tracks Docker services, Traefik request metrics, The dashboard tracks Docker services, Traefik request metrics,
Prometheus system resources, and Jaeger traces across the Prometheus system resources, and Jaeger traces all running on
deployment. my VPS deployment.
</p> </p>
<a href="/dashboard" className={cn(buttonVariants())}> <a href="/dashboard" className={cn(buttonVariants())}>
Live Dashboard Live Dashboard
+13
View File
@@ -1,13 +1,17 @@
"use client"; "use client";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import type { DashboardData } from "@/lib/dashboard/types"; import type { DashboardData } from "@/lib/dashboard/types";
export function DashboardHeader() { export function DashboardHeader() {
const [degraded, setDegraded] = useState(0); const [degraded, setDegraded] = useState(0);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [time, setTime] = useState(""); const [time, setTime] = useState("");
const { theme, setTheme } = useTheme();
useEffect(() => { useEffect(() => {
const fetchStatus = async () => { const fetchStatus = async () => {
@@ -74,6 +78,15 @@ export function DashboardHeader() {
? `${degraded} degraded` ? `${degraded} degraded`
: "All Systems Operational"} : "All Systems Operational"}
</Badge> </Badge>
<Button
variant="ghost"
size="icon-xs"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
aria-label="Toggle theme"
>
<Sun className="hidden dark:block!" />
<Moon className="block dark:hidden!" />
</Button>
<span className="text-[10px] text-muted-foreground">{time}</span> <span className="text-[10px] text-muted-foreground">{time}</span>
</div> </div>
</header> </header>
+1 -1
View File
@@ -12,7 +12,7 @@ function Card({
data-slot="card" data-slot="card"
data-size={size} data-size={size}
className={cn( className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl border border-border bg-card py-(--card-spacing) text-sm text-card-foreground [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className, className,
)} )}
{...props} {...props}