diff --git a/package.json b/package.json
index 5d43e4a..81600b2 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"@prisma/client",
"@prisma/engines",
"bcrypt",
+ "esbuild",
"prisma",
"sharp",
"unrs-resolver"
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index dcc9e96..b827c8e 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -105,3 +105,15 @@ model Tag {
blogs Blog[]
}
+
+model SiteConfig {
+ id String @id @default(cuid())
+ cvUrl String?
+ linkedinUrl String?
+ githubUrl String?
+ twitterUrl String?
+ heroTitle String?
+ heroSubtitle String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
diff --git a/src/app/(portfolio)/blog/page.tsx b/src/app/(portfolio)/blog/page.tsx
index 5e565f4..cb43590 100644
--- a/src/app/(portfolio)/blog/page.tsx
+++ b/src/app/(portfolio)/blog/page.tsx
@@ -1,7 +1,13 @@
import { prisma } from "@/lib/prisma";
import { format } from "date-fns";
import Link from "next/link";
-import { Calendar, Clock, ArrowRight, Tag as TagIcon, Search } from "lucide-react";
+import {
+ Calendar,
+ Clock,
+ ArrowRight,
+ Tag as TagIcon,
+ Search,
+} from "lucide-react";
import { cn } from "@/lib/utils";
export const dynamic = "force-dynamic";
@@ -34,14 +40,18 @@ export default async function BlogPage() {
Blog.
- Exploring the world of fullstack development, architecture, and developer productivity. Sharing what I learn along the way.
+ Exploring the world of fullstack development, architecture, and
+ developer productivity. Sharing what I learn along the way.
@@ -95,13 +111,17 @@ function BlogCard({ blog, index }: { blog: any; index: number }) {
- {blog.excerpt || "No excerpt available for this post. Click to read the full article."}
+ {blog.excerpt ||
+ "No excerpt available for this post. Click to read the full article."}
{blog.tags.slice(0, 2).map((tag: any) => (
-
+
{tag.name}
diff --git a/src/app/api/dashboard/cms/site-config/route.ts b/src/app/api/dashboard/cms/site-config/route.ts
new file mode 100644
index 0000000..160f885
--- /dev/null
+++ b/src/app/api/dashboard/cms/site-config/route.ts
@@ -0,0 +1,54 @@
+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 GET() {
+ try {
+ const siteConfig = await prisma.siteConfig.findFirst();
+ return NextResponse.json({ success: true, data: siteConfig });
+ } catch (error) {
+ console.error("Site config error:", error);
+ return NextResponse.json(
+ { success: false, error: "Failed to get site config" },
+ { status: 500 },
+ );
+ }
+}
+
+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 },
+ );
+ }
+}
diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts
new file mode 100644
index 0000000..59767b2
--- /dev/null
+++ b/src/app/api/health/route.ts
@@ -0,0 +1,12 @@
+export const dynamic = "force-dynamic";
+import { NextResponse } from "next/server";
+
+export async function GET() {
+ return NextResponse.json(
+ {
+ status: "ok",
+ timestamp: new Date().toISOString(),
+ },
+ { status: 200 },
+ );
+}
diff --git a/src/components/sections/portfolio/ContactSection.tsx b/src/components/sections/portfolio/ContactSection.tsx
index f60f20d..75e144c 100644
--- a/src/components/sections/portfolio/ContactSection.tsx
+++ b/src/components/sections/portfolio/ContactSection.tsx
@@ -1,58 +1,60 @@
-'use client'
+"use client";
-import { useState } from 'react'
-import { motion, AnimatePresence } from 'framer-motion'
-import { useForm } from 'react-hook-form'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { z } from 'zod'
-import { Send, CheckCircle2, Mail, MapPin, Clock } from 'lucide-react'
-import type { ContactFormData } from '@/types'
+import { useState } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { Send, CheckCircle2, Mail, MapPin, Clock } from "lucide-react";
+import type { ContactFormData } from "@/types";
const schema = z.object({
- name: z.string().min(2, 'Name must be at least 2 characters'),
- email: z.string().email('Please enter a valid email'),
- subject: z.string().min(4, 'Subject must be at least 4 characters'),
- message: z.string().min(10, 'Message must be at least 10 characters'),
-})
+ name: z.string().min(2, "Name must be at least 2 characters"),
+ email: z.string().email("Please enter a valid email"),
+ subject: z.string().min(4, "Subject must be at least 4 characters"),
+ message: z.string().min(10, "Message must be at least 10 characters"),
+});
const contactInfo = [
- { icon: Mail, label: 'Email', value: 'hello@yourportfolio.dev' },
- { icon: MapPin, label: 'Location', value: 'Indonesia' },
- { icon: Clock, label: 'Response time', value: 'Within 24 hours' },
-]
+ { icon: Mail, label: "Email", value: "developer@dzulfikri.com" },
+ { icon: MapPin, label: "Location", value: "Indonesia" },
+ { icon: Clock, label: "Response time", value: "Within 24 hours" },
+];
export function ContactSection() {
- const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
- const [errorMsg, setErrorMsg] = useState('')
+ const [status, setStatus] = useState<
+ "idle" | "loading" | "success" | "error"
+ >("idle");
+ const [errorMsg, setErrorMsg] = useState("");
const {
register,
handleSubmit,
reset,
formState: { errors },
- } = useForm({ resolver: zodResolver(schema) })
+ } = useForm({ resolver: zodResolver(schema) });
const onSubmit = async (data: ContactFormData) => {
- setStatus('loading')
+ setStatus("loading");
try {
- const res = await fetch('/api/contact', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ const res = await fetch("/api/contact", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
- })
- const json = await res.json()
+ });
+ const json = await res.json();
if (json.success) {
- setStatus('success')
- reset()
+ setStatus("success");
+ reset();
} else {
- setErrorMsg(json.error || 'Something went wrong')
- setStatus('error')
+ setErrorMsg(json.error || "Something went wrong");
+ setStatus("error");
}
} catch {
- setErrorMsg('Network error. Please try again.')
- setStatus('error')
+ setErrorMsg("Network error. Please try again.");
+ setStatus("error");
}
- }
+ };
return (
@@ -88,8 +90,8 @@ export function ContactSection() {
transition={{ duration: 0.5, delay: 0.2 }}
className="text-foreground-muted mt-4 max-w-xl mx-auto"
>
- Have a project in mind? Let's talk about how I can help you build something
- remarkable.
+ Have a project in mind? Let's talk about how I can help you build
+ something remarkable.