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}