Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions src/app/api/contact/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NextResponse } from 'next/server'
import { after, NextResponse } from 'next/server'
import nodemailer from 'nodemailer'
import {
getWebhookDelivery,
Expand Down Expand Up @@ -160,26 +160,30 @@ export async function POST(request: Request) {
],
}

// Discord is the primary notification channel. Gmail is optional and must not
// prevent a configured webhook from receiving the contact message.
const [webhookResult, emailResult] = await Promise.allSettled([
sendDiscordWebhook(webhookUrl, webhookPayload),
sendEmail(name, email, message),
])

if (webhookResult.status === 'rejected') {
console.error('Contact webhook delivery failed:', webhookResult.reason)
// Discord is the primary delivery channel. The response depends only on this
// request, so unavailable or slow Gmail delivery can never reject the form.
try {
await sendDiscordWebhook(webhookUrl, webhookPayload)
} catch (error) {
console.error('Contact webhook delivery failed:', error)
return NextResponse.json(
{ error: 'Unable to deliver your message right now.' },
{ status: 502 },
)
}

const emailConfigured = Boolean(process.env.GMAIL_USER && process.env.GMAIL_PASSWORD)
if (emailConfigured) {
after(async () => {
await sendEmail(name, email, message)
})
}

return NextResponse.json({
ok: true,
deliveries: {
discord: true,
email: emailResult.status === 'fulfilled' && emailResult.value,
email: emailConfigured ? 'scheduled' : 'not_configured',
},
})
}
16 changes: 2 additions & 14 deletions src/app/api/notify-comment/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import {
sendDiscordWebhook,
} from '@/lib/webhookSettings'

const OWNER_EMAIL = 'dev.sxhd@gmail.com'

const clean = (value: unknown, limit = 1000) =>
typeof value === 'string' ? value.trim().slice(0, limit) : ''

Expand All @@ -32,7 +30,6 @@ export async function POST(req: NextRequest) {
color: 0x7c3aed,
fields: [
{ name: '👤 From', value: `**${name}**`, inline: true },
{ name: '📧 Notify', value: OWNER_EMAIL, inline: true },
{ name: '💬 Message', value: comment.slice(0, 1024), inline: false },
...(imageUrl ? [{ name: '🖼️ Image', value: imageUrl.slice(0, 1024), inline: false }] : []),
],
Expand All @@ -41,18 +38,9 @@ export async function POST(req: NextRequest) {
}],
})

// ── Build mailto URL (opened on client if user wants to reply) ─
const subject = encodeURIComponent(`Re: Comment from ${name} on your portfolio`)
const body = encodeURIComponent(
`Hi ${name},\n\nThanks for your comment on my portfolio!\n\n` +
`> "${comment}"\n\n` +
`---\nMuhammad Sahad\nportfolio-v1-eta-nine.vercel.app`
)

return NextResponse.json({
ok: true,
mailtoUrl: `mailto:${name}@?subject=${subject}&body=${body}`,
ownerMail: `mailto:${OWNER_EMAIL}?subject=${encodeURIComponent(`New comment from ${name}`)}&body=${encodeURIComponent(`Name: ${name}\nComment: ${comment}`)}`,
ok: true,
deliveries: { discord: true },
})
} catch (err: unknown) {
console.error('Comment webhook delivery failed:', err)
Expand Down
32 changes: 21 additions & 11 deletions src/components/sections/contact/CommentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useState } from 'react'
import { motion, AnimatePresence, Variants } from 'framer-motion'
import { Upload, Heart, Pin, Send, CheckCircle, Mail } from 'lucide-react'
import { Upload, Heart, Pin, Send, CheckCircle, AlertCircle, MessageSquare } from 'lucide-react'
import useComments from '@/hooks/useComments'

const smoothEase: [number, number, number, number] = [0.22, 1, 0.36, 1]
Expand All @@ -25,6 +25,7 @@ export default function CommentsSection() {
const [image, setImage] = useState<File | null>(null)
const [preview, setPreview] = useState<string | null>(null)
const [posted, setPosted] = useState(false)
const [webhookDelivered, setWebhookDelivered] = useState(true)

const handleImage = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
Expand All @@ -36,13 +37,16 @@ export default function CommentsSection() {
const handleSubmit = async () => {
if (!name.trim() || !comment.trim()) return

await addComment({ name, comment, image })
const result = await addComment({ name, comment, image })
if (!result?.posted) return

if (name.trim()) localStorage.setItem('_visitor_name', name.trim())

setName('')
setComment('')
setImage(null)
setPreview(null)
setWebhookDelivered(result.webhookDelivered)
setPosted(true)
setTimeout(() => setPosted(false), 4000)
}
Expand All @@ -59,10 +63,10 @@ export default function CommentsSection() {
<div className="mb-5 md:mb-6">
<h3 className="text-xl md:text-2xl font-semibold mb-1">Comments</h3>
<p className="text-xs md:text-sm text-white/40 flex items-center gap-1.5">
Leave your thoughts —
Leave your thoughts —
<span className="flex items-center gap-1 text-white/30">
<Mail size={11} />
dev.sxhd@gmail.com
<MessageSquare size={11} />
direct webhook notification
</span>
</p>
</div>
Expand All @@ -77,10 +81,16 @@ export default function CommentsSection() {
transition={{ duration: 0.35, ease: smoothEase }}
className="mb-4 flex items-center gap-2.5 px-4 py-3 rounded-2xl border border-emerald-500/20 bg-emerald-500/10"
>
<CheckCircle size={15} className="text-emerald-400 shrink-0" />
{webhookDelivered
? <CheckCircle size={15} className="text-emerald-400 shrink-0" />
: <AlertCircle size={15} className="text-amber-400 shrink-0" />}
<div>
<p className="text-[13px] text-emerald-300 font-medium">Comment posted!</p>
<p className="text-[11px] text-emerald-400/60">Muhammad Sahad has been notified at dev.sxhd@gmail.com</p>
<p className="text-[11px] text-emerald-400/60">
{webhookDelivered
? 'The comments webhook received your message.'
: 'The comment was saved, but its notification could not be delivered.'}
</p>
</div>
</motion.div>
)}
Expand Down Expand Up @@ -149,13 +159,13 @@ export default function CommentsSection() {
}
</motion.button>

{/* Email notice */}
{/* Delivery notice */}
<motion.p
variants={itemVariants}
className="text-center text-[11px] text-white/20 flex items-center justify-center gap-1"
>
<Mail size={10} />
Your comment will be sent to dev.sxhd@gmail.com
<MessageSquare size={10} />
Your comment will notify the comments webhook directly
</motion.p>
</motion.div>

Expand All @@ -174,7 +184,7 @@ export default function CommentsSection() {
initial={{ opacity: 0 }} animate={{ opacity: 1 }}
className="flex flex-col items-center justify-center h-32 gap-2 text-white/20"
>
<Mail size={22} />
<MessageSquare size={22} />
<p className="text-xs">Be the first to leave a comment</p>
</motion.div>
)}
Expand Down
5 changes: 3 additions & 2 deletions src/components/sections/contact/ContactForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default function ContactForm({ settings }: ContactFormProps) {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(15_000),
body: JSON.stringify({
name,
email,
Expand Down Expand Up @@ -113,7 +114,7 @@ export default function ContactForm({ settings }: ContactFormProps) {
className="flex items-center gap-2 mb-4 px-4 py-3 rounded-2xl border border-emerald-500/20 bg-emerald-500/10 text-emerald-300 text-sm"
>
<CheckCircle size={16} />
Message sent successfully.
Message delivered to the contact webhook.
</motion.div>
)}

Expand All @@ -124,7 +125,7 @@ export default function ContactForm({ settings }: ContactFormProps) {
className="flex items-center gap-2 mb-4 px-4 py-3 rounded-2xl border border-red-500/20 bg-red-500/10 text-red-300 text-sm"
>
<AlertCircle size={16} />
Message failed. Please try again.
Message could not reach the contact webhook. Please try again.
</motion.div>
)}

Expand Down
8 changes: 5 additions & 3 deletions src/hooks/useComments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,18 @@ export default function useComments() {
imageUrl = await uploadCommentImageService(image)
}

const newComment = await createCommentService({
const result = await createCommentService({
name,
comment,
imageUrl,
})

// instant UI update (without waiting for real time)
setComments((prev) => [newComment, ...prev])
setComments((prev) => [result.comment, ...prev])
return { posted: true, webhookDelivered: result.webhookDelivered }
} catch (err) {
console.log(err)
return { posted: false, webhookDelivered: false }
} finally {
setLoading(false)
}
Expand Down Expand Up @@ -116,4 +118,4 @@ export default function useComments() {
addComment,
likeComment,
}
}
}
5 changes: 3 additions & 2 deletions src/lib/commentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ export const createCommentService = async ({
body: JSON.stringify({ name, comment, imageUrl }),
}).catch(() => null)

if (!notificationResponse?.ok) {
const webhookDelivered = notificationResponse?.ok === true
if (!webhookDelivered) {
console.warn('The comment was saved, but its Discord notification was not delivered.')
}

return data
return { comment: data, webhookDelivered }
}