Cara Membuat Kolom Komentar Cerdas (Smart UX) di Next.js dengan NextAuth dan Tailwind CSS

PE
Pena Edukasi
Tampilan kolom komentar website modern dengan form tulis komentar dan daftar komentar pengguna

Tampilan fitur komentar website modern dengan kolom komentar, tombol kirim, dan daftar komentar pengguna.

Interaksi pembaca adalah urat nadi bagi sebuah portal artikel atau blog. Namun, salah satu masalah terbesar yang sering membuat pengunjung batal berkomentar adalah proses pendaftaran (login) yang rumit. Bayangkan seorang pembaca sudah lelah mengetik komentar panjang, lalu saat mengklik tombol "Kirim", mereka diarahkan ke halaman login dan komentar mereka hilang begitu saja. Ini adalah mimpi buruk dalam User Experience (UX).

Artikel ini akan membahas secara tuntas cara membangun Kolom Komentar Cerdas (Smart UX) menggunakan Next.js, Tailwind CSS, dan NextAuth.js. Kita akan menerapkan fitur Post-Login Redirect Action: jika pengunjung yang belum login mengirim komentar, sistem akan menyimpan draf tersebut secara otomatis, mengarahkan mereka untuk login via Google, dan langsung menerbitkan komentar tersebut sesaat setelah login berhasil.

Persiapan Sistem (Prasyarat)

Sebelum memulai pembuatan komponen, pastikan proyek website Anda sudah memiliki spesifikasi dasar berikut:

  • Framework: Next.js (disarankan menggunakan App Router app/).
  • Styling: Tailwind CSS untuk desain antarmuka yang responsif, modern, dan minimalis.
  • Autentikasi: NextAuth.js yang sudah dikonfigurasi dengan Google Provider (atau provider lainnya).

Langkah 1: Membuat Komponen CommentSection.tsx

Langkah pertama adalah membuat antarmuka (UI) kolom komentar dan menanamkan logika cerdas di dalamnya. Buat sebuah file baru di dalam direktori components (misalnya src/components/CommentSection.tsx) dan salin kode berikut.

Catatan: Kode di bawah ini menggunakan localStorage untuk menahan teks sementara selama proses autentikasi berlangsung.

"use client";

import { useState, useEffect } from "react";
import { useSession, signIn } from "next-auth/react";

type CommentType = {
  id: number;
  initial: string;
  avatarUrl?: string;
  bgColor: string;
  name: string;
  time: string;
  text: string;
  likes: number;
  dislikes: number;
};

export default function CommentSection() {
  const { data: session, status } = useSession();
  const isLoggedIn = status === "authenticated";

  const [commentText, setCommentText] = useState("");
  const [comments, setComments] = useState<CommentType[]>([]);
  const [activeTab, setActiveTab] = useState("Terbaru");

  const maxChars = 1000;
  const tabs = ["Terbaru", "Terpilih", "Terpopuler", "Teramai", "Komentar yang disematkan"];

  // ==========================================
  // EFEK SETELAH LOGIN SUKSES (Mengecek Draf)
  // ==========================================
  useEffect(() => {
    if (isLoggedIn && session?.user) {
      const pendingComment = localStorage.getItem("pending_comment");
      
      if (pendingComment) {
        const googleName = session.user.name || "Akun Pengunjung";
        const words = googleName.trim().split(" ");
        const init = words.length === 1 
            ? words[0].substring(0, 2).toUpperCase()
            : (words[0][0] + words[1][0]).toUpperCase();

        const autoComment: CommentType = {
          id: Date.now(),
          initial: init,
          avatarUrl: session.user.image || undefined,
          bgColor: "#1e3a8a",
          name: googleName,
          time: "Baru saja",
          text: pendingComment,
          likes: 0,
          dislikes: 0,
        };

        setComments((prev) => [autoComment, ...prev]);
        setCommentText("");
        localStorage.removeItem("pending_comment");
      }
    }
  }, [isLoggedIn, session]);

  // ==========================================
  // FUNGSI TOMBOL KIRIM
  // ==========================================
  const handleKirim = () => {
    if (commentText.trim() === "") return;

    // SKENARIO 1: JIKA BELUM LOGIN
    if (!isLoggedIn) {
      const confirmLogin = window.confirm(
        "Anda tidak bisa komentar sebelum login terlebih dahulu.\n\nKlik OK untuk lanjut login."
      );
      
      if (confirmLogin) {
        localStorage.setItem("pending_comment", commentText);
        signIn("google");
      }
      return;
    }

    // SKENARIO 2: JIKA SUDAH LOGIN
    const googleName = session?.user?.name || "Akun Pengunjung";
    const words = googleName.trim().split(" ");
    const init = words.length === 1 
        ? words[0].substring(0, 2).toUpperCase()
        : (words[0][0] + words[1][0]).toUpperCase();

    const newComment: CommentType = {
      id: Date.now(),
      initial: init,
      avatarUrl: session?.user?.image || undefined,
      bgColor: "#1e3a8a",
      name: googleName,
      time: "Baru saja",
      text: commentText,
      likes: 0,
      dislikes: 0,
    };

    setComments([newComment, ...comments]);
    setCommentText("");
  };

  return (
    <div className="w-full mt-10 font-sans">
      <h3 className="text-xl font-bold text-foreground mb-4">
        Komentar <span className="font-normal">({comments.length})</span>
      </h3>

      <div className="bg-slate-50 dark:bg-slate-900 p-4 rounded-2xl mb-6">
        <div className="bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl p-3 shadow-sm">
          <textarea
            value={commentText}
            onChange={(e) => setCommentText(e.target.value)}
            maxLength={maxChars}
            placeholder={isLoggedIn ? `Berkomentar sebagai ${session?.user?.name}...` : "Tulis Komentar..."}
            className="w-full resize-none outline-none bg-transparent text-sm text-foreground placeholder:text-slate-400 min-h-[60px]"
          />
          
          <div className="flex justify-between items-center mt-2 border-t border-slate-100 dark:border-slate-800 pt-3">
            <span className="text-xs text-slate-500 font-medium">
              <strong className="text-foreground font-semibold">{maxChars - commentText.length}</strong> Karakter tersisa
            </span>
            <button 
              onClick={handleKirim}
              disabled={commentText.trim() === ""}
              className={`flex items-center gap-2 px-5 py-1.5 rounded-full text-sm font-medium transition-colors ${
                commentText.trim() === "" 
                  ? "bg-slate-300 text-slate-500 cursor-not-allowed" 
                  : "bg-[#1e3a8a] hover:bg-[#152c6e] text-white"
              }`}
            >
              Kirim
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M22 2 11 13" />
                <path d="M22 2 15 22 11 13 2 9 22 2z" />
              </svg>
            </button>
          </div>
        </div>
      </div>

      <div className="flex overflow-x-auto gap-2.5 mb-6 pb-2 scrollbar-hide">
        {tabs.map((tab) => (
          <button
            key={tab}
            onClick={() => setActiveTab(tab)}
            className={`whitespace-nowrap px-4 py-1.5 rounded-full text-sm font-medium border transition-colors ${
              activeTab === tab
                ? "bg-[#1e3a8a] border-[#1e3a8a] text-white"
                : "bg-transparent border-slate-300 dark:border-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800"
            }`}
          >
            {tab}
          </button>
        ))}
      </div>

      <div className="flex flex-col">
        {comments.length === 0 ? (
          <div className="text-center py-8 text-slate-500 text-sm">
            Belum ada komentar. Jadilah yang pertama memberikan tanggapan!
          </div>
        ) : (
          comments.map((comment) => (
            <div key={comment.id} className="flex gap-4 py-5 border-b border-slate-100 dark:border-slate-800">
              <div 
                className="w-12 h-12 rounded-full flex items-center justify-center text-white font-bold text-lg flex-shrink-0 overflow-hidden"
                style={{ backgroundColor: comment.bgColor }}
              >
                {comment.avatarUrl ? (
                  <img src={comment.avatarUrl} alt={comment.name} className="w-full h-full object-cover" />
                ) : (
                  comment.initial
                )}
              </div>

              <div className="flex-1 min-w-0">
                <div className="flex justify-between items-start mb-1">
                  <div>
                    <h4 className="font-bold text-foreground text-sm inline-block mr-2">{comment.name}</h4>
                    <span className="text-xs text-slate-500">{comment.time}</span>
                  </div>
                </div>
                <p className="text-sm text-foreground mt-1.5 leading-relaxed whitespace-pre-wrap">
                  {comment.text}
                </p>
              </div>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

Baca Juga: Adab kepada Orang Tua dan Guru: Dua Pintu Keberkahan dalam Hidup

Penjelasan Logika Sistem:

  1. Validasi Input: Tombol "Kirim" dilengkapi properti disabled yang aktif jika input kosong. Ini mencegah pengunjung mengirim spam atau komentar kosong secara tidak sengaja.
  2. Skenario Guest (Belum Login): Saat tombol kirim ditekan oleh guest, sistem memunculkan pop-up peringatan wajib login. Jika pengunjung menekan OK, teks komentar mereka disimpan sementara di memori browser (localStorage.setItem). Sistem kemudian mengeksekusi signIn("google").
  3. Reklamasi Draf Otomatis (useEffect): Ketika pengunjung kembali ke artikel setelah berhasil login, fungsi useEffect akan mendeteksi status isLoggedIn. Sistem kemudian memeriksa memori lokal. Jika ada draf yang tertinggal, sistem membungkusnya dengan data Nama dan Foto Profil asli akun Google mereka, memunculkannya ke layar, dan segera menghapus draf tersebut agar tidak terkirim dua kali.

Langkah 2: Memasang Komponen di Halaman Artikel

Setelah komponen selesai dibuat, langkah terakhir adalah memanggilnya ke dalam template halaman artikel Anda. Di Next.js App Router, file ini biasanya terletak di rute dinamis app/post/[slug]/page.tsx.

  1. Impor Komponen: Tambahkan baris pemanggil ini di deretan import pada bagian atas file Anda.
import CommentSection from "@/components/CommentSection"
  1. Letakkan di Bawah Artikel: Gulir ke bagian paling bawah dari struktur kode konten Anda, temukan batas akhir dari komponen artikel (misalnya di bawah Artikel Terkait), lalu sisipkan kode <CommentSection /> beserta garis pemisah horizontal agar terlihat rapi.
{/* ===================================================
    GARIS PEMISAH & KOLOM KOMENTAR
    =================================================== */}
<div className="px-4 md:px-0">
  <hr className="my-10 border-t border-border opacity-60" />
  <CommentSection />
</div>
{/* =================================================== */}

Kesimpulan

Dengan menerapkan strategi ini, Anda tidak hanya mendapatkan desain kolom komentar yang bersih layaknya portal media nasional premium, tetapi juga berhasil menyelamatkan interaksi pengguna yang berharga. Pengunjung tidak perlu frustrasi mengetik ulang pemikiran mereka dari nol akibat proses autentikasi. Hal ini secara langsung akan mendorong metrik keterlibatan (engagement rate) serta durasi sesi pada website Anda meningkat secara signifikan.nteraksi pembaca adalah urat nadi bagi sebuah portal artikel atau blog. Namun, salah satu masalah terbesar yang sering membuat pengunjung batal berkomentar adalah proses pendaftaran (login) yang rumit. Bayangkan seorang pembaca sudah lelah mengetik komentar panjang, lalu saat mengklik tombol "Kirim", mereka diarahkan ke halaman login dan komentar mereka hilang begitu saja. Ini adalah mimpi buruk dalam User Experience (UX).

Artikel ini akan membahas secara tuntas cara membangun Kolom Komentar Cerdas (Smart UX) menggunakan Next.js, Tailwind CSS, dan NextAuth.js. Kita akan menerapkan fitur Post-Login Redirect Action: jika pengunjung yang belum login mengirim komentar, sistem akan menyimpan draf tersebut secara otomatis, mengarahkan mereka untuk login via Google, dan langsung menerbitkan komentar tersebut sesaat setelah login berhasil.

Baca Juga: Cara Generate dan Verifikasi API Key IndexNow di Bing Webmaster Tools

Persiapan Sistem (Prasyarat)

Sebelum memulai pembuatan komponen, pastikan proyek website Anda sudah memiliki spesifikasi dasar berikut:

  • Framework: Next.js (disarankan menggunakan App Router app/).
  • Styling: Tailwind CSS untuk desain antarmuka yang responsif, modern, dan minimalis.
  • Autentikasi: NextAuth.js yang sudah dikonfigurasi dengan Google Provider (atau provider lainnya).

Langkah 1: Membuat Komponen CommentSection.tsx

Langkah pertama adalah membuat antarmuka (UI) kolom komentar dan menanamkan logika cerdas di dalamnya. Buat sebuah file baru di dalam direktori components (misalnya src/components/CommentSection.tsx) dan salin kode berikut.

Catatan: Kode di bawah ini menggunakan localStorage untuk menahan teks sementara selama proses autentikasi berlangsung.

"use client";

import { useState, useEffect } from "react";
import { useSession, signIn } from "next-auth/react";

type CommentType = {
  id: number;
  initial: string;
  avatarUrl?: string;
  bgColor: string;
  name: string;
  time: string;
  text: string;
  likes: number;
  dislikes: number;
};

export default function CommentSection() {
  const { data: session, status } = useSession();
  const isLoggedIn = status === "authenticated";

  const [commentText, setCommentText] = useState("");
  const [comments, setComments] = useState<CommentType[]>([]);
  const [activeTab, setActiveTab] = useState("Terbaru");

  const maxChars = 1000;
  const tabs = ["Terbaru", "Terpilih", "Terpopuler", "Teramai", "Komentar yang disematkan"];

  // ==========================================
  // EFEK SETELAH LOGIN SUKSES (Mengecek Draf)
  // ==========================================
  useEffect(() => {
    if (isLoggedIn && session?.user) {
      const pendingComment = localStorage.getItem("pending_comment");
      
      if (pendingComment) {
        const googleName = session.user.name || "Akun Pengunjung";
        const words = googleName.trim().split(" ");
        const init = words.length === 1 
            ? words[0].substring(0, 2).toUpperCase()
            : (words[0][0] + words[1][0]).toUpperCase();

        const autoComment: CommentType = {
          id: Date.now(),
          initial: init,
          avatarUrl: session.user.image || undefined,
          bgColor: "#1e3a8a",
          name: googleName,
          time: "Baru saja",
          text: pendingComment,
          likes: 0,
          dislikes: 0,
        };

        setComments((prev) => [autoComment, ...prev]);
        setCommentText("");
        localStorage.removeItem("pending_comment");
      }
    }
  }, [isLoggedIn, session]);

  // ==========================================
  // FUNGSI TOMBOL KIRIM
  // ==========================================
  const handleKirim = () => {
    if (commentText.trim() === "") return;

    // SKENARIO 1: JIKA BELUM LOGIN
    if (!isLoggedIn) {
      const confirmLogin = window.confirm(
        "Anda tidak bisa komentar sebelum login terlebih dahulu.\n\nKlik OK untuk lanjut login."
      );
      
      if (confirmLogin) {
        localStorage.setItem("pending_comment", commentText);
        signIn("google");
      }
      return;
    }

    // SKENARIO 2: JIKA SUDAH LOGIN
    const googleName = session?.user?.name || "Akun Pengunjung";
    const words = googleName.trim().split(" ");
    const init = words.length === 1 
        ? words[0].substring(0, 2).toUpperCase()
        : (words[0][0] + words[1][0]).toUpperCase();

    const newComment: CommentType = {
      id: Date.now(),
      initial: init,
      avatarUrl: session?.user?.image || undefined,
      bgColor: "#1e3a8a",
      name: googleName,
      time: "Baru saja",
      text: commentText,
      likes: 0,
      dislikes: 0,
    };

    setComments([newComment, ...comments]);
    setCommentText("");
  };

  return (
    <div className="w-full mt-10 font-sans">
      <h3 className="text-xl font-bold text-foreground mb-4">
        Komentar <span className="font-normal">({comments.length})</span>
      </h3>

      <div className="bg-slate-50 dark:bg-slate-900 p-4 rounded-2xl mb-6">
        <div className="bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl p-3 shadow-sm">
          <textarea
            value={commentText}
            onChange={(e) => setCommentText(e.target.value)}
            maxLength={maxChars}
            placeholder={isLoggedIn ? `Berkomentar sebagai ${session?.user?.name}...` : "Tulis Komentar..."}
            className="w-full resize-none outline-none bg-transparent text-sm text-foreground placeholder:text-slate-400 min-h-[60px]"
          />
          
          <div className="flex justify-between items-center mt-2 border-t border-slate-100 dark:border-slate-800 pt-3">
            <span className="text-xs text-slate-500 font-medium">
              <strong className="text-foreground font-semibold">{maxChars - commentText.length}</strong> Karakter tersisa
            </span>
            <button 
              onClick={handleKirim}
              disabled={commentText.trim() === ""}
              className={`flex items-center gap-2 px-5 py-1.5 rounded-full text-sm font-medium transition-colors ${
                commentText.trim() === "" 
                  ? "bg-slate-300 text-slate-500 cursor-not-allowed" 
                  : "bg-[#1e3a8a] hover:bg-[#152c6e] text-white"
              }`}
            >
              Kirim
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M22 2 11 13" />
                <path d="M22 2 15 22 11 13 2 9 22 2z" />
              </svg>
            </button>
          </div>
        </div>
      </div>

      <div className="flex overflow-x-auto gap-2.5 mb-6 pb-2 scrollbar-hide">
        {tabs.map((tab) => (
          <button
            key={tab}
            onClick={() => setActiveTab(tab)}
            className={`whitespace-nowrap px-4 py-1.5 rounded-full text-sm font-medium border transition-colors ${
              activeTab === tab
                ? "bg-[#1e3a8a] border-[#1e3a8a] text-white"
                : "bg-transparent border-slate-300 dark:border-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800"
            }`}
          >
            {tab}
          </button>
        ))}
      </div>

      <div className="flex flex-col">
        {comments.length === 0 ? (
          <div className="text-center py-8 text-slate-500 text-sm">
            Belum ada komentar. Jadilah yang pertama memberikan tanggapan!
          </div>
        ) : (
          comments.map((comment) => (
            <div key={comment.id} className="flex gap-4 py-5 border-b border-slate-100 dark:border-slate-800">
              <div 
                className="w-12 h-12 rounded-full flex items-center justify-center text-white font-bold text-lg flex-shrink-0 overflow-hidden"
                style={{ backgroundColor: comment.bgColor }}
              >
                {comment.avatarUrl ? (
                  <img src={comment.avatarUrl} alt={comment.name} className="w-full h-full object-cover" />
                ) : (
                  comment.initial
                )}
              </div>

              <div className="flex-1 min-w-0">
                <div className="flex justify-between items-start mb-1">
                  <div>
                    <h4 className="font-bold text-foreground text-sm inline-block mr-2">{comment.name}</h4>
                    <span className="text-xs text-slate-500">{comment.time}</span>
                  </div>
                </div>
                <p className="text-sm text-foreground mt-1.5 leading-relaxed whitespace-pre-wrap">
                  {comment.text}
                </p>
              </div>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

Baca Juga: Cara Otomatisasi IndexNow Bing di Next.js dan Cloudflare Pages CMS

Penjelasan Logika Sistem:

  1. Validasi Input: Tombol "Kirim" dilengkapi properti disabled yang aktif jika input kosong. Ini mencegah pengunjung mengirim spam atau komentar kosong secara tidak sengaja.
  2. Skenario Guest (Belum Login): Saat tombol kirim ditekan oleh guest, sistem memunculkan pop-up peringatan wajib login. Jika pengunjung menekan OK, teks komentar mereka disimpan sementara di memori browser (localStorage.setItem). Sistem kemudian mengeksekusi signIn("google").
  3. Reklamasi Draf Otomatis (useEffect): Ketika pengunjung kembali ke artikel setelah berhasil login, fungsi useEffect akan mendeteksi status isLoggedIn. Sistem kemudian memeriksa memori lokal. Jika ada draf yang tertinggal, sistem membungkusnya dengan data Nama dan Foto Profil asli akun Google mereka, memunculkannya ke layar, dan segera menghapus draf tersebut agar tidak terkirim dua kali.

Langkah 2: Memasang Komponen di Halaman Artikel

Setelah komponen selesai dibuat, langkah terakhir adalah memanggilnya ke dalam template halaman artikel Anda. Di Next.js App Router, file ini biasanya terletak di rute dinamis app/post/[slug]/page.tsx.

  1. Impor Komponen: Tambahkan baris pemanggil ini di deretan import pada bagian atas file Anda.
import CommentSection from "@/components/CommentSection"
  1. Letakkan di Bawah Artikel: Gulir ke bagian paling bawah dari struktur kode konten Anda, temukan batas akhir dari komponen artikel (misalnya di bawah Artikel Terkait), lalu sisipkan kode <CommentSection /> beserta garis pemisah horizontal agar terlihat rapi.
{/* ===================================================
    GARIS PEMISAH & KOLOM KOMENTAR
    =================================================== */}
<div className="px-4 md:px-0">
  <hr className="my-10 border-t border-border opacity-60" />
  <CommentSection />
</div>
{/* =================================================== */}

Kesimpulan

Dengan menerapkan strategi ini, Anda tidak hanya mendapatkan desain kolom komentar yang bersih layaknya portal media nasional premium, tetapi juga berhasil menyelamatkan interaksi pengguna yang berharga. Pengunjung tidak perlu frustrasi mengetik ulang pemikiran mereka dari nol akibat proses autentikasi. Hal ini secara langsung akan mendorong metrik keterlibatan (engagement rate) serta durasi sesi pada website Anda meningkat secara signifikan.

Artikel Terkait