feat: implement initial fullstack portfolio application including dashboard, CMS, and analytics features.
This commit is contained in:
19
src/app/api/cron/publish/route.ts
Normal file
19
src/app/api/cron/publish/route.ts
Normal 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 });
|
||||
}
|
||||
38
src/app/api/dashboard/auth/login/route.ts
Normal file
38
src/app/api/dashboard/auth/login/route.ts
Normal 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 });
|
||||
}
|
||||
7
src/app/api/dashboard/auth/logout/route.ts
Normal file
7
src/app/api/dashboard/auth/logout/route.ts
Normal 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" });
|
||||
}
|
||||
14
src/app/api/dashboard/cms/analytics/track/route.ts
Normal file
14
src/app/api/dashboard/cms/analytics/track/route.ts
Normal 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 });
|
||||
}
|
||||
41
src/app/api/dashboard/cms/contact/route.ts
Normal file
41
src/app/api/dashboard/cms/contact/route.ts
Normal 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
0
src/app/blog/page.tsx
Normal file
13
src/app/dashboard/auth/page.tsx
Normal file
13
src/app/dashboard/auth/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
16
src/app/dashboard/layout.tsx
Normal file
16
src/app/dashboard/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
src/app/dashboard/overview/page.tsx
Normal file
7
src/app/dashboard/overview/page.tsx
Normal 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
193
src/app/globals.css
Normal 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
119
src/app/layout.tsx
Normal 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
161
src/app/not-found.tsx
Normal 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'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'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
134
src/app/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user