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.

- - + @@ -61,8 +71,13 @@ export default async function BlogPage() {
-

No publications yet

-

Writing takes time. Check back soon for new insights and stories.

+

+ No publications yet +

+

+ Writing takes time. Check back soon for new insights and + stories. +

)} @@ -82,11 +97,12 @@ function BlogCard({ blog, index }: { blog: any; index: number }) {
- {blog.publishedAt ? format(new Date(blog.publishedAt), "MMM dd, yyyy") : "Draft"} + {blog.publishedAt + ? format(new Date(blog.publishedAt), "MMM dd, yyyy") + : "Draft"}
- - 5 min read + 5 min read
@@ -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.
@@ -103,7 +105,7 @@ export function ContactSection() { className="lg:col-span-2 space-y-4" > {contactInfo.map((item) => { - const Icon = item.icon + const Icon = item.icon; return (
-

{item.label}

-

{item.value}

+

+ {item.label} +

+

+ {item.value} +

- ) + ); })}

- I'm currently open to{' '} - freelance projects,{' '} - full-time positions, and - interesting technical collaborations. + I'm currently open to{" "} + + freelance projects + + ,{" "} + + full-time positions + + , and interesting technical collaborations.

@@ -140,7 +151,7 @@ export function ContactSection() { >
- {status === 'success' ? ( + {status === "success" ? (

- Thank you for reaching out. I'll get back to you within 24 hours. + Thank you for reaching out. I'll get back to you within 24 + hours.

@@ -192,13 +206,15 @@ export function ContactSection() { Email {errors.email && ( -

{errors.email.message}

+

+ {errors.email.message} +

)}
@@ -208,12 +224,14 @@ export function ContactSection() { Subject {errors.subject && ( -

{errors.subject.message}

+

+ {errors.subject.message} +

)} @@ -222,17 +240,19 @@ export function ContactSection() { Message