Langkah – langkah Instal Nextjs dan Supabase Siap Pakai Untuk Pemula

17 Desember 2025
Bagikan:
Langkah – langkah Instal Nextjs dan Supabase Siap Pakai Untuk Pemula

Dalam kesempatan ini saya akan menjelaskan tentang bagaimana membangun web modern menggunakan NextJS dan Supabase. Dalam studi kasus kali ini mungkin saya akan menginstall  beberapa library/package yang mungkin akan dibutuhkan didalam project dari mulai plugins/module atau component shadcn tertentu.

Baca juga : Membuat Function Handle New USer Supabase 

Berikut ini adalah teknologi utama yang digunakan dalam proyek ini:

  • Framework: Next.js 16 dengan App Router
    Ini adalah framework React yang modern untuk membangun aplikasi web yang cepat dan SEO-friendly.
  • Bahasa: TypeScript.
    Menambahkan keamanan tipe statis pada JavaScript untuk mengurangi bug dan meningkatkan pengalaman pengembangan.
  • Styling: Tailwind CSS 4
    Sebuah framework CSS utility-first untuk membangun desain kustom dengan cepat tanpa meninggalkan HTML Kamu.
  • Komponen UI: Shadcn/UI.
    Ini bukan library komponen biasa, melainkan kumpulan komponen yang dapat digunakan kembali yang dibangun menggunakan Radix UI dan Tailwind CSS. Kamu dapat menyalin dan menempelkannya ke dalam proyek Kamu dan memodifikasinya sesuai kebutuhan.
  • Backend & Database: Supabase. 
    proyek ini menggunakan Supabase sebagai backend, yang menyediakan database Postgres, otentikasi, penyimpanan, dan banyak lagi.
  • Manajemen Form: React Hook Form & Zod
    Kombinasi ini digunakan untuk mengelola state form dan melakukan validasi skema data dengan cara yang efisien dan aman.
  • Internasionalisasi (i18n): next-intl
    Library ini digunakan untuk mengelola terjemahan teks (dalam kasus ini, ke Bahasa Indonesia) di dalam aplikasi Next.js.
  • Notifikasi: Sonner. 
    Untuk menampilkan notifikasi toast yang simpel dan elegan.
  • Ikon: Lucide React
    Sebuah library ikon yang ringan dan mudah dikustomisasi.
  • Utilitas Tanggal & Waktu: date-fns
    Library modern untuk manipulasi tanggal dan waktu.
  • Eksport Data: ExcelJS. 
    Digunakan untuk membuat file Excel, seperti pada fitur ekspor data pesanan.

Langkah – langkah Instal NextJS dan Supabase Siap Pakai Untuk Pemula

1. Instalasi NextJS
npx create-next-app@latest nama-proyek-kamu --typescript --tailwind --eslint

Dalam projek nextjs ini saya menggunakan struktur folder src.

Selanjutnya masuk ke direktori proyek kamu:

cd nama-proyek-kamu
2. Instalasi Dependensi Utama

Instal semua library utama yang dibutuhkan oleh proyek.

npm install @hookform/resolvers @supabase/ssr @supabase/supabase-js class-variance-authority clsx date-fns exceljs jsonwebtoken lucide-react next-intl next-themes react-day-picker react-hook-form react-markdown remark-gfm sonner tailwind-merge zod

3. Instalasi Dependensi Pengembangan (Dev Dependencies)

Instal dependensi yang hanya dibutuhkan saat pengembangan.

npm install -D @types/jsonwebtoken @types/node @types/react @types/react-dom typescript
4. Konfigurasi Shadcn/UI

Shadcn/UI memiliki CLI sendiri untuk proses inisialisasi dan penambahan komponen.
Inisialisasi Shadcn/UI: Jalankan perintah berikut di terminal Anda:

npx shadcn@latest init

5. Tambahkan Komponen Shadcn/UI yang Digunakan

Berdasarkan analisis file kode, berikut adalah komponen-komponen dari shadcn/ui yang perlu Anda tambahkan:
Bash


npx shadcn@latest add alert
npx shadcn@latest add avatar
npx shadcn@latest add badge
npx shadcn@latest add breadcrumb
npx shadcn@latest add button
npx shadcn@latest add calendar
npx shadcn@latest add card
npx shadcn@latest add checkbox
npx shadcn@latest add collapsible
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu
npx shadcn@latest add form
npx shadcn@latest add input
npx shadcn@latest add label
npx shadcn@latest add popover
npx shadcn@latest add select
npx shadcn@latest add switch
npx shadcn@latest add table


Catatan: Perintah npx shadcn@latest add form akan secara otomatis menginstal react-hook-form dan @hookform/resolvers jika belum ada.

6. Konfigurasi lain – lain

Ada beberapa tambahan atau penyesuain khususnya untuk translation, eslint dan prettier

npm install next-intl

Supaya eslint dan prettier tidak bentrok, kita perlu lakukan konfigurasi tambahan

npm install --save-dev prettier prettier-plugin-tailwindcss
npm install --save-dev eslint prettier eslint-config-prettier eslint-plugin-prettier

Tambahkan konfigurasi tambahan untuk prettier

# .prettierignore
# Folder dependensi
node_modules
# Folder build Next.js
.next
# File environment (jangan diformat)
.env.local
# File cache dan log lainnya
*.log

Kemudian tambahkan konfigurasi untuk next.config.ts

import createNextIntlPlugin from "next-intl/plugin";
import type { NextConfig } from "next";
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {
 experimental: {
   serverActions: {
     allowedOrigins: [
       "localhost:3000",
       "*.devtunnels.ms",
       "*.asse.devtunnels.ms",
     ],
   },
 },
 images: {
   remotePatterns: [
     {
       protocol: "https",
       hostname: "yoursupabasepublichostname.supabase.co",
       port: "",
       pathname: "/storage/v1/object/public/**",
     },
   ],
 },
 async rewrites() {
   return [
     // {
     //   source: "/ingest/static/:path*",
     //   destination: "https://us-assets.i.posthog.com/static/:path*",
     // },
     // {
     //   source: "/ingest/:path*",
     //   destination: "https://us.i.posthog.com/:path*",
     // },
   ];
 },
 // This is required to support PostHog trailing slash API requests
 skipTrailingSlashRedirect: true,
};
export default withNextIntl(nextConfig);

7. Integrasi dengan Editor Kode (Contoh: VS Code)

Langkah yang paling penting adalah mengintegrasikannya dengan editor Anda agar pemformatan berjalan otomatis.
Jika Anda menggunakan Visual Studio Code:

  1. Instal ekstensi Prettier - Code formatter.
  2. Buka Pengaturan VS Code (File > Preferences > Settings, atau Ctrl + ,).
  3. Cari "Format On Save" dan pastikan opsinya dicentang. Ini akan membuat VS Code otomatis memformat file setiap kali Anda menyimpannya.
  4. (Opsional) Anda bisa mengatur Prettier sebagai pemformat default untuk file JavaScript/TypeScript agar tidak konflik dengan pemformat lain.
8. Konfigurasi .env.local

Selanjutnya masukan api key untuk koneksi supabase masing – masing

NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY =

9. Konfigurasi untuk kebutuhan Api

Untuk kebutuhan api dan berkomunikasi dengan supabase, kamu perlu menambahkan konfigurasi untuk kebutuhan koneksi yang digunakan dalam operasi client atau server.

Dalam folder src terdapat direktori lib, nah didalam direktori lib, kamu buat direktori supabase, lalu didalamnya buat empat file yakni, admin.ts, client.ts, server.ts dan middleware.ts

//admin.ts untuk operasi yang membutuhkan service role key untuk bypass, contoh untuk kasus penambahan user ke supabase
import { createClient } from "@supabase/supabase-js";
export const createAdminClient = () => {
 return createClient(
   process.env.NEXT_PUBLIC_SUPABASE_URL!,
   process.env.SUPABASE_SERVICE_ROLE_KEY!
   //process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
 );
};
//client.ts
import { createBrowserClient } from "@supabase/ssr";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
export const createClient = () => createBrowserClient(supabaseUrl!, supabaseKey!);
 
 
//middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function updateSession(request: NextRequest) {
 let supabaseResponse = NextResponse.next({
   request,
 });
 const supabase = createServerClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, {
   cookies: {
     getAll() {
       return request.cookies.getAll();
     },
     setAll(cookiesToSet) {
       cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
       supabaseResponse = NextResponse.next({
         request,
       });
       cookiesToSet.forEach(({ name, value, options }) => supabaseResponse.cookies.set(name, value, options));
     },
   },
 });
 const {
   data: { user },
 } = await supabase.auth.getUser();
 // Definisikan rute dan peran yang diizinkan
 const adminOnlyRoutes = ["/users"]; // Contoh rute khusus admin
 const protectedRoutes = ["/dashboard", "/profile", ...adminOnlyRoutes];
 const authRoutes = ["/sign-in", "/forgot-password"];
 const isProtectedRoute = protectedRoutes.some((route) => request.nextUrl.pathname.startsWith(route));
 const isAuthRoute = authRoutes.some((route) => request.nextUrl.pathname.startsWith(route));
 const isAdminOnlyRoute = adminOnlyRoutes.some((route) => request.nextUrl.pathname.startsWith(route));
 // 1. Jika pengguna tidak login dan mencoba mengakses rute yang dilindungi
 if (isProtectedRoute && !user) {
   const redirectUrl = new URL("/sign-in", request.url);
   redirectUrl.searchParams.set("redirectTo", request.nextUrl.pathname);
   return NextResponse.redirect(redirectUrl);
 }
 // 2. Jika pengguna sudah login, periksa perannya untuk rute khusus admin
 if (user && isAdminOnlyRoute) {
   const userRole = user.app_metadata?.role;
   if (userRole !== 'admin') {
     // Alihkan ke halaman dashboard utama jika bukan admin
     return NextResponse.redirect(new URL("/dashboard", request.url));
   }
 }
 // 3. Jika pengguna sudah login dan mencoba mengakses halaman login/register
 if (isAuthRoute && user) {
   const redirectTo = request.nextUrl.searchParams.get("redirectTo");
   if (redirectTo) {
     return NextResponse.redirect(new URL(redirectTo, request.url));
   }
   return NextResponse.redirect(new URL("/", request.url));
 }
 return supabaseResponse;
}

 

//server.ts
//untuk operasi api dari sisi admin
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export async function createClient() {
 const cookieStore = await cookies();
 return createServerClient(supabaseUrl, supabaseKey, {
   cookies: {
     getAll() {
       return cookieStore.getAll();
     },
     setAll(cookiesToSet) {
       try {
         cookiesToSet.forEach(({ name, value, options }) => {
           cookieStore.set(name, value, options);
         });
       } catch {
         /* ignore in server components */
       }
     },
   },
 });
}


Selanjutnya didalam folder lib buat juga folder api kemudian didalamnya masukan file dengan nama response.ts untuk respon api ketika sukses atau gagal

import { NextResponse } from "next/server";
export interface ApiSuccessResponse<T = any> {
 success: true;
 data: T;
 message: string;
 meta?: {
   page?: number;
   limit?: number;
   total?: number;
   totalPages?: number;
 };
}
export interface ApiErrorResponse {
 success: false;
 error: {
   message: string;
   details?: any;
 };
}
export type ApiResponse<T = any> = ApiSuccessResponse<T> | ApiErrorResponse;
export function successResponse<T = any>(data: T, message: string = "OK", status: number = 200, meta?: ApiSuccessResponse["meta"]) {
 const response: ApiSuccessResponse<T> = {
   success: true,
   data,
   message,
   ...(meta && { meta }),
 };
 return NextResponse.json(response, { status });
}
export function errorResponse(message: string, status: number = 500, details?: any, error?: any) {
 if (error) {
   console.error(`[API Error - ${status}]`, message, error);
 }
 const response: ApiErrorResponse = {
   success: false,
   error: {
     message,
     ...(details && { details }),
   },
 };
 
 return NextResponse.json(response, { status });
}
export function badRequestResponse(message: string = "The request could not be understood or was missing required parameters", details?: any) {
 return errorResponse(message, 400, details);
}

export function unauthorizedResponse(message: string = "Authentication is required to access this resource", details?: any) {
 return errorResponse(message, 401, details);
}
export function forbiddenResponse(message: string = "You do not have permission to access this resource", details?: any) {
 return errorResponse(message, 403, details);
}
export function notFoundResponse(message: string = "The requested resource was not found", details?: any) {
 return errorResponse(message, 404, details);
}
export function conflictResponse(message: string = "The request could not be completed due to a conflict with the current state", details?: any) {
 return errorResponse(message, 409, details);
}
export function validationErrorResponse(message: string = "The provided data failed validation", details?: any) {
 return errorResponse(message, 422, details);
}
export function serverErrorResponse(message: string = "An unexpected error occurred while processing your request", error?: any) {
 return errorResponse(message, 500, undefined, error);
}
export function createdResponse<T = any>(data: T, message?: string) {
 return successResponse(data, message || "Resource created successfully", 201);
}
export function noContentResponse() {
 return new NextResponse(null, { status: 204 });
}
export function paginatedResponse<T = any>(data: T[], page: number, limit: number, total: number, message?: string) {
 return successResponse(data, message, 200, {
   page,
   limit,
   total,
   totalPages: Math.ceil(total / limit),
 });
}
10. Konfigurasi Translation

Sebenarnya ini optional, tapi ideal jika ada, untuk konfigurasinya, pertama buat folder i18n didalam folder src lalu didalam folder tersebut buat file en.json dan id.json

{
 "HomePage": {
   "title": "Hello world!"
 }
}

Kemudian buat file request.ts

import { getRequestConfig } from "next-intl/server";
import { cookies, headers } from "next/headers";
const SUPPORTED = ["id", "en"] as const;
type Locale = (typeof SUPPORTED)[number];
const DEFAULT_LOCALE: Locale = "id";
function pickFromAcceptLanguage(accept: string | null): Locale {
   if (!accept) return DEFAULT_LOCALE;
   const langs = accept.split(",").map((s) => s.split(";")[0].trim().toLowerCase());
   for (const l of langs) {
       const base = l.split("-")[0];
       if (SUPPORTED.includes(base as Locale)) return base as Locale;
   }
   return DEFAULT_LOCALE;
}
export default getRequestConfig(async () => {
   const cookieStore = await cookies();
   const headerStore = await headers();
   const cookieLocale = cookieStore.get("NEXT_LOCALE")?.value as Locale | undefined;
   const headerLocale = pickFromAcceptLanguage(headerStore.get("accept-language"));
   const locale: Locale = cookieLocale && SUPPORTED.includes(cookieLocale) ? cookieLocale : headerLocale;
   return {
       locale,
       messages: (await import(`./${locale}.json`)).default,
   };
});
{

 

11. Contexts

Selanjutnya kita membutuhkan sistem autentifikasi untuk melakukan identifikasi user atau untuk membatasi user ke halaman tertentu.

Pada folder src , buat folder baru bernama contexts. Kemudian buat dua buah file didalamnya yakni auth-context.tsx dan auth-provider.tsx

//auth context
"use client";
import { createContext } from "react";
import type { User } from "@supabase/supabase-js";
export type AuthContextType = {
 user: User | null;
 loading: boolean;
 role: string | null;
 signIn: (email: string, password: string) => Promise<{ error: string | null }>;
 signOut: () => Promise<void>;
};
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
//auth-provider.tsx
"use client";
import { useEffect, useState, ReactNode } from "react";
import { AuthContext } from "./auth-context";
import { createClient } from "@/lib/supabase/client";
import type { User } from "@supabase/supabase-js";
export function AuthProvider({ children }: { children: ReactNode }) {
 const supabase = createClient();
 const [user, setUser] = useState<User | null>(null);
 const [loading, setLoading] = useState(true);
 const [role, setRole] = useState<string | null>(null);
 useEffect(() => {
   // Listener untuk perubahan status autentikasi (login, logout)
   const {
     data: { subscription },
   } = supabase.auth.onAuthStateChange(async (_event, session) => {
     const currentUser = session?.user ?? null;
     setUser(currentUser);
     // Ambil peran langsung dari JWT (app_metadata)
     // Ini cepat karena sudah ada di dalam sesi
     if (currentUser && currentUser.app_metadata) {
       setRole(currentUser.app_metadata.role || null);
     } else {
       setRole(null); // Reset peran saat logout
     }
     // Jika ada pengguna, kita juga perbarui data user_metadata
     // Ini penting agar data seperti nama depan/belakang selalu terbaru
     if (currentUser) {
       // Memanggil getUser() akan memaksa refresh data dari server
       await supabase.auth.getUser();
     }
     setLoading(false);
   });
   // Cek sesi awal saat komponen dimuat
   const checkInitialSession = async () => {
     const {
       data: { session },
     } = await supabase.auth.getSession();
     // Jika tidak ada sesi, kita bisa berhenti loading
     if (!session) {
       setLoading(false);
     }
     // Jika ada sesi, onAuthStateChange akan otomatis terpanggil dan menangani sisanya.
   };
   checkInitialSession();
   return () => {
     subscription.unsubscribe();
   };
 }, [supabase]);
 const signOut = async () => {
   await supabase.auth.signOut();
   // onAuthStateChange akan menangani reset state user dan profile
   // Redirect bisa dilakukan di komponen jika perlu, atau di sini
   window.location.href = "/sign-in";
 };
 const signIn = async (email: string, password: string) => {
   const { error } = await supabase.auth.signInWithPassword({ email, password });
   // onAuthStateChange akan otomatis menangani pengambilan profil setelah login berhasil
   return { error: error?.message || null };
 };
 const value = {
   user,
   loading,
   role, // Sediakan role langsung
   signIn,
   signOut,
 };
 return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

Selanjutnya pada folder src buat kembali sebuah folder bernama hooks. Kemudian didalamnya buat sebuah file bernama use-auth.ts

"use client";
import { useContext } from "react";
import { AuthContext } from "@/contexts/auth-context";
export function useAuth() {
 const context = useContext(AuthContext);
 if (!context) {
   throw new Error("useAuth must be used within an AuthProvider");
 }
 return context;
}

 

12. Layout

Pada folder app, buat file layout.tsx, providers.tsx dan not-found.tsx. Pada folder app, di root direktori tidak perlu page.tsx, nanti page.tsx akan kita arahkan ke folder (public)

//layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { getLocale, getMessages } from "next-intl/server";
import { Providers } from "./providers";
import "./globals.css";
const geistSans = Geist({
 variable: "--font-geist-sans",
 subsets: ["latin"],
});
const geistMono = Geist_Mono({
 variable: "--font-geist-mono",
 subsets: ["latin"],
});
export const metadata: Metadata = {
 title: "Sakola - LMS",
 description: "LMS nomor 1 Indonesia",
};
export default async function RootLayout({
 children,
}: {
 children: React.ReactNode;
}) {
 const locale = await getLocale();
 const messages = await getMessages();
 return (
   <html lang={locale}>
     <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
       <Providers locale={locale} messages={messages}>
         {children}
       </Providers>
     </body>
   </html>
 );
}

 

//providers.tsx
"use client";
import { NextIntlClientProvider } from "next-intl";
import { AuthProvider } from "@/contexts/auth-provider";
import { Toaster } from "sonner"; // atau react-hot-toast
export function Providers({
 children,
 locale,
 messages,
}: {
 children: React.ReactNode;
 locale: string;
 messages: any;
}) {
 return (
   <NextIntlClientProvider locale={locale} messages={messages}>
     <AuthProvider>
       {children}
       <Toaster />
     </AuthProvider>
   </NextIntlClientProvider>
 );
}
//not-found.tsx
import Image from "next/image";
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function NotFound() {
 return (
   <div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 px-6 py-12 text-center">
     <div className="max-w-md">
       {/* Logo */}
       
       {/* 404 Title */}
       <h1 className="text-7xl font-bold text-gray-900">404</h1>
       {/* Page Not Found */}
       <h2 className="mt-4 text-2xl font-semibold text-gray-700">
         Page Not Found
       </h2>
       {/* Description */}
       <p className="mt-2 text-gray-500">
         The page you're looking for either doesn’t exist or has been moved.
       </p>
       {/* Button */}
       <Button asChild className="mt-6">
         <Link href="/">Go Back Home</Link>
       </Button>
     </div>
   </div>
 );
}
13. Implementasi Halaman Nextjs dengan autentifikasi supabase

Pertama Selanjutnya buat file/folder (protected)/dashboard/page.tsx. dan layout.tsx

//layout.tsx
"use client";
import { useAuth } from "@/hooks/use-auth";
import { redirect } from "next/navigation";
export default function ProtectedLayout({
 children,
}: {
 children: React.ReactNode;
}) {
 const { user, loading } = useAuth();
 if (loading) return <p>Loading...</p>;
 if (!user) {
   redirect("/sign-in");
 }
 return (
   <div>
     {/* Navbar / Sidebar */}
     {children}
   </div>
 );
}
//(protected)/dashboard/page.tsx
"use client";
import { useAuth } from "@/hooks/use-auth";
export default function DashboardPage() {
 const { user, role, loading, signOut } = useAuth();
 if (loading) return <p>Loading...</p>;
 if (!user) return <p>Not logged in</p>;
 return (
   <div>
     <p>Email: {user.email}</p>
     <p>Role: {role}</p>
     <button onClick={signOut}>Logout</button>
   </div>
 );
}

Kedua Buat file layout.tsx dan page.tsx pada folder (public)

 

//layout.tsx
export default function PublicLayout({
 children,
}: {
 children: React.ReactNode;
}) {
 return (
   <main style={{ minHeight: "100vh" }}>
     {children}
   </main>
 );
}


//page.tsx
export default function HomePage() {
 return (
   <main style={{ padding: '2rem' }}>
     <h1>🚀 Next.js Starter</h1>
     <p>App Router + TypeScript siap digunakan.</p>
     <ul>
       <li>📁 App Router</li>
       <li>⚡ Server Components</li>
       <li>🔌 API Routes</li>
     </ul>
   </main>
 )
}

 

Ketiga buat file/folder sign-in/page.tsx di (public)

"use client";
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Loader2, Eye, EyeOff, KeyRound, AtSign } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useAuth } from "@/hooks/use-auth";
import Link from "next/link";
export default function SignInPage() {
 const router = useRouter();
 const searchParams = useSearchParams();
 const { signIn } = useAuth();
 const [loading, setLoading] = useState(false);
 const [error, setError] = useState<string | null>(null);
 const [redirectTo, setRedirectTo] = useState("/dashboard");
 const [email, setEmail] = useState("");
 const [password, setPassword] = useState("");
 const [showPassword, setShowPassword] = useState(false);
 useEffect(() => {
   const redirect = searchParams.get("redirectTo");
   if (redirect && redirect.startsWith("/")) {
     setRedirectTo(redirect);
   }
 }, [searchParams]);
 const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
   event.preventDefault();
   setLoading(true);
   setError(null);
   try {
     const result = await signIn(email, password);
     if (result.error) {
       throw new Error(result.error);
     }
     router.push(redirectTo);
     router.refresh();
   } catch (err) {
     setError(err instanceof Error ? err.message : "An error occurred");
   } finally {
     setLoading(false);
   }
 };
 return (
   <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-orange-50 to-orange-200 p-4">
     <div className="w-full max-w-md">
       <div className="mb-10 text-center">
         <img src="/images/logo.png" alt="Logo" className="mx-auto h-12 w-auto" />
       </div>
       <Card className="bg-white/80 shadow-xl backdrop-blur-lg">
         <CardHeader>
           <CardTitle className="text-2xl font-bold">Selamat Datang Kembali</CardTitle>
           <CardDescription>Masukkan kredensial untuk mengakses dashboard Anda.</CardDescription>
         </CardHeader>
         <form onSubmit={handleSubmit}>
           <CardContent className="space-y-4">
             <div className="space-y-3">
               <Label htmlFor="email">Email</Label>
               <div className="relative">
                 <AtSign className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
                 <Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="user@example.com" required className="pl-10" />
               </div>
             </div>
             <div className="space-y-3">
               <div className="flex items-center justify-between">
                 <Label htmlFor="password">Kata Sandi</Label>
                 <Link href="#" className="text-sm text-orange-600 hover:underline">
                   Lupa kata sandi?
                 </Link>
               </div>
               <div className="relative">
                 <KeyRound className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
                 <Input
                   id="password"
                   type={showPassword ? "text" : "password"}
                   value={password}
                   onChange={(e) => setPassword(e.target.value)}
                   placeholder="••••••••"
                   required
                   minLength={6}
                   className="pl-10 pr-10"
                 />
                 <Button
                   type="button"
                   variant="ghost"
                   size="sm"
                   className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
                   onClick={() => setShowPassword(!showPassword)}
                 >
                   {showPassword ? <EyeOff className="text-muted-foreground h-4 w-4" /> : <Eye className="text-muted-foreground h-4 w-4" />}
                 </Button>
               </div>
             </div>
           </CardContent>
           <CardFooter className="flex flex-col gap-4 mt-4">
             {error && <p className="text-destructive text-sm font-medium">{error}</p>}
             <Button
               type="submit"
               size="lg"
               className="w-full bg-orange-600 text-white shadow-md transition-transform hover:bg-orange-700 hover:scale-[1.02]"
               disabled={loading}
             >
               {loading ? (
                 <>
                   <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                   Signing in...
                 </>
               ) : (
                 "Sign In"
               )}
             </Button>
           </CardFooter>
         </form>
       </Card>
     </div>
   </div>
 );
}

Daftar Refrensi : 

Langkah – langkah Instal Nextjs dan Supabase Siap Pakai Untuk Pemula | Blog Pursem.com | Pursem