Cara Membuat Ikon Share, Like, dan Simpan Premium di Next.js (Persisten & Ringan)

PE
Pena Edukasi
Cara Membuat Ikon Share, Like, dan Simpan Premium di Next.js (Persisten & Ringan)

Meningkatkan interaksi (engagement) pengunjung adalah kunci kesuksesan sebuah artikel blog atau portal berita. Salah satu cara paling efektif adalah dengan menyediakan tombol aksi yang intuitif, seperti Suka (Like), Simpan (Bookmark), dan Bagikan (Share). Namun, tantangan terbesar bagi developer adalah bagaimana membuat fitur ini terlihat premium, berjalan sangat ringan tanpa membebani loading halaman, dan yang terpenting: datanya tidak hilang saat halaman di-refresh.

Dalam panduan ini, kita akan membangun komponen ArticleActions menggunakan Next.js dan Tailwind CSS. Kita akan memanfaatkan Web Share API bawaan browser untuk fungsi berbagi yang 100% native, serta mengintegrasikan sistem database (menggunakan Supabase/Firebase sebagai contoh logika) agar jumlah Like dan status Simpan tersimpan secara persisten (permanen).

Mengapa Pendekatan Ini Lebih Baik?

  • Nol Dependensi Berat: Kita tidak menggunakan library ikon pihak ketiga yang besar seperti FontAwesome atau Material Icons. Kita menggunakan SVG inline murni yang besarnya hanya beberapa byte.
  • Native Share: Fungsi share langsung memanggil menu bawaan sistem operasi (Android/iOS) sehingga terasa natural bagi pengguna smartphone.
  • Persisten: Menggunakan kombinasi React State dan panggilan API backend agar interaksi pengguna terekam di database, bukan sekadar manipulasi visual sesaat.

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

Langkah 1: Persiapan Database & API Route (Backend)

Agar data "Suka" dan "Simpan" tidak hilang saat di-refresh, kita membutuhkan database. Anda bisa menggunakan Supabase, Firebase, atau database pilihan Anda. Konsep dasarnya adalah membuat API Route di Next.js untuk menerima dan mengirim data interaksi.

Buat file API (misal: app/api/article-actions/route.ts). Ini adalah pseudo-code (kode konsep) yang menunjukkan bagaimana API Anda harus merespons:

// app/api/article-actions/route.ts
import { NextResponse } from "next/server";
// Import koneksi database Anda di sini (misal: db)

export async function POST(request: Request) {
  const { slug, action, userId } = await request.json();

  try {
    if (action === "LIKE") {
      // Logika Database: Tambahkan 1 ke kolom 'likes' untuk artikel dengan 'slug'
      // db.articles.update(...)
    } else if (action === "SAVE") {
      // Logika Database: Tambahkan artikel ini ke daftar 'saved' milik userId
      // db.users.update(...)
    }

    return NextResponse.json({ success: true, message: "Aksi berhasil direkam" });
  } catch (error) {
    return NextResponse.json({ success: false, message: "Terjadi kesalahan" }, { status: 500 });
  }
}

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const slug = searchParams.get("slug");
  
  // Logika Database: Ambil total likes untuk artikel ini
  // const likesCount = await db.articles.getLikes(slug);
  
  // Contoh kembalian
  return NextResponse.json({ likes: 150 });
}

Langkah 2: Membuat Komponen ArticleActions.tsx

Sekarang, kita buat komponen antarmuka pengguna (UI). Komponen ini akan menampilkan tombol-tombol tersebut dan mengirimkan request ke API yang telah kita buat di Langkah 1.

Buat file baru bernama ArticleActions.tsx di dalam folder components Anda:

"use client";

import { useState, useEffect } from "react";

// Props untuk menerima slug artikel agar API tahu artikel mana yang diinteraksi
interface ArticleActionsProps {
  articleSlug: string;
}

export default function ArticleActions({ articleSlug }: ArticleActionsProps) {
  const [isLiked, setIsLiked] = useState(false);
  const [likesCount, setLikesCount] = useState(0); // Menyimpan jumlah like
  const [isSaved, setIsSaved] = useState(false);
  const [isLoading, setIsLoading] = useState(true);

  // ==================================================
  // 1. Tarik Data Awal Saat Komponen Dimuat (Persistensi)
  // ==================================================
  useEffect(() => {
    const fetchInitialData = async () => {
      try {
        // Mengambil jumlah like dari database berdasarkan slug
        const res = await fetch(`/api/article-actions?slug=${articleSlug}`);
        if (res.ok) {
          const data = await res.json();
          setLikesCount(data.likes || 0);
          
          // Jika Anda punya sistem login (NextAuth), 
          // cek juga apakah user saat ini sudah me-like/save artikel ini
          // setIsLiked(data.userHasLiked);
          // setIsSaved(data.userHasSaved);
        }
      } catch (error) {
        console.error("Gagal mengambil data", error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchInitialData();
  }, [articleSlug]);

  // ==================================================
  // 2. Fungsi Menangani "Suka"
  // ==================================================
  const handleLike = async () => {
    // Animasi optimistik: ubah UI langsung sebelum menunggu database
    setIsLiked(!isLiked);
    setLikesCount((prev) => isLiked ? prev - 1 : prev + 1);

    try {
      // Kirim data ke database secara diam-diam (background)
      await fetch("/api/article-actions", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ slug: articleSlug, action: isLiked ? "UNLIKE" : "LIKE" }),
      });
    } catch (error) {
      // Rollback jika gagal
      console.error("Gagal menyimpan like", error);
      setIsLiked(!isLiked);
      setLikesCount((prev) => isLiked ? prev + 1 : prev - 1);
    }
  };

  // ==================================================
  // 3. Fungsi Menangani "Simpan"
  // ==================================================
  const handleSave = async () => {
    setIsSaved(!isSaved);
    try {
      await fetch("/api/article-actions", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ slug: articleSlug, action: isSaved ? "UNSAVE" : "SAVE" }),
      });
    } catch (error) {
      console.error("Gagal menyimpan artikel", error);
      setIsSaved(!isSaved);
    }
  };

  // ==================================================
  // 4. Fungsi Menangani "Share" (Web Share API)
  // ==================================================
  const handleShare = async () => {
    if (navigator.share) {
      try {
        await navigator.share({
          title: document.title,
          url: window.location.href,
        });
      } catch (error) {
        console.log("Membatalkan share atau tidak didukung", error);
      }
    } else {
      // Fallback untuk browser laptop lama (Copy Link)
      navigator.clipboard.writeText(window.location.href);
      alert("Tautan artikel berhasil disalin!");
    }
  };

  // Jangan tampilkan tombol sebelum data awal (jumlah like) selesai ditarik
  if (isLoading) return <div className="animate-pulse h-8 w-48 bg-slate-200 rounded mt-12 mb-4"></div>;

  return (
    <div style={{ display: "flex", alignItems: "center", gap: "32px", marginTop: "48px", marginBottom: "16px" }}>
      
      {/* Tombol Suka */}
      <button 
        onClick={handleLike} 
        style={{ display: "flex", alignItems: "center", gap: "8px", background: "none", border: "none", cursor: "pointer", color: isLiked ? "#ef4444" : "currentColor", opacity: isLiked ? 1 : 0.6, padding: 0, fontFamily: "'Plus Jakarta Sans', sans-serif", transition: "all 0.2s ease" }}
      >
        <svg viewBox="0 0 24 24" fill={isLiked ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width="20" height="20">
          <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
        </svg>
        <span style={{ fontSize: "14px", fontWeight: 500 }}>
          {isLiked ? "Disukai" : "Suka"} <span className="opacity-70 ml-1">({likesCount})</span>
        </span>
      </button>

      {/* Tombol Simpan */}
      <button 
        onClick={handleSave} 
        style={{ display: "flex", alignItems: "center", gap: "8px", background: "none", border: "none", cursor: "pointer", color: isSaved ? "#3b82f6" : "currentColor", opacity: isSaved ? 1 : 0.6, padding: 0, fontFamily: "'Plus Jakarta Sans', sans-serif", transition: "all 0.2s ease" }}
      >
        <svg viewBox="0 0 24 24" fill={isSaved ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width="20" height="20">
          <path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"></path>
        </svg>
        <span style={{ fontSize: "14px", fontWeight: 500 }}>{isSaved ? "Tersimpan" : "Simpan"}</span>
      </button>

      {/* Tombol Bagikan */}
      <button 
        onClick={handleShare} 
        style={{ display: "flex", alignItems: "center", gap: "8px", background: "none", border: "none", cursor: "pointer", color: "currentColor", opacity: 0.6, padding: 0, fontFamily: "'Plus Jakarta Sans', sans-serif", transition: "all 0.2s ease" }}
      >
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width="20" height="20">
          <circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle>
          <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
          <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
        </svg>
        <span style={{ fontSize: "14px", fontWeight: 500 }}>Bagikan</span>
      </button>

    </div>
  );
}

Penjelasan Logika UI (Optimistic UI):

  1. Optimistic UI: Pada fungsi handleLike, kita menggunakan pendekatan Optimistic UI. Artinya, segera setelah tombol ditekan, kita langsung mengubah warna ikon menjadi merah (setIsLiked(!isLiked)) dan menambah angkanya secara visual tanpa menunggu balasan dari database. Ini membuat website terasa secepat kilat (Nol-Latensi) bagi pengunjung. Jika ternyata database gagal menyimpan (karena internet putus), barulah kita membatalkan animasi tersebut di blok catch.
  2. Web Share API: Fungsi handleShare memanfaatkan navigator.share. Ini sangat revolusioner karena Anda tidak perlu memasang ikon Facebook, Twitter, dan WhatsApp berjejeran yang memakan tempat. Satu tombol ini akan membuka menu bagi-pakai bawaan HP pengunjung.
  3. SVG Inline: Semua ikon berupa kode <svg> murni. Tidak ada tambahan beban jaringan (network request) untuk memuat font ikon. Kecepatan loading halaman artikel Anda tetap 100%.

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

Langkah 3: Memasang di Halaman Artikel

Setelah komponen selesai, Anda harus memasangkannya di halaman artikel utama Anda (misalnya app/post/[slug]/page.tsx). Pastikan Anda mengirimkan parameter slug artikel agar komponen tahu data artikel mana yang harus dihitung.

import ArticleActions from "@/components/ArticleActions"

export default async function PostPage({ params }) {
  const { slug } = params;
  
  // Mengambil konten artikel dari CMS/Database Anda
  const article = await getArticleBySlug(slug);

  return (
    <article>
      {/* Konten Utama Artikel Anda */}
      <div dangerouslySetInnerHTML={{ __html: article.content }} />

      {/* Area Tags ... */}

      {/* Tombol Aksi - Lempar prop slug ke komponen */}
      <div className="px-4 md:px-0">
        <ArticleActions articleSlug={slug} />
      </div>
      
    </article>
  )
}

Kesimpulan

Membangun fitur interaksi premium tidak selalu berarti harus mengorbankan kecepatan website. Dengan memadukan Native Web Share API, SVG Inline, dan konsep Optimistic UI (React State) yang terhubung ke backend, Anda telah berhasil menciptakan deretan tombol aksi (Like, Save, Share) yang tidak hanya memanjakan mata, tetapi juga mencatat data secara permanen tanpa menghilangkan predikat "Cepat" dari portal artikel Anda.

HM

Penulis: Hayatul Makki

Tim Jurnalis Pena Edukasi

Artikel Terkait