feat: implement initial fullstack portfolio application including dashboard, CMS, and analytics features.

This commit is contained in:
Moh Dzulfikri Maulana
2026-03-07 16:32:49 +07:00
commit bdd61d11d3
59 changed files with 11107 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET() {
const now = new Date();
await prisma.blog.updateMany({
where: {
status: "SCHEDULED",
scheduledAt: { lte: now },
},
data: {
status: "PUBLISHED",
publishedAt: now,
},
});
return NextResponse.json({ success: true });
}

View File

@@ -0,0 +1,38 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { cookies } from "next/headers";
export async function POST(req: Request) {
const { email, password } = await req.json();
console.log("API LOGIN", email, password);
const user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET!, {
expiresIn: "1h",
});
(await cookies()).set("token", token, {
httpOnly: true,
secure: true,
sameSite: "strict",
path: "/",
});
return NextResponse.json({ success: true });
}

View File

@@ -0,0 +1,7 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function POST() {
(await cookies()).delete("token");
return NextResponse.json({ success: true, message: "Logout success" });
}

View File

@@ -0,0 +1,14 @@
import { prisma } from "@/lib/prisma";
export async function POST(req: Request) {
const { visitorId, path } = await req.json();
await prisma.pageView.create({
data: {
visitorId,
path,
},
});
return Response.json({ ok: true });
}

View File

@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
const contactSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
subject: z.string().min(4, 'Subject must be at least 4 characters'),
message: z.string().min(10, 'Message must be at least 10 characters'),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const parsed = contactSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ success: false, error: parsed.error.errors[0].message },
{ status: 400 }
)
}
const { name, email, subject, message } = parsed.data
await prisma.contactMessage.create({
data: { name, email, subject, message },
})
return NextResponse.json({
success: true,
message: 'Message received! I will get back to you soon.',
})
} catch (error) {
console.error('Contact form error:', error)
return NextResponse.json(
{ success: false, error: 'Failed to send message. Please try again.' },
{ status: 500 }
)
}
}

0
src/app/blog/page.tsx Normal file
View File

View File

@@ -0,0 +1,13 @@
import { LoginForm } from "@/components/dashboard/auth/LoginForm";
export default function AuthPage() {
return (
<section className="min-h-[100vh] flex items-center justify-center p-4 relative overflow-hidden bg-background">
{/* Background radial gradients for premium depth */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-accent/5 rounded-full blur-[120px] pointer-events-none" />
<div className="absolute bottom-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-[100px] pointer-events-none" />
<LoginForm />
</section>
);
}

View File

@@ -0,0 +1,16 @@
import { DashboardSidebar } from "@/components/sections/portfolio/DashboardSidebar";
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen bg-background">
<DashboardSidebar />
<div className="flex-1 flex flex-col min-w-0 md:pt-0 pt-16">
<main className="flex-1">{children}</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function CMSPage() {
return (
<section className="min-h-[100vh] flex items-center justify-center p-4 relative overflow-hidden bg-background">
<h1>CMS</h1>
</section>
);
}

193
src/app/globals.css Normal file
View File

@@ -0,0 +1,193 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 248 250 252;
--foreground: 15 23 42;
--card: 255 255 255;
--card-foreground: 15 23 42;
--popover: 255 255 255;
--popover-foreground: 15 23 42;
--primary: 99 102 241;
--primary-foreground: 255 255 255;
--secondary: 34 211 238;
--secondary-foreground: 248 250 252;
--muted: 241 245 249;
--muted-foreground: 71 85 105;
--accent: 99 102 241;
--accent-foreground: 15 23 42;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 34 211 238;
--input: 34 211 238;
--ring: 99 102 241;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 2 6 23;
--foreground: 248 250 252;
--card: 15 23 42;
--card-foreground: 248 250 252;
--popover: 15 23 42;
--popover-foreground: 248 250 252;
--primary: 99 102 241;
--primary-foreground: 255 255 255;
--secondary: 71 85 105;
--secondary-foreground: 248 250 252;
--muted: 248 250 252;
--muted-foreground: 248 250 252;
--accent: 99 102 241;
--accent-foreground: 2 6 23;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 71 85 105;
--input: 71 85 105;
--ring: 99 102 241;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
/* ─────────────────────────────────────────── */
/* DARK THEME (default) */
/* ─────────────────────────────────────────── */
html,
:root,
.dark {
--color-background: #020617;
--color-surface: #020617;
--color-surface-elevated: #0F172A;
--color-border: #475569;
--color-border-subtle: #47556980;
--color-accent: #6366F1;
--color-accent-glow: #6366F140;
--color-accent-dim: #6366F1;
--color-muted: #F8FAFC;
--color-subtle: #F8FAFC;
--color-foreground: #F8FAFC;
--color-foreground-muted: #F8FAFC;
--color-foreground-subtle: #F8FAFC;
}
/* ─────────────────────────────────────────── */
/* LIGHT THEME */
/* ─────────────────────────────────────────── */
:root.light,
.light {
--color-background: #F8FAFC;
--color-surface: #FFFFFF;
--color-surface-elevated: #F1F5F9;
--color-border: #22D3EE;
--color-border-subtle: #22D3EE60;
--color-accent: #6366F1;
--color-accent-glow: #6366F120;
--color-accent-dim: #6366F1;
--color-muted: #64748B;
--color-subtle: #475569;
--color-foreground: #0F172A;
--color-foreground-muted: #475569;
--color-foreground-subtle: #64748B;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html {
scroll-behavior: smooth;
scroll-padding-top: 80px;
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
transition:
background-color 0.3s ease,
color 0.3s ease;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-background);
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-accent);
}
/* Selection */
::selection {
background: var(--color-accent-glow);
color: var(--color-foreground);
}
/* Grid background utility */
.bg-grid {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='rgb(71 85 105 / 0.4)'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e");
}
.light .bg-grid {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32' fill='none' stroke='rgb(2 6 23 / 0.3)'%3e%3cpath d='M0 .5H31.5V32'/%3e%3c/svg%3e");
}
/* Gradient text utility */
.gradient-text {
background: linear-gradient(
135deg,
var(--color-foreground) 0%,
var(--color-accent) 50%,
#22D3EE 100%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.gradient-text-blue {
background: linear-gradient(135deg, var(--color-accent-dim) 0%, var(--color-accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Noise texture overlay */
.noise::after {
content: "";
position: absolute;
inset: 0;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
pointer-events: none;
}
/* Glow effect */
.glow-blue {
box-shadow: 0 0 30px var(--color-accent-glow);
}
/* Timeline */
.timeline-line {
background: linear-gradient(to bottom, var(--color-accent), transparent);
}

119
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,119 @@
import type { Metadata } from "next";
import { Syne, DM_Sans, DM_Mono } from "next/font/google";
import "./globals.css";
import { Navbar } from "@/components/sections/portfolio/Navbar";
import { Footer } from "@/components/sections/portfolio/Footer";
import { ThemeProvider } from "@/components/ThemeProvider";
import { Analytics } from "@vercel/analytics/react";
const syne = Syne({
subsets: ["latin"],
weight: ["400", "500", "600", "700", "800"],
variable: "--font-display",
display: "swap",
});
const dmSans = DM_Sans({
subsets: ["latin"],
weight: ["300", "400", "500", "600"],
style: ["normal", "italic"],
variable: "--font-sans",
display: "swap",
});
const dmMono = DM_Mono({
subsets: ["latin"],
weight: ["300", "400", "500"],
variable: "--font-mono",
display: "swap",
});
export const metadata: Metadata = {
metadataBase: new URL(
process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
),
title: {
default: "Fikri — Fullstack Developer",
template: "%s | Fikri",
},
description:
"Fullstack Developer with 3+ years of experience building scalable and production-ready web applications using Next.js, NestJS, and PostgreSQL. Focused on clean architecture, performance, and maintainable systems.",
keywords: [
"Fikri",
"Fullstack Developer",
"Next.js Developer",
"NestJS Developer",
"TypeScript",
"React",
"Node.js",
"PostgreSQL",
"Prisma",
"Clean Architecture",
"Web Application Development",
],
authors: [{ name: "Fikri" }],
creator: "Fikri",
openGraph: {
type: "website",
locale: "en_US",
url: "https://yourportfolio.dev", // ganti dengan domain kamu
siteName: "Fikri — Fullstack Developer",
title: "Fikri — Fullstack Developer",
description:
"Fullstack Developer specializing in Next.js & NestJS with 3+ years of experience building scalable and production-ready web applications.",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Fikri — Fullstack Developer",
},
],
},
twitter: {
card: "summary_large_image",
title: "Fikri — Fullstack Developer",
description:
"Next.js & NestJS Specialist | Clean Architecture | Scalable Systems",
images: ["/og-image.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${syne.variable} ${dmSans.variable} ${dmMono.variable} dark`}
suppressHydrationWarning
>
<body className="bg-background text-foreground antialiased">
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange={false}
>
<Navbar />
<main>{children}</main>
<Footer />
<Analytics />
</ThemeProvider>
</body>
</html>
);
}

161
src/app/not-found.tsx Normal file
View File

@@ -0,0 +1,161 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { Home, MoveLeft, Terminal as TerminalIcon, AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
export default function NotFound() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<div className="relative min-h-[calc(100vh-80px)] flex flex-col items-center justify-center overflow-hidden px-6">
{/* Background effects */}
<div
className="absolute inset-0 bg-grid opacity-20"
aria-hidden="true"
/>
<div
className="absolute inset-0 bg-gradient-to-b from-background via-background/95 to-background"
aria-hidden="true"
/>
{/* Blur orbs */}
<div
className="absolute top-1/4 left-1/4 w-96 h-96 bg-accent/20 rounded-full blur-[120px] pointer-events-none animate-pulse-slow"
aria-hidden="true"
/>
<div
className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-purple-500/15 rounded-full blur-[100px] pointer-events-none animate-pulse-slow"
style={{ animationDelay: "2s" }}
aria-hidden="true"
/>
{/* Floating Icons */}
<div className="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
{[...Array(6)].map((_, i) => (
<motion.div
key={i}
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: [0.05, 0.15, 0.05],
scale: [1, 1.2, 1],
x: [0, Math.random() * 40 - 20, 0],
y: [0, Math.random() * 40 - 20, 0]
}}
transition={{
duration: 5 + Math.random() * 5,
repeat: Infinity,
delay: Math.random() * 5
}}
className="absolute"
style={{
top: `${Math.random() * 100}%`,
left: `${Math.random() * 100}%`,
}}
>
<div className="p-4 rounded-xl border border-border/20 bg-surface-elevated/10 backdrop-blur-sm">
<TerminalIcon size={24} className="text-accent/30" />
</div>
</motion.div>
))}
</div>
<div className="relative z-10 max-w-2xl w-full text-center">
{/* Error Code Container */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
className="mb-12 relative"
>
{/* Main Icon */}
<div className="inline-flex items-center justify-center w-28 h-28 rounded-[2rem] bg-surface-elevated border border-border/50 shadow-2xl mb-8 relative group">
<div className="absolute inset-0 bg-accent/30 rounded-[2rem] blur-2xl group-hover:bg-accent/50 transition-all duration-500" />
<AlertTriangle size={56} className="text-accent relative z-10" />
</div>
<div className="relative">
<h1 className="font-display font-extrabold text-[120px] md:text-[180px] tracking-[ -0.05em] leading-none mb-4 select-none">
<span className="text-foreground -mr-4 md:-mr-8">4</span>
<span className="gradient-text drop-shadow-glow">0</span>
<span className="text-foreground -ml-4 md:-ml-8">4</span>
</h1>
<motion.div
animate={{ opacity: [0.4, 1, 0.4] }}
transition={{ duration: 2, repeat: Infinity }}
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-full bg-accent/5 blur-[120px] pointer-events-none"
/>
</div>
</motion.div>
{/* Message */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="space-y-4"
>
<h2 className="text-3xl md:text-5xl font-display font-bold text-foreground tracking-tight">
Oops! You&apos;re <span className="text-accent italic">Lost</span>
</h2>
<p className="text-foreground-muted text-lg md:text-xl mb-12 max-w-lg mx-auto leading-relaxed">
The page you are looking for has vanished into the digital void.
Don&apos;t worry, we can find our way back together.
</p>
</motion.div>
{/* Actions */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}
className="flex flex-col sm:flex-row items-center justify-center gap-6 mt-12"
>
<Link
href="/"
className="group relative inline-flex items-center gap-3 px-10 py-5 bg-accent text-white font-bold rounded-2xl hover:bg-accent/90 transition-all hover:shadow-glow hover:-translate-y-1.5 duration-300 w-full sm:w-auto justify-center overflow-hidden"
>
<div className="absolute inset-0 w-3/4 h-full bg-white/20 -skew-x-[45deg] -translate-x-full group-hover:translate-x-[200%] transition-transform duration-1000" />
<Home size={20} className="relative z-10" />
<span className="relative z-10">Back to Home</span>
</Link>
<button
onClick={() => window.history.back()}
className="inline-flex items-center gap-3 px-10 py-5 bg-surface-elevated/50 backdrop-blur-md border border-border text-foreground font-semibold rounded-2xl hover:border-accent/60 hover:bg-surface-elevated/80 transition-all hover:-translate-y-1.5 duration-300 w-full sm:w-auto justify-center"
>
<MoveLeft size={20} className="text-accent" />
Return Back
</button>
</motion.div>
{/* Terminal Status */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, delay: 1 }}
className="mt-20 flex items-center justify-center gap-4 text-foreground-subtle font-mono text-xs md:text-sm tracking-widest uppercase"
>
<div className="flex gap-2">
<div className="w-2 h-2 rounded-full bg-red-500/50" />
<div className="w-2 h-2 rounded-full bg-yellow-500/50" />
<div className="w-2 h-2 rounded-full bg-green-500/50" />
</div>
<span className="border-l border-border/30 pl-4">System Error: 404_PAGE_NOT_FOUND</span>
<span className="w-2 h-5 bg-accent animate-pulse" />
</motion.div>
</div>
{/* Noise Texture */}
<div className="noise pointer-events-none" aria-hidden="true" />
</div>
);
}

134
src/app/page.tsx Normal file
View File

@@ -0,0 +1,134 @@
import { HeroSection } from "@/components/sections/portfolio/HeroSection";
import { AboutSection } from "@/components/sections/portfolio/AboutSection";
import { TechStackSection } from "@/components/sections/portfolio/TechStackSection";
import { ProjectsSection } from "@/components/sections/portfolio/ProjectsSection";
import { ExperienceSection } from "@/components/sections/portfolio/ExperienceSection";
import { PrincipleSection } from "@/components/sections/portfolio/PrincipleSection";
import { ContactSection } from "@/components/sections/portfolio/ContactSection";
import { prisma } from "@/lib/prisma";
const fallbackProjects = [
{
id: "1",
title: "Enterprise ERP System",
description:
"Full-scale ERP system built with NestJS microservices, handling inventory, finance, and HR modules for 500+ concurrent users.",
imageUrl: null,
liveUrl: null,
githubUrl: "https://github.com",
techStack: ["NestJS", "Next.js", "PostgreSQL", "Redis", "Docker"],
featured: true,
order: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "2",
title: "Real-time Analytics Dashboard",
description:
"Interactive analytics platform with real-time data visualization, WebSocket integration, and multi-tenant architecture.",
imageUrl: null,
liveUrl: null,
githubUrl: "https://github.com",
techStack: ["Next.js", "NestJS", "Prisma", "Chart.js", "PostgreSQL"],
featured: true,
order: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "3",
title: "API Gateway & Auth Service",
description:
"Scalable API Gateway with JWT authentication, rate limiting, and microservice orchestration using Clean Architecture principles.",
imageUrl: null,
liveUrl: null,
githubUrl: "https://github.com",
techStack: ["NestJS", "Redis", "JWT", "Docker", "Kubernetes"],
featured: true,
order: 3,
createdAt: new Date(),
updatedAt: new Date(),
},
];
const fallbackExperiences = [
{
id: "1",
company: "Tech Corp Indonesia",
role: "Senior Fullstack Engineer",
startDate: new Date("2022-01-01"),
endDate: null,
current: true,
description:
"Leading development of enterprise-grade web applications with focus on scalability and clean architecture.",
highlights: [
"Architected microservices-based ERP system serving 500+ concurrent users",
"Reduced API response time by 60% through Redis caching & query optimization",
"Mentored 3 junior developers on NestJS patterns and SOLID principles",
],
techStack: ["Next.js", "NestJS", "PostgreSQL", "Redis", "Docker"],
order: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: "2",
company: "Startup Fintech",
role: "Fullstack Developer",
startDate: new Date("2020-03-01"),
endDate: new Date("2021-12-31"),
current: false,
description:
"Built core financial services platform from ground up with emphasis on security and performance.",
highlights: [
"Developed payment processing system handling Rp 50B+ monthly transactions",
"Built real-time notification system using WebSockets and Redis pub/sub",
"Implemented automated testing achieving 85%+ code coverage",
],
techStack: ["React", "Node.js", "Express", "MongoDB", "PostgreSQL"],
order: 2,
createdAt: new Date(),
updatedAt: new Date(),
},
];
async function getProjects() {
try {
return await prisma.project.findMany({
where: { featured: true },
orderBy: { order: "asc" },
});
} catch {
return fallbackProjects;
}
}
async function getExperiences() {
try {
return await prisma.experience.findMany({
orderBy: { order: "asc" },
});
} catch {
return fallbackExperiences;
}
}
export default async function HomePage() {
const [projects, experiences] = await Promise.all([
getProjects(),
getExperiences(),
]);
return (
<>
<HeroSection />
<AboutSection />
<TechStackSection />
<ProjectsSection projects={projects} />
<ExperienceSection experiences={experiences} />
<PrincipleSection />
<ContactSection />
</>
);
}