diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts index 22616d7..7220034 100644 --- a/src/app/api/contact/route.ts +++ b/src/app/api/contact/route.ts @@ -1,4 +1,4 @@ -import { NextResponse } from 'next/server' +import { after, NextResponse } from 'next/server' import nodemailer from 'nodemailer' import { getWebhookDelivery, @@ -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', }, }) } diff --git a/src/app/api/notify-comment/route.ts b/src/app/api/notify-comment/route.ts index 1ea94eb..e9a67b8 100644 --- a/src/app/api/notify-comment/route.ts +++ b/src/app/api/notify-comment/route.ts @@ -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) : '' @@ -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 }] : []), ], @@ -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) diff --git a/src/components/sections/contact/CommentsSection.tsx b/src/components/sections/contact/CommentsSection.tsx index 99d2ca9..9cec4ca 100644 --- a/src/components/sections/contact/CommentsSection.tsx +++ b/src/components/sections/contact/CommentsSection.tsx @@ -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] @@ -25,6 +25,7 @@ export default function CommentsSection() { const [image, setImage] = useState(null) const [preview, setPreview] = useState(null) const [posted, setPosted] = useState(false) + const [webhookDelivered, setWebhookDelivered] = useState(true) const handleImage = (e: React.ChangeEvent) => { const file = e.target.files?.[0] @@ -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) } @@ -59,10 +63,10 @@ export default function CommentsSection() {

Comments

- Leave your thoughts — + Leave your thoughts — - - dev.sxhd@gmail.com + + direct webhook notification

@@ -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" > - + {webhookDelivered + ? + : }

Comment posted!

-

Muhammad Sahad has been notified at dev.sxhd@gmail.com

+

+ {webhookDelivered + ? 'The comments webhook received your message.' + : 'The comment was saved, but its notification could not be delivered.'} +

)} @@ -149,13 +159,13 @@ export default function CommentsSection() { } - {/* Email notice */} + {/* Delivery notice */} - - Your comment will be sent to dev.sxhd@gmail.com + + Your comment will notify the comments webhook directly @@ -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" > - +

Be the first to leave a comment

)} diff --git a/src/components/sections/contact/ContactForm.tsx b/src/components/sections/contact/ContactForm.tsx index 5cc6054..7b916ac 100644 --- a/src/components/sections/contact/ContactForm.tsx +++ b/src/components/sections/contact/ContactForm.tsx @@ -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, @@ -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" > - Message sent successfully. + Message delivered to the contact webhook. )} @@ -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" > - Message failed. Please try again. + Message could not reach the contact webhook. Please try again. )} diff --git a/src/hooks/useComments.ts b/src/hooks/useComments.ts index c5b8f22..93e065a 100644 --- a/src/hooks/useComments.ts +++ b/src/hooks/useComments.ts @@ -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) } @@ -116,4 +118,4 @@ export default function useComments() { addComment, likeComment, } -} \ No newline at end of file +} diff --git a/src/lib/commentService.ts b/src/lib/commentService.ts index 3c128fd..ae063aa 100644 --- a/src/lib/commentService.ts +++ b/src/lib/commentService.ts @@ -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 } }