<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vytroo</title>
    <description>The latest articles on DEV Community by Vytroo (@vytroo_e77e89f49d4188b8fa).</description>
    <link>https://dev.to/vytroo_e77e89f49d4188b8fa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3026105%2F2b14a0d7-6423-483b-a5cf-0827085b5700.jpg</url>
      <title>DEV Community: Vytroo</title>
      <link>https://dev.to/vytroo_e77e89f49d4188b8fa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vytroo_e77e89f49d4188b8fa"/>
    <language>en</language>
    <item>
      <title>NextJS Consume api</title>
      <dc:creator>Vytroo</dc:creator>
      <pubDate>Thu, 10 Apr 2025 23:02:07 +0000</pubDate>
      <link>https://dev.to/vytroo_e77e89f49d4188b8fa/nextjs-consume-api-4jbj</link>
      <guid>https://dev.to/vytroo_e77e89f49d4188b8fa/nextjs-consume-api-4jbj</guid>
      <description>&lt;p&gt;login&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// pages/login.js
import { useState } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { setToken, isAuthenticated } from "@/utils/auth";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";

const formSchema = z.object({
  username: z.string().min(1, "Username is required"),
  password: z.string().min(5, "Password must be at least 5 characters long"),
});

export default function LoginPage({ serverError }) {
  const router = useRouter();
  const [errorMessage, setErrorMessage] = useState(serverError || "");
  const [isLoading, setIsLoading] = useState(false);

  const form = useForm({
    defaultValues: {
      username: "",
      password: "",
    },
    resolver: zodResolver(formSchema),
  });

  const onSubmit = async (data) =&amp;gt; {
    setErrorMessage("");
    setIsLoading(true);

    try {
      const response = await fetch("http://localhost:8000/api/login/", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
        credentials: 'include', // Penting untuk cookies
      });

      const result = await response.json();

      if (!response.ok) {
        throw new Error(result.message || "Login failed. Please check your credentials.");
      }

      // Simpan token di cookie dan localStorage
      setToken(result.access);

      router.push("/menu");
    } catch (error) {
      setErrorMessage(error.message);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    &amp;lt;div className="min-h-screen flex items-center justify-center w-full"&amp;gt;
      &amp;lt;div className="max-w-xs w-full flex flex-col items-center"&amp;gt;
        &amp;lt;p className="mt-4 mb-8.5 text-xl font-bold tracking-tight"&amp;gt;
          Log in to RestoKu
        &amp;lt;/p&amp;gt;

        {errorMessage &amp;amp;&amp;amp; (
          &amp;lt;div className="bg-red-50 text-red-600 p-3 rounded mb-4 text-sm w-full"&amp;gt;
            {errorMessage}
          &amp;lt;/div&amp;gt;
        )}

        &amp;lt;Form {...form}&amp;gt;
          &amp;lt;form
            className="w-full space-y-4"
            onSubmit={form.handleSubmit(onSubmit)}
          &amp;gt;
            &amp;lt;FormField
              control={form.control}
              name="username"
              render={({ field }) =&amp;gt; (
                &amp;lt;FormItem&amp;gt;
                  &amp;lt;FormLabel&amp;gt;Username&amp;lt;/FormLabel&amp;gt;
                  &amp;lt;FormControl&amp;gt;
                    &amp;lt;Input
                      type="text"
                      placeholder="Username"
                      className="w-full"
                      disabled={isLoading}
                      {...field}
                    /&amp;gt;
                  &amp;lt;/FormControl&amp;gt;
                  &amp;lt;FormMessage /&amp;gt;
                &amp;lt;/FormItem&amp;gt;
              )}
            /&amp;gt;
            &amp;lt;FormField
              control={form.control}
              name="password"
              render={({ field }) =&amp;gt; (
                &amp;lt;FormItem&amp;gt;
                  &amp;lt;FormLabel&amp;gt;Password&amp;lt;/FormLabel&amp;gt;
                  &amp;lt;FormControl&amp;gt;
                    &amp;lt;Input
                      type="password"
                      placeholder="Password"
                      className="w-full"
                      disabled={isLoading}
                      {...field}
                    /&amp;gt;
                  &amp;lt;/FormControl&amp;gt;
                  &amp;lt;FormMessage /&amp;gt;
                &amp;lt;/FormItem&amp;gt;
              )}
            /&amp;gt;
            &amp;lt;Button 
              type="submit" 
              className="mt-4 w-full"
              disabled={isLoading}
            &amp;gt;
              {isLoading ? "Logging in..." : "Login"}
            &amp;lt;/Button&amp;gt;
          &amp;lt;/form&amp;gt;
        &amp;lt;/Form&amp;gt;
        &amp;lt;div className="mt-5 space-y-5"&amp;gt;
          &amp;lt;Link
            href="/forgot-password"
            className="text-sm block underline text-muted-foreground text-center"
          &amp;gt;
            Forgot your password?
          &amp;lt;/Link&amp;gt;
          &amp;lt;p className="text-sm text-center"&amp;gt;
            Don&amp;amp;apos;t have an account?
            &amp;lt;Link href="/register" className="ml-1 underline text-muted-foreground"&amp;gt;
              Create account
            &amp;lt;/Link&amp;gt;
          &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export async function getServerSideProps(context) {
  // Cek jika pengguna sudah terautentikasi
  if (isAuthenticated(context)) {
    return {
      redirect: {
        destination: '/menu',
        permanent: false,
      },
    };
  }

  const { error } = context.query;

  return {
    props: {
      serverError: error || null,
    },
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;utils/auth&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// utils/auth.js
import Cookies from 'js-cookie';
import { parseCookies } from 'nookies';

// Fungsi untuk menyimpan token di cookies dan localStorage (untuk kompatibilitas)
export const setToken = (token) =&amp;gt; {
  if (typeof window !== 'undefined') {
    // Client-side
    localStorage.setItem('adminToken', token);
    // Mengatur cookie dengan secure flag, httpOnly untuk produksi
    Cookies.set('adminToken', token, { expires: 7 }); // Expires in 7 days
  }
};

// Fungsi untuk mendapatkan token
export const getToken = (ctx) =&amp;gt; {
  // Server-side
  if (ctx) {
    const cookies = parseCookies(ctx);
    return cookies.adminToken;
  }

  // Client-side
  if (typeof window !== 'undefined') {
    return Cookies.get('adminToken') || localStorage.getItem('adminToken');
  }

  return null;
};

// Fungsi untuk menghapus token saat logout
export const removeToken = () =&amp;gt; {
  if (typeof window !== 'undefined') {
    localStorage.removeItem('adminToken');
    Cookies.remove('adminToken');
  }
};

// Fungsi untuk memeriksa apakah pengguna sudah terautentikasi
export const isAuthenticated = (ctx) =&amp;gt; {
  const token = getToken(ctx);
  return !!token;
};


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;menu&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// pages/menu.tsx
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { PlusIcon } from "lucide-react";
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { getToken, removeToken } from "@/utils/auth";
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';

interface Menuitem {
  id_menu: number;
  nama_menu: string;
  harga: number;
  menu_image: string;
  id_kategori: number;
}

async function getMenuData() {
  const token = getToken({ req: { headers: { cookie: cookies().toString() } });

  if (!token) {
    redirect('/login');
  }

  try {
    const response = await fetch("http://127.0.0.1:8000/api/menu/", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      cache: 'no-store'
    });

    if (response.status === 401) {
      removeToken();
      redirect('/login');
    }

    if (!response.ok) {
      throw new Error('Failed to fetch menu data');
    }

    return await response.json();
  } catch (error) {
    console.error("Error fetching menu data:", error);
    return [];
  }
}

export default async function MenuPage() {
  const menus: Menuitem[] = await getMenuData();

  async function handleAddMenu(formData: FormData) {
    'use server';

    const token = getToken({ req: { headers: { cookie: cookies().toString() } });

    if (!token) {
      redirect('/login');
    }

    const namaMenu = formData.get('nama_menu') as string;
    const harga = formData.get('harga') as string;
    const kategori = formData.get('id_kategori') as string;
    const imageFile = formData.get('menu_image') as File;

    const postData = new FormData();
    postData.append('nama_menu', namaMenu);
    postData.append('harga', harga);
    postData.append('id_kategori', kategori);

    if (imageFile.size &amp;gt; 0) {
      postData.append('menu_image', imageFile);
    }

    try {
      const response = await fetch("http://127.0.0.1:8000/api/menu/", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
        },
        body: postData,
      });

      if (response.status === 401) {
        removeToken();
        redirect('/login');
      }

      if (!response.ok) {
        throw new Error('Failed to add menu');
      }

      redirect('/menu');
    } catch (error) {
      console.error("Error adding menu:", error);
      throw error;
    }
  }

  return (
    &amp;lt;div className="max-w-screen-lg w-full py-10 px-6"&amp;gt;
      &amp;lt;div className="flex justify-between items-center"&amp;gt;
        &amp;lt;h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl mb-4"&amp;gt;
          Daftar Menu
        &amp;lt;/h1&amp;gt;
        &amp;lt;form action="/logout" method="POST"&amp;gt;
          &amp;lt;Button type="submit" variant="outline"&amp;gt;
            Logout
          &amp;lt;/Button&amp;gt;
        &amp;lt;/form&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;Dialog&amp;gt;
        &amp;lt;DialogTrigger asChild&amp;gt;
          &amp;lt;Button
            variant="outline"
            size="icon"
            className="w-[140px] font-normal bg-gray-100"
          &amp;gt;
            &amp;lt;PlusIcon className="mr-2" /&amp;gt; Tambah Menu
          &amp;lt;/Button&amp;gt;
        &amp;lt;/DialogTrigger&amp;gt;
        &amp;lt;DialogContent&amp;gt;
          &amp;lt;DialogHeader&amp;gt;
            &amp;lt;DialogTitle&amp;gt;Tambah Menu Baru&amp;lt;/DialogTitle&amp;gt;
          &amp;lt;/DialogHeader&amp;gt;

          &amp;lt;form action={handleAddMenu}&amp;gt;
            &amp;lt;div className="grid gap-4 py-4"&amp;gt;
              &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                &amp;lt;Label htmlFor="nama" className="text-right"&amp;gt;
                  Nama
                &amp;lt;/Label&amp;gt;
                &amp;lt;Input
                  id="nama"
                  name="nama_menu"
                  className="col-span-3"
                  required
                /&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                &amp;lt;Label htmlFor="harga" className="text-right"&amp;gt;
                  Harga
                &amp;lt;/Label&amp;gt;
                &amp;lt;Input
                  id="harga"
                  name="harga"
                  type="number"
                  className="col-span-3"
                  required
                /&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                &amp;lt;Label htmlFor="kategori" className="text-right"&amp;gt;
                  Kategori
                &amp;lt;/Label&amp;gt;
                &amp;lt;Input
                  id="kategori"
                  name="id_kategori"
                  type="number"
                  defaultValue="1"
                  className="col-span-3"
                  required
                /&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                &amp;lt;Label htmlFor="gambar" className="text-right"&amp;gt;
                  Gambar
                &amp;lt;/Label&amp;gt;
                &amp;lt;Input
                  id="gambar"
                  name="menu_image"
                  type="file"
                  accept="image/*"
                  className="col-span-3"
                /&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;DialogFooter&amp;gt;
              &amp;lt;Button type="submit"&amp;gt;Simpan&amp;lt;/Button&amp;gt;
            &amp;lt;/DialogFooter&amp;gt;
          &amp;lt;/form&amp;gt;
        &amp;lt;/DialogContent&amp;gt;
      &amp;lt;/Dialog&amp;gt;

      &amp;lt;div className="mt-8 w-full mx-auto grid md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-8"&amp;gt;
        {menus.map((menu) =&amp;gt; (
          &amp;lt;Card
            key={menu.id_menu}
            className="flex flex-col border rounded-xl overflow-hidden shadow-none"
          &amp;gt;
            &amp;lt;CardHeader&amp;gt;
              &amp;lt;h4 className="!mt-3 text-xl font-semibold tracking-tight"&amp;gt;
                {menu.nama_menu}
              &amp;lt;/h4&amp;gt;
              &amp;lt;p className="mt-1 text-muted-foreground text-[17px]"&amp;gt;
                Rp {menu.harga.toLocaleString()}
              &amp;lt;/p&amp;gt;
            &amp;lt;/CardHeader&amp;gt;
            &amp;lt;CardContent className="mt-auto px-0 pb-0"&amp;gt;
              &amp;lt;div&amp;gt;
                &amp;lt;img
                  src={menu.menu_image}
                  alt={menu.nama_menu}
                  className="bg-muted h-40 ml-6 rounded-tl-xl object-cover w-[calc(100%-1.5rem)]"
                /&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/CardContent&amp;gt;
          &amp;lt;/Card&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;meja&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// app/features-02/page.tsx
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PlusIcon } from "lucide-react";
import { getToken, removeToken } from "@/utils/auth";
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';

interface MejaItem {
  id_meja: number;
  no_meja: number;
  kapasitas: number;
  image_meja: string;
}

async function getMejaData() {
  const token = getToken({ req: { headers: { cookie: cookies().toString() } });

  if (!token) {
    redirect('/login');
  }

  try {
    const response = await fetch("http://127.0.0.1:8000/api/meja/", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      cache: 'no-store'
    });

    if (response.status === 401) {
      removeToken();
      redirect('/login');
    }

    if (!response.ok) {
      throw new Error('Failed to fetch table data');
    }

    return await response.json();
  } catch (error) {
    console.error("Error fetching table data:", error);
    return [];
  }
}

export default async function Features02Page() {
  const mejas: MejaItem[] = await getMejaData();

  async function handleAddTable(formData: FormData) {
    'use server';

    const token = getToken({ req: { headers: { cookie: cookies().toString() } });

    if (!token) {
      redirect('/login');
    }

    const no_meja = formData.get('no_meja') as string;
    const kapasitas = formData.get('kapasitas') as string;
    const imageFile = formData.get('image_meja') as File;

    const postData = new FormData();
    postData.append('no_meja', no_meja);
    postData.append('kapasitas', kapasitas);

    if (imageFile.size &amp;gt; 0) {
      postData.append('image_meja', imageFile);
    }

    try {
      const response = await fetch("http://127.0.0.1:8000/api/meja/", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
        },
        body: postData,
      });

      if (response.status === 401) {
        removeToken();
        redirect('/login');
      }

      if (!response.ok) {
        throw new Error('Failed to add table');
      }

      redirect('/features-02');
    } catch (error) {
      console.error("Error adding table:", error);
      throw error;
    }
  }

  return (
    &amp;lt;div className="min-h-screen flex justify-center py-12"&amp;gt;
      &amp;lt;div className="w-full"&amp;gt;
        &amp;lt;div className="flex justify-between items-center px-6"&amp;gt;
          &amp;lt;h2 className="text-4xl sm:text-5xl font-bold tracking-tight mb-14 text-center"&amp;gt;
            Daftar Meja
          &amp;lt;/h2&amp;gt;
          &amp;lt;form action="/logout" method="POST"&amp;gt;
            &amp;lt;Button type="submit" variant="outline"&amp;gt;
              Logout
            &amp;lt;/Button&amp;gt;
          &amp;lt;/form&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div className="ml-[315px]"&amp;gt;
          &amp;lt;Dialog&amp;gt;
            &amp;lt;DialogTrigger asChild&amp;gt;
              &amp;lt;Button
                variant="outline"
                size="icon"
                className="w-[140px] font-normal bg-gray-100"
              &amp;gt;
                &amp;lt;PlusIcon className="mr-2" /&amp;gt; Tambah Meja
              &amp;lt;/Button&amp;gt;
            &amp;lt;/DialogTrigger&amp;gt;
            &amp;lt;DialogContent&amp;gt;
              &amp;lt;DialogHeader&amp;gt;
                &amp;lt;DialogTitle&amp;gt;Tambah Meja Baru&amp;lt;/DialogTitle&amp;gt;
              &amp;lt;/DialogHeader&amp;gt;

              &amp;lt;form action={handleAddTable}&amp;gt;
                &amp;lt;div className="grid gap-4 py-4"&amp;gt;
                  &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                    &amp;lt;Label htmlFor="no_meja" className="text-right"&amp;gt;
                      No Meja
                    &amp;lt;/Label&amp;gt;
                    &amp;lt;Input
                      id="no_meja"
                      name="no_meja"
                      className="col-span-3"
                      required
                    /&amp;gt;
                  &amp;lt;/div&amp;gt;
                  &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                    &amp;lt;Label htmlFor="kapasitas" className="text-right"&amp;gt;
                      Kapasitas
                    &amp;lt;/Label&amp;gt;
                    &amp;lt;Input
                      id="kapasitas"
                      name="kapasitas"
                      type="number"
                      className="col-span-3"
                      required
                    /&amp;gt;
                  &amp;lt;/div&amp;gt;
                  &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                    &amp;lt;Label htmlFor="gambar" className="text-right"&amp;gt;
                      Gambar
                    &amp;lt;/Label&amp;gt;
                    &amp;lt;Input
                      id="gambar"
                      name="image_meja"
                      type="file"
                      accept="image/*"
                      className="col-span-3"
                    /&amp;gt;
                  &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;DialogFooter&amp;gt;
                  &amp;lt;Button type="submit"&amp;gt;Simpan&amp;lt;/Button&amp;gt;
                &amp;lt;/DialogFooter&amp;gt;
              &amp;lt;/form&amp;gt;
            &amp;lt;/DialogContent&amp;gt;
          &amp;lt;/Dialog&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div className="mt-10 grid sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-12 max-w-md sm:max-w-screen-md lg:max-w-screen-lg w-full mx-auto px-6"&amp;gt;
          {mejas.map((meja) =&amp;gt; (
            &amp;lt;div key={meja.id_meja} className="flex flex-col text-start"&amp;gt;
              &amp;lt;div&amp;gt;
                &amp;lt;img
                  src={meja.image_meja}
                  alt={`Meja ${meja.no_meja}`}
                  className="mb-5 sm:mb-6 w-full aspect-[3/3] bg-muted rounded-xl object-cover"
                /&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;span className="text-2xl font-semibold tracking-tight"&amp;gt;
                Nomor Meja: {meja.no_meja}
              &amp;lt;/span&amp;gt;
              &amp;lt;p className="mt-2 max-w-[25ch] text-muted-foreground text-[17px]"&amp;gt;
                Kapasitas: {meja.kapasitas} orang
              &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
          ))}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Flutter consume api login</title>
      <dc:creator>Vytroo</dc:creator>
      <pubDate>Thu, 10 Apr 2025 12:16:40 +0000</pubDate>
      <link>https://dev.to/vytroo_e77e89f49d4188b8fa/flutter-consume-api-login-1dcp</link>
      <guid>https://dev.to/vytroo_e77e89f49d4188b8fa/flutter-consume-api-login-1dcp</guid>
      <description>&lt;p&gt;cors di api&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALLOWED_HOSTS = ['*']
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://localhost:54475",
    "http://10.0.2.2:8000",
    "http://127.0.0.1:8000",   
    "http://192.168.118.247:8000",  
    'http://192.168.1.5:8000',
    'http://10.0.2.2', 
    'http://10.0.2.2:8000', 
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;main&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
import 'package:flutter/material.dart';
import 'screens/login_screen.dart';

void main() =&amp;gt; runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Reservasi Meja',
      home: LoginScreen(),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;/lib/screens/login_Screen.dart&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import 'package:flutter/material.dart';
import '../services/api_service.dart';
import 'menu_screen.dart';

class LoginScreen extends StatefulWidget {
  @override
  _LoginScreenState createState() =&amp;gt; _LoginScreenState();
}

class _LoginScreenState extends State&amp;lt;LoginScreen&amp;gt; {
  final usernameController = TextEditingController();
  final passwordController = TextEditingController();

  void login() async {
    final token = await ApiService.login(
      usernameController.text,
      passwordController.text,
    );
    if (token != null) {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (_) =&amp;gt; MenuScreen(token: token)),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Login gagal')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          children: [
            TextField(controller: usernameController, decoration: InputDecoration(labelText: 'Username')),
            TextField(controller: passwordController, decoration: InputDecoration(labelText: 'Password'), obscureText: true),
            SizedBox(height: 20),
            ElevatedButton(onPressed: login, child: Text('Login')),
          ],
        ),
      ),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;screens/menu_screen&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import 'package:flutter/material.dart';
import '../services/api_service.dart';
import '../models/menu_models.dart';

class MenuScreen extends StatelessWidget {
  final String token;
  const MenuScreen({super.key, required this.token});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Daftar Menu')),
      body: FutureBuilder&amp;lt;List&amp;lt;MenuModel&amp;gt;&amp;gt;(
        future: ApiService.getMenu(token),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) return Center(child: CircularProgressIndicator());
          if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text('Tidak ada menu'));
          final menuList = snapshot.data!;

          return ListView(
            children: [
              ...menuList.map((m) =&amp;gt; ListTile(
                    title: Text(m.nama_menu),
                    subtitle: Text('Rp ${m.harga}'),
                  )),

            ],
          );
        },
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;/services/api_service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/menu_models.dart';

class ApiService {
  static const baseUrl = 'http://10.0.2.2:8000/api';

  static Future&amp;lt;String?&amp;gt; login(String username, String password) async {
    final response = await http.post(
      Uri.parse('$baseUrl/login/'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'username': username, 'password': password}),
    );
    if (response.statusCode == 200) {
      final json = jsonDecode(response.body);
      return json['access'];
    }
    return null;
  }

  static Future&amp;lt;List&amp;lt;MenuModel&amp;gt;&amp;gt; getMenu(String token) async {
    final response = await http.get(
      Uri.parse('$baseUrl/menu/'),
      headers: {'Authorization': 'Bearer $token'},
    );
    if (response.statusCode == 200) {
      return (jsonDecode(response.body) as List)
          .map((e) =&amp;gt; MenuModel.fromJson(e))
          .toList();
    }
    throw Exception('Failed to load menus');

  }

  // static Future&amp;lt;List&amp;lt;MejaModel&amp;gt;&amp;gt; getMeja(String token) async {
  //   final response = await http.get(
  //     Uri.parse('$baseUrl/meja/'),
  //     headers: {'Authorization': 'Bearer $token'},
  //   );
  //   if (response.statusCode == 200) {
  //     final List data = jsonDecode(response.body);
  //     return data.map((e) =&amp;gt; MejaModel.fromJson(e)).toList();
  //   }
  //   return [];
  // }

  static Future&amp;lt;void&amp;gt; updateMeja(String token, int id, String status) async {
    await http.patch(
      Uri.parse('$baseUrl/meja/$id/'),
      headers: {
        'Authorization': 'Bearer $token',
        'Content-Type': 'application/json',
      },
      body: jsonEncode({"status": status}),
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;models/menu_models&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class MenuModel {
  final int id_menu;
  final String nama_menu;
  final double harga;
  final int stok;
  final String foto;
  MenuModel(
      {required this.id_menu,
      required this.nama_menu,
      required this.harga,
      required this.foto,
      required this.stok});

  factory MenuModel.fromJson(Map json) {
    return MenuModel(
        id_menu: json['id_menu'],
        nama_menu: json['nama_menu'],
        harga: double.tryParse(json['harga'].toString()) ?? 0.0,
        stok: json['stok'],
        foto: json['foto']);
  }
  Map toJson() =&amp;gt; {
        'id_menu': id_menu,
        'nama_menu': nama_menu,
        'harga': harga,
        'stok': stok,
        'foto': foto,
      };
}

// ==== models/meja_model.dart ====
class MejaModel {
  final int? id;
  final int noMeja;
  final int kapasitas;
  final String status;

  MejaModel(
      {this.id,
      required this.noMeja,
      required this.kapasitas,
      required this.status});

  factory MejaModel.fromJson(Map&amp;lt;String, dynamic&amp;gt; json) {
    return MejaModel(
      id: json['id'],
      noMeja: json['no_meja'],
      kapasitas: json['kapasitas'],
      status: json['status'],
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;menu extend&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import 'package:flutter/material.dart';
import '../services/api_service.dart';
import '../models/menu_models.dart';

class MenuScreen extends StatelessWidget {
  final String token;
  const MenuScreen({super.key, required this.token});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Menu Makanan')),
      body: FutureBuilder&amp;lt;List&amp;lt;MenuModel&amp;gt;&amp;gt;(
        future: ApiService.getMenu(token),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) return Center(child: CircularProgressIndicator());
          if (!snapshot.hasData || snapshot.data!.isEmpty) return Center(child: Text('Tidak ada menu tersedia'));

          final menuList = snapshot.data!;

          return Padding(
            padding: const EdgeInsets.all(12.0),
            child: GridView.builder(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 12,
                mainAxisSpacing: 12,
                childAspectRatio: 0.75,
              ),
              itemCount: menuList.length,
              itemBuilder: (context, index) {
                final m = menuList[index];
                return Card(
                  elevation: 4,
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      // Foto Menu
                      ClipRRect(
                        borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
                        child: Image.network(
                          m.foto,
                          height: 110,
                          width: double.infinity,
                          fit: BoxFit.cover,
                          errorBuilder: (context, error, stackTrace) =&amp;gt; Container(
                            height: 110,
                            color: Colors.grey[300],
                            child: Icon(Icons.image_not_supported),
                          ),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(
                              m.nama_menu,
                              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                            ),
                            SizedBox(height: 4),
                            Text('Rp ${m.harga.toStringAsFixed(0)}'),
                            Text('Stok: ${m.stok}'),
                            SizedBox(height: 8),
                            ElevatedButton(
                              onPressed: () {
                                // TODO: Tambahkan logika tambah ke keranjang atau pesan
                                ScaffoldMessenger.of(context).showSnackBar(
                                  SnackBar(content: Text('${m.nama_menu} ditambahkan ke pesanan')),
                                );
                              },
                              style: ElevatedButton.styleFrom(
                                minimumSize: Size(double.infinity, 36),
                                backgroundColor: Colors.green,
                              ),
                              child: Text('Pesan'),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          );
        },
      ),

      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          // TODO: Buka halaman keranjang
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Menuju halaman keranjang...')),
          );
        },
        label: Text('Keranjang'),
        icon: Icon(Icons.shopping_cart),
      ),
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Fitur Restoran NextJS</title>
      <dc:creator>Vytroo</dc:creator>
      <pubDate>Mon, 07 Apr 2025 14:23:13 +0000</pubDate>
      <link>https://dev.to/vytroo_e77e89f49d4188b8fa/fitur-restoran-nextjs-21jd</link>
      <guid>https://dev.to/vytroo_e77e89f49d4188b8fa/fitur-restoran-nextjs-21jd</guid>
      <description>&lt;p&gt;data-table&lt;/p&gt;

&lt;p&gt;✅ 1. &lt;strong&gt;Masalah Edit dan Delete Belum Berfungsi&lt;/strong&gt;&lt;br&gt;
Di &lt;code&gt;getUserColumns&lt;/code&gt;, kamu cuma nge-&lt;code&gt;console.log&lt;/code&gt; saat tombol edit/delete diklik:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getUserColumns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Delete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔧 Solusi:&lt;br&gt;
Tambahkan state dan handler di &lt;code&gt;UserTable&lt;/code&gt; untuk memicu edit/delete yang sesungguhnya:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;editingUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEditingUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleUpdateClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setEditingUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setUSername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setNo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;no_hp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setAlamat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alamat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleDeleteClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adminToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://localhost:8000/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DELETE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Gagal hapus user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Delete Error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ganti pemanggilan fungsi ini di bawah:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getUserColumns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handleUpdateClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleDeleteClick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ 2. &lt;strong&gt;Tombol Submit Tidak Memperbarui User Jika Mode Edit&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sekarang &lt;code&gt;handleSubmit&lt;/code&gt; hanya menambah user baru. Kalau kamu sedang edit, perlu membedakan logic-nya.&lt;/p&gt;

&lt;h4&gt;
  
  
  🔧 Solusi:
&lt;/h4&gt;

&lt;p&gt;Modifikasi &lt;code&gt;handleSubmit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_hp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;no_hp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alamat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;alamat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;adminToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;editingUser&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`http://127.0.0.1:8000/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;editingUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://127.0.0.1:8000/api/user/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;editingUser&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Gagal simpan data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;editingUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Update data di list&lt;/span&gt;
      &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;editingUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;newUser&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Tambahkan user baru&lt;/span&gt;
      &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newUser&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Reset form&lt;/span&gt;
    &lt;span class="nf"&gt;setUSername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setAlamat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setNo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setEditingUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Submit Error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ 3. &lt;strong&gt;Tampilkan Nama Dialog Berubah Saat Edit&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Ganti bagian ini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Tambahkan User&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Menjadi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;editingUser&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Edit User&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tambahkan User&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  ✅ 4. &lt;strong&gt;Bug Minor: Tipe Email&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Kamu pakai &lt;code&gt;email: number;&lt;/code&gt; di &lt;code&gt;User&lt;/code&gt;, padahal email itu &lt;code&gt;string&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ini sebelumnya number&lt;/span&gt;
  &lt;span class="nl"&gt;no_hp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;alamat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Kalau kamu butuh bantuin struktur update endpoint dari backend biar cocok (PUT dengan FormData), tinggal bilang aja ya.&lt;/p&gt;

&lt;p&gt;Mau sekalian ku bantuin styling atau pop-up konfirmasi delete juga?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

1. Menu

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client"
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { PlusIcon } from "lucide-react";
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";

import { useEffect, useState } from "react";

interface Menuitem {
  id_menu: number;
  nama_menu: string;
  harga: number;
  menu_image: string;
  id_kategori:number;
}

const Features05Page = () =&amp;gt; {
  const [menus, setMenu] = useState&amp;lt;Menuitem[]&amp;gt;([]);
  const [namaMenu, setNamaMenu] = useState("");
  const [harga, setHarga] = useState("");
  const [imageFile, setImageFile] = useState&amp;lt;File | null&amp;gt;(null);
  const [kategori,setKategori]=useState(1)


  const handleSubmit = async () =&amp;gt; {
    const token = localStorage.getItem("adminToken");
    const formData = new FormData();
    formData.append("nama_menu", namaMenu);
    formData.append("harga", harga);
    formData.append("id_kategori", kategori.toString()); // pastikan string

    if (imageFile) {
      formData.append("menu_image", imageFile);
    }

    try {
      const response = await fetch("http://127.0.0.1:8000/api/menu/", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          // jangan tambahkan content-type, biarkan browser mengatur sendiri
        },
        body: formData,
      });

      if (!response.ok) {
        throw new Error("Gagal menambahkan menu");
      }

      const data = await response.json();
      setMenu((prev) =&amp;gt; [...prev, data]); // update UI langsung
      setNamaMenu("");
      setHarga("");
      setKategori(1)
      setImageFile(null);
    } catch (error) {
      console.error("Error:", error);
    }
  };
  useEffect(() =&amp;gt; {
    const token = localStorage.getItem("adminToken");
    fetch("http://127.0.0.1:8000/api/menu/", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    })
      .then((response) =&amp;gt; response.json())
      .then((data: Menuitem[]) =&amp;gt; setMenu(data))
      .catch((error) =&amp;gt; console.log("error", error));
  }, []);

  return (
    &amp;lt;div className="max-w-screen-lg w-full py-10 px-6"&amp;gt;
      &amp;lt;h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl mb-4"&amp;gt;
        Daftar Menu
      &amp;lt;/h1&amp;gt;
      &amp;lt;Dialog&amp;gt;
        &amp;lt;DialogTrigger asChild&amp;gt;
          &amp;lt;Button
            variant="outline"
            size="icon"
            className="w-[140px] font-normal bg-gray-100"
          &amp;gt;
            &amp;lt;PlusIcon className="mr-2" /&amp;gt; Tambah Menu
          &amp;lt;/Button&amp;gt;
        &amp;lt;/DialogTrigger&amp;gt;
        &amp;lt;DialogContent&amp;gt;
          &amp;lt;DialogHeader&amp;gt;
            &amp;lt;DialogTitle&amp;gt;Tambah Menu Baru&amp;lt;/DialogTitle&amp;gt;
          &amp;lt;/DialogHeader&amp;gt;

          &amp;lt;div className="grid gap-4 py-4"&amp;gt;
            &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
              &amp;lt;Label htmlFor="nama" className="text-right"&amp;gt;
                Nama
              &amp;lt;/Label&amp;gt;
              &amp;lt;Input
                id="nama"
                value={namaMenu}
                onChange={(e) =&amp;gt; setNamaMenu(e.target.value)}
                className="col-span-3"
              /&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
              &amp;lt;Label htmlFor="harga" className="text-right"&amp;gt;
                Harga
              &amp;lt;/Label&amp;gt;
              &amp;lt;Input
                id="harga"
                value={harga}
                onChange={(e) =&amp;gt; setHarga(e.target.value)}
                className="col-span-3"
              /&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
              &amp;lt;Label htmlFor="kategori" className="text-right"&amp;gt;
                kategori
              &amp;lt;/Label&amp;gt;
              &amp;lt;Input
                id="kategori"
                value={kategori}
                onChange={(e) =&amp;gt; setKategori(parseInt(e.target.value))}
                className="col-span-3"
              /&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
              &amp;lt;Label htmlFor="gambar" className="text-right"&amp;gt;
                Gambar
              &amp;lt;/Label&amp;gt;
              &amp;lt;Input
                id="gambar"
                type="file"
                accept="image/*"
                onChange={(e) =&amp;gt; setImageFile(e.target.files?.[0] || null)}
                className="col-span-3"
              /&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;

          &amp;lt;DialogFooter&amp;gt;
            &amp;lt;Button onClick={handleSubmit}&amp;gt;Simpan&amp;lt;/Button&amp;gt;
          &amp;lt;/DialogFooter&amp;gt;
        &amp;lt;/DialogContent&amp;gt;
      &amp;lt;/Dialog&amp;gt;
      &amp;lt;div className="mt-8 w-full mx-auto grid md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-8"&amp;gt;
        {menus.map((menu) =&amp;gt; (
          &amp;lt;Card
            key={menu.id_menu}
            className="flex flex-col border rounded-xl overflow-hidden shadow-none"
          &amp;gt;
            &amp;lt;CardHeader&amp;gt;
              &amp;lt;h4 className="!mt-3 text-xl font-semibold tracking-tight"&amp;gt;
                {menu.nama_menu}
              &amp;lt;/h4&amp;gt;
              &amp;lt;p className="mt-1 text-muted-foreground text-[17px]"&amp;gt;
                {menu.harga}
              &amp;lt;/p&amp;gt;
            &amp;lt;/CardHeader&amp;gt;
            &amp;lt;CardContent className="mt-auto px-0 pb-0"&amp;gt;
              &amp;lt;div&amp;gt;
                &amp;lt;img
                  src={menu.menu_image}
                  alt="Error"
                  className="bg-muted h-40 ml-6 rounded-tl-xl"
                /&amp;gt;
              &amp;lt;/div&amp;gt;
            &amp;lt;/CardContent&amp;gt;
          &amp;lt;/Card&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Features05Page;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Meja
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useEffect, useState } from "react";
import { Button } from "../ui/button";
import {
  Dialog,
  DialogContent,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Label } from "@/components/ui/label";
import { PlusIcon } from "lucide-react";

const features = [
  {
    title: "Identify Opportunities",
    description: "Find untapped areas to explore effortlessly.",
  },
  {
    title: "Build Authority",
    description: "Craft content that resonates and inspires trust.",
  },
  {
    title: "Instant Insights",
    description: "Get actionable insights instantly at a glance.",
  },
];

interface MejaItem {
  id_meja: number;
  no_meja: number;
  kapasitas: number;
  image_meja: string;
}
const Features02Page = () =&amp;gt; {
  const [mejas, setMeja] = useState&amp;lt;MejaItem[]&amp;gt;([]);
  const [no_meja, setNomeja] = useState("");
  const [id_meja, setIDMeja] = useState("");
  const [kapasitas, setKapasitas] = useState("");
  const [imageMeja, setImageMeja] = useState&amp;lt;File | null&amp;gt;(null);
  const [open, setOpen] = useState(false);

  useEffect(() =&amp;gt; {
    const token = localStorage.getItem("adminToken");

    try {
      fetch("http://127.0.0.1:8000/api/meja/", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      })
        .then((response) =&amp;gt; response.json())
        .then((data: MejaItem[]) =&amp;gt; setMeja(data))
        .catch((error) =&amp;gt; console.log("error ", error));
    } catch (error) {
      console.log("error ", error);
    }
  }, []);

  const handleSubmit = async () =&amp;gt; {
    const token = localStorage.getItem("adminToken");
    const newForm = new FormData();

    newForm.append("no_meja", no_meja);
    newForm.append("kapasitas", kapasitas);
    if (imageMeja) {
      newForm.append("image_meja", imageMeja);
    }

    try {
      const response = await fetch("http://127.0.0.1:8000/api/meja/", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
        },
        body: newForm,
      });
      if (!response.ok) {
        throw new Error("Gagal menambahkan meja");
      }
      const data = await response.json();
      setMeja((prev) =&amp;gt; [...prev, data]);
      setNomeja("");
      setKapasitas("");
      setImageMeja(null);
      setOpen(false);
    } catch (error) {
      console.log(error);
    }
  };
  return (
    &amp;lt;div className="min-h-screen flex justify-center py-12"&amp;gt;
      &amp;lt;div className="w-full"&amp;gt;
        &amp;lt;h2 className="text-4xl sm:text-5xl font-bold tracking-tight mb-14 text-center"&amp;gt;
          Daftar Meja
        &amp;lt;/h2&amp;gt;
        &amp;lt;div className="ml-[315px]"&amp;gt;
          &amp;lt;Dialog open={open} onOpenChange={setOpen}&amp;gt;
            &amp;lt;DialogTrigger asChild&amp;gt;
              &amp;lt;Button
                variant="outline"
                size="icon"
                className="w-[140px]  font-normal bg-gray-100"
              &amp;gt;
                &amp;lt;PlusIcon className="mr-2" /&amp;gt; Tambah Menu
              &amp;lt;/Button&amp;gt;
            &amp;lt;/DialogTrigger&amp;gt;
            &amp;lt;DialogContent&amp;gt;
              &amp;lt;DialogHeader&amp;gt;
                &amp;lt;DialogTitle&amp;gt;Tambah Menu Baru&amp;lt;/DialogTitle&amp;gt;
              &amp;lt;/DialogHeader&amp;gt;

              &amp;lt;div className="grid gap-4 py-4"&amp;gt;
                &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                  &amp;lt;Label htmlFor="no_meja" className="text-right"&amp;gt;
                    No Meja
                  &amp;lt;/Label&amp;gt;
                  &amp;lt;Input
                    id="no_meja"
                    value={no_meja}
                    onChange={(e) =&amp;gt; setNomeja(e.target.value)}
                    className="col-span-3"
                  /&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                  &amp;lt;Label htmlFor="kapasitas" className="text-right"&amp;gt;
                    Kapasitas
                  &amp;lt;/Label&amp;gt;
                  &amp;lt;Input
                    id="kapasitas"
                    value={kapasitas}
                    onChange={(e) =&amp;gt; setKapasitas(e.target.value)}
                    className="col-span-3"
                  /&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
                  &amp;lt;Label htmlFor="gambar" className="text-right"&amp;gt;
                    Gambar
                  &amp;lt;/Label&amp;gt;
                  &amp;lt;Input
                    id="gambar"
                    type="file"
                    accept="image/*"
                    onChange={(e) =&amp;gt; setImageMeja(e.target.files?.[0] || null)}
                    className="col-span-3"
                  /&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/div&amp;gt;

              &amp;lt;DialogFooter&amp;gt;
                &amp;lt;Button onClick={handleSubmit}&amp;gt;Simpan&amp;lt;/Button&amp;gt;
              &amp;lt;/DialogFooter&amp;gt;
            &amp;lt;/DialogContent&amp;gt;
          &amp;lt;/Dialog&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className="mt-10 grid sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-12 max-w-md sm:max-w-screen-md lg:max-w-screen-lg w-full mx-auto px-6"&amp;gt;
          {mejas.map((meja) =&amp;gt; (
            &amp;lt;div key={meja.id_meja} className="flex flex-col text-start"&amp;gt;
              &amp;lt;div&amp;gt;
                &amp;lt;img
                  src={meja.image_meja}
                  alt="Image Not Found"
                  className="mb-5 sm:mb-6 w-full aspect-[3/3] bg-muted rounded-xl"
                /&amp;gt;
              &amp;lt;/div&amp;gt;
              &amp;lt;span className="text-2xl font-semibold tracking-tight"&amp;gt;
                Nomer Meja : {meja.no_meja}
              &amp;lt;/span&amp;gt;
              &amp;lt;p className="mt-2 max-w-[25ch] text-muted-foreground text-[17px]"&amp;gt;
                Kapasitas : {meja.kapasitas}
              &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
          ))}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Features02Page;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Reservasi
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// page
"use client"

import React from "react"
import { ReservationTable } from "./data-table"

export default function ReservationPage() {
  return (
    &amp;lt;div className="p-4"&amp;gt;
      &amp;lt;h1 className="text-xl font-bold mb-4"&amp;gt;Reservation List&amp;lt;/h1&amp;gt;
      &amp;lt;ReservationTable /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
// column.tsx


import { ColumnDef } from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ArrowUpDown, Edit, Trash2 } from "lucide-react"
import { Reservation } from "./data-table"

export function getReservationColumns(
  handleUpdateClick: (reservation: Reservation) =&amp;gt; void,
  handleDeleteClick: (reservation: Reservation) =&amp;gt; void
): ColumnDef&amp;lt;Reservation&amp;gt;[] {
  return [
    {
      id: "select",
      header: ({ table }) =&amp;gt; (
        &amp;lt;Checkbox
          checked={
            table.getIsAllPageRowsSelected() ||
            (table.getIsSomePageRowsSelected() &amp;amp;&amp;amp; "indeterminate")
          }
          onCheckedChange={(value) =&amp;gt; table.toggleAllPageRowsSelected(!!value)}
          aria-label="Select all"
        /&amp;gt;    
      ),
      cell: ({ row }) =&amp;gt; (
        &amp;lt;Checkbox
          checked={row.getIsSelected()}
          onCheckedChange={(value) =&amp;gt; row.toggleSelected(!!value)}
          aria-label="Select row"
        /&amp;gt;
      ),
      enableSorting: false,
      enableHiding: false,
    },
    {
      header: "ID",
      cell: ({ row }) =&amp;gt; &amp;lt;div&amp;gt;{row.index + 1}&amp;lt;/div&amp;gt;,
    },
    {
      accessorKey: "nama_customer",
      header: ({ column }) =&amp;gt; (
        &amp;lt;Button
          variant="ghost"
          onClick={() =&amp;gt; column.toggleSorting(column.getIsSorted() === "asc")}
        &amp;gt;
          Customer Name
          &amp;lt;ArrowUpDown className="ml-2 h-4 w-4" /&amp;gt;
        &amp;lt;/Button&amp;gt;
      ),
      cell: ({ row }) =&amp;gt; &amp;lt;div&amp;gt;{row.getValue("nama_customer")}&amp;lt;/div&amp;gt;,
    },
    {
      accessorKey: "tanggal_reservasi",
      header: ({ column }) =&amp;gt; (
        &amp;lt;Button
          variant="ghost"
          onClick={() =&amp;gt; column.toggleSorting(column.getIsSorted() === "asc")}
        &amp;gt;
          Date
          &amp;lt;ArrowUpDown className="ml-2 h-4 w-4" /&amp;gt;
        &amp;lt;/Button&amp;gt;
      ),
      cell: ({ row }) =&amp;gt; {
        const date = new Date(row.getValue("tanggal_reservasi"))
        return &amp;lt;div&amp;gt;{date.toLocaleDateString("en-GB")}&amp;lt;/div&amp;gt;
      },
    },
    {
      accessorKey: "status",
      header: "Status",
      cell: ({ row }) =&amp;gt; {
        const status = row.getValue("status") as string
        const statusClass = {
          booked: "text-blue-600",
          confirmed: "text-green-600",
          pending: "text-amber-600",
          cancelled: "text-red-600",
        }[status.toLowerCase()] || ""
        return &amp;lt;div className={`capitalize font-medium ${statusClass}`}&amp;gt;{status}&amp;lt;/div&amp;gt;
      },
    },
    {
      accessorKey: "id_meja",
      header: "Table No.",
      cell: ({ row }) =&amp;gt; &amp;lt;div&amp;gt;Table {row.getValue("id_meja")}&amp;lt;/div&amp;gt;,
    },
    {
      id: "actions",
      enableHiding: false,
      header: "Actions",
      cell: ({ row }) =&amp;gt; {
        const reservation = row.original
        return (
          &amp;lt;div className="flex space-x-2"&amp;gt;
            &amp;lt;Button
              variant="outline"
              size="sm"
              className="h-8 w-8 p-0"
              onClick={() =&amp;gt; handleUpdateClick(reservation)}
            &amp;gt;
              &amp;lt;Edit className="h-4 w-4" /&amp;gt;
            &amp;lt;/Button&amp;gt;
            &amp;lt;Button
              variant="outline"
              size="sm"
              className="h-8 w-8 p-0 text-red-500 hover:text-red-600 hover:bg-red-50"
              onClick={() =&amp;gt; handleDeleteClick(reservation)}
            &amp;gt;
              &amp;lt;Trash2 className="h-4 w-4" /&amp;gt;
            &amp;lt;/Button&amp;gt;
          &amp;lt;/div&amp;gt;
        )
      },
    },
  ]
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// data table

"use client";

import React, { use, useState } from "react";
import {
  ColumnFiltersState,
  SortingState,
  VisibilityState,
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from "@tanstack/react-table";
import { Input } from "@/components/ui/input";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { getReservationColumns } from "./column";
import { Button } from "@/components/ui/button";
import { PlusIcon } from "lucide-react";
import {
  Dialog,
  DialogContent,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { DialogFooter, DialogHeader } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { log } from "console";
import { Select, SelectContent, SelectItem } from "@/components/ui/select";
import { SelectTrigger, SelectValue } from "@radix-ui/react-select";

export type Reservation = {
  id_reservasi: number;
  nama_customer: string;
  tanggal_reservasi: string;
  status: string;
  id_meja: number;
  id_admin: number;
};

export function ReservationTable() {
  const [data, setData] = React.useState&amp;lt;Reservation[]&amp;gt;([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState&amp;lt;string | null&amp;gt;(null);
  const [sorting, setSorting] = React.useState&amp;lt;SortingState&amp;gt;([]);
  const [columnFilters, setColumnFilters] = React.useState&amp;lt;ColumnFiltersState&amp;gt;(
    []
  );
  const [columnVisibility, setColumnVisibility] =
    React.useState&amp;lt;VisibilityState&amp;gt;({});
  const [rowSelection, setRowSelection] = React.useState({});
  const [id_admin, setIdAdmin] = useState("");
  const [nama_customer, setNama] = useState("");
  const [tanggal_reservasi, setTanggal] = useState("");
  const [status, setStatus] = useState("");
  const [id_meja, setMeja] = useState("");

  const fetchReservations = async () =&amp;gt; {
    try {
      setLoading(true);
      const token = localStorage.getItem("adminToken");

      const response = await fetch("http://localhost:8000/api/reservasi/", {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
      });
      if (!response.ok) throw new Error("Failed to fetch reservation data");
      const result = await response.json();
      setData(Array.isArray(result) ? result : [result]);
    } catch (err) {
      setError("Failed to load reservations. Please try again later.");
    } finally {
      setLoading(false);
    }
  };

  React.useEffect(() =&amp;gt; {
    fetchReservations();
  }, []);

  const handleSubmit = async () =&amp;gt; {
    const formData = new FormData();
    formData.append("id_admin", id_admin);
    formData.append("nama_customer", nama_customer);
    formData.append("tanggal_reservasi", tanggal_reservasi);
    formData.append("status", status);
    formData.append("id_meja", id_meja);
    const token = localStorage.getItem("adminToken");

    try {
      const response = await fetch("http://127.0.0.1:8000/api/reservasi/", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
        },
        body: formData,
      });
      if (!response.ok) {
        throw new Error("Gagal menambahkan reservasi");
      }
      const data = await response.json();
      setData((prev) =&amp;gt; [...prev, data]);
      setIdAdmin("");
      setNama("");
      setStatus("");
      setTanggal("");
      setMeja("");
    } catch (err) {
      console.log("Error ", err);
    }
  };

  const columns = getReservationColumns(
    (r) =&amp;gt; console.log("Update", r),
    (r) =&amp;gt; console.log("Delete", r)
  );

  const table = useReactTable({
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    state: {
      sorting,
      columnFilters,
      columnVisibility,
      rowSelection,
    },
  });

  if (loading)
    return (
      &amp;lt;div className="flex justify-center items-center h-48"&amp;gt;
        Loading reservation data...
      &amp;lt;/div&amp;gt;
    );
  if (error)
    return (
      &amp;lt;div className="text-red-500 flex justify-center items-center h-48"&amp;gt;
        {error}
      &amp;lt;/div&amp;gt;
    );

  return (
    &amp;lt;div className="w-full"&amp;gt;
      &amp;lt;div className="flex items-center py-4"&amp;gt;
        &amp;lt;Input
          placeholder="Filter by customer name..."
          value={
            (table.getColumn("nama_customer")?.getFilterValue() as string) ?? ""
          }
          onChange={(e) =&amp;gt;
            table.getColumn("nama_customer")?.setFilterValue(e.target.value)
          }
          className="max-w-sm"
        /&amp;gt;
        &amp;lt;Dialog&amp;gt;
          &amp;lt;DialogTrigger asChild&amp;gt;
            &amp;lt;Button
              variant="outline"
              size="icon"
              className="w-[160px] ml-2 font-normal bg-gray-100"
            &amp;gt;
              &amp;lt;PlusIcon className="mr-2" /&amp;gt; Tambah Reservasi
            &amp;lt;/Button&amp;gt;
          &amp;lt;/DialogTrigger&amp;gt;
          &amp;lt;DialogContent&amp;gt;
            &amp;lt;DialogHeader&amp;gt;
              &amp;lt;DialogTitle&amp;gt;Tambahkan Reservasi&amp;lt;/DialogTitle&amp;gt;
            &amp;lt;/DialogHeader&amp;gt;
            &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
              &amp;lt;Label htmlFor="id_admin" className="text-right"&amp;gt;
                Id Admin
              &amp;lt;/Label&amp;gt;
              &amp;lt;Input
                id="id_admin"
                value={id_admin}
                onChange={(e) =&amp;gt; setIdAdmin(e.target.value)}
                className="col-span-3"
              /&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
              &amp;lt;Label htmlFor="meja" className="text-right"&amp;gt;
                Id Meja
              &amp;lt;/Label&amp;gt;
              &amp;lt;Input
                id="meja"
                value={id_meja}
                onChange={(e) =&amp;gt; setMeja(e.target.value)}
                className="col-span-3"
              /&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
              &amp;lt;Label htmlFor="nama" className="text-right"&amp;gt;
                Nama
              &amp;lt;/Label&amp;gt;
              &amp;lt;Input
                id="nama"
                value={nama_customer}
                onChange={(e) =&amp;gt; setNama(e.target.value)}
                className="col-span-3"
              /&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
              &amp;lt;Label htmlFor="status" className="text-right"&amp;gt;
                Status
              &amp;lt;/Label&amp;gt;
              &amp;lt;Select value={status} onValueChange={setStatus}&amp;gt;
                &amp;lt;SelectTrigger
                  id="status"
                  className="col-span-3 border border-input bg-background rounded-md px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
                &amp;gt; 
                  &amp;lt;SelectValue placeholder="Pilih status" /&amp;gt;
                &amp;lt;/SelectTrigger&amp;gt;
                &amp;lt;SelectContent&amp;gt;
                  &amp;lt;SelectItem value="Pending"&amp;gt;Pending&amp;lt;/SelectItem&amp;gt;
                  &amp;lt;SelectItem value="Booked"&amp;gt;Booked&amp;lt;/SelectItem&amp;gt;
                  &amp;lt;SelectItem value="Success"&amp;gt;Success&amp;lt;/SelectItem&amp;gt;
                  &amp;lt;SelectItem value="Cancelled"&amp;gt;Cancelled&amp;lt;/SelectItem&amp;gt;
                &amp;lt;/SelectContent&amp;gt;
              &amp;lt;/Select&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div className="grid grid-cols-4 items-center gap-4"&amp;gt;
              &amp;lt;Label htmlFor="tanggal" className="text-right"&amp;gt;
                Tanggal
              &amp;lt;/Label&amp;gt;
              &amp;lt;Input
                id="tanggal_reservasi"
                value={tanggal_reservasi}
                onChange={(e) =&amp;gt; setTanggal(e.target.value)}
                className="col-span-3"
                type="date"
              /&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;DialogFooter&amp;gt;
              &amp;lt;Button onClick={handleSubmit}&amp;gt;Simpan&amp;lt;/Button&amp;gt;
            &amp;lt;/DialogFooter&amp;gt;
          &amp;lt;/DialogContent&amp;gt;
        &amp;lt;/Dialog&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div className="rounded-md border"&amp;gt;
        &amp;lt;Table&amp;gt;
          &amp;lt;TableHeader&amp;gt;
            {table.getHeaderGroups().map((headerGroup) =&amp;gt; (
              &amp;lt;TableRow key={headerGroup.id}&amp;gt;
                {headerGroup.headers.map((header) =&amp;gt; (
                  &amp;lt;TableHead key={header.id}&amp;gt;
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                  &amp;lt;/TableHead&amp;gt;
                ))}
              &amp;lt;/TableRow&amp;gt;
            ))}
          &amp;lt;/TableHeader&amp;gt;

          &amp;lt;TableBody&amp;gt;
            {table.getRowModel().rows?.length ? (
              table.getRowModel().rows.map((row) =&amp;gt; (
                &amp;lt;TableRow
                  key={row.id}
                  data-state={row.getIsSelected() &amp;amp;&amp;amp; "selected"}
                &amp;gt;
                  {row.getVisibleCells().map((cell) =&amp;gt; (
                    &amp;lt;TableCell key={cell.id}&amp;gt;
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext()
                      )}
                    &amp;lt;/TableCell&amp;gt;
                  ))}
                &amp;lt;/TableRow&amp;gt;
              ))
            ) : (
              &amp;lt;TableRow&amp;gt;
                &amp;lt;TableCell
                  colSpan={columns.length}
                  className="h-24 text-center"
                &amp;gt;
                  No results.
                &amp;lt;/TableCell&amp;gt;
              &amp;lt;/TableRow&amp;gt;
            )}
          &amp;lt;/TableBody&amp;gt;
        &amp;lt;/Table&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Report
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'use client'

import ProductCard from "@/components/reportcard"
import { DataTableDemo } from "./table"

export default function Report(){
    return(
        &amp;lt;div&amp;gt;
            &amp;lt;DataTableDemo/&amp;gt;
            &amp;lt;div&amp;gt;
            &amp;lt;ProductCard/&amp;gt;

            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    )
}

// table.tsx
"use client"

import React, { useEffect, useState } from "react"
import {
  ColumnDef,
  ColumnFiltersState,
  SortingState,
  VisibilityState,
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from "@tanstack/react-table"
import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

// 1. Tipe data
export type Payment = {
  id: string
  nama_customer: string
  grand_total: number
  status_pembayaran: "Pending" | "Processing" | "Success" | "Failed"
}

// 2. Kolom tetap sama
export const columns: ColumnDef&amp;lt;Payment&amp;gt;[] = [
  {
    id: "select",
    header: ({ table }) =&amp;gt; (
      &amp;lt;Checkbox
        checked={
          table.getIsAllPageRowsSelected() ||
          (table.getIsSomePageRowsSelected() &amp;amp;&amp;amp; "indeterminate")
        }
        onCheckedChange={(value) =&amp;gt; table.toggleAllPageRowsSelected(!!value)}
        aria-label="Select all"
      /&amp;gt;
    ),
    cell: ({ row }) =&amp;gt; (
      &amp;lt;Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) =&amp;gt; row.toggleSelected(!!value)}
        aria-label="Select row"
      /&amp;gt;
    ),
    enableSorting: false,
    enableHiding: false,
  },
  {
    accessorKey: "status_pembayaran",
    header: "Status",
    cell: ({ row }) =&amp;gt; (
      &amp;lt;div className="capitalize"&amp;gt;{row.getValue("status_pembayaran")}&amp;lt;/div&amp;gt;
    ),
  },
  {
    accessorKey: "nama_customer",
    header: ({ column }) =&amp;gt; (
      &amp;lt;Button
        variant="ghost"
        onClick={() =&amp;gt; column.toggleSorting(column.getIsSorted() === "asc")}
      &amp;gt;
        Nama Customer
        &amp;lt;ArrowUpDown className="ml-2 h-4 w-4" /&amp;gt;
      &amp;lt;/Button&amp;gt;
    ),
    cell: ({ row }) =&amp;gt; &amp;lt;div className="lowercase"&amp;gt;{row.getValue("nama_customer")}&amp;lt;/div&amp;gt;,
  },
  {
    accessorKey: "grand_total",
    header: () =&amp;gt; &amp;lt;div className="text-right"&amp;gt;Amount&amp;lt;/div&amp;gt;,
    cell: ({ row }) =&amp;gt; {
      const amount = parseFloat(row.getValue("grand_total"))

      return &amp;lt;div className="text-right font-medium"&amp;gt;{amount}&amp;lt;/div&amp;gt;
    },
  },
  {
    id: "actions",
    enableHiding: false,
    cell: ({ row }) =&amp;gt; {
      const payment = row.original
      return (
        &amp;lt;DropdownMenu&amp;gt;
          &amp;lt;DropdownMenuTrigger asChild&amp;gt;
            &amp;lt;Button variant="ghost" className="h-8 w-8 p-0"&amp;gt;
              &amp;lt;span className="sr-only"&amp;gt;Open menu&amp;lt;/span&amp;gt;
              &amp;lt;MoreHorizontal /&amp;gt;
            &amp;lt;/Button&amp;gt;
          &amp;lt;/DropdownMenuTrigger&amp;gt;
          &amp;lt;DropdownMenuContent align="end"&amp;gt;
            &amp;lt;DropdownMenuLabel&amp;gt;Actions&amp;lt;/DropdownMenuLabel&amp;gt;
            &amp;lt;DropdownMenuItem
              onClick={() =&amp;gt; navigator.clipboard.writeText(payment.id)}
            &amp;gt;
              Copy payment ID
            &amp;lt;/DropdownMenuItem&amp;gt;
            &amp;lt;DropdownMenuSeparator /&amp;gt;
            &amp;lt;DropdownMenuItem&amp;gt;View customer&amp;lt;/DropdownMenuItem&amp;gt;
            &amp;lt;DropdownMenuItem&amp;gt;View payment details&amp;lt;/DropdownMenuItem&amp;gt;
          &amp;lt;/DropdownMenuContent&amp;gt;
        &amp;lt;/DropdownMenu&amp;gt;
      )
    },
  },
]

// 3. Komponen utama
export function DataTableDemo() {
  const [data, setData] = useState&amp;lt;Payment[]&amp;gt;([])
  const [loading, setLoading] = useState(true)

  const [sorting, setSorting] = useState&amp;lt;SortingState&amp;gt;([])
  const [columnFilters, setColumnFilters] = useState&amp;lt;ColumnFiltersState&amp;gt;([])
  const [columnVisibility, setColumnVisibility] = useState&amp;lt;VisibilityState&amp;gt;({})
  const [rowSelection, setRowSelection] = useState({})

  // 4. Fetch API saat mount
  useEffect(() =&amp;gt; {
    const fetchData = async () =&amp;gt; {
        const token = localStorage.getItem("adminToken")
      try {
        const res = await fetch("http://127.0.0.1:8000/api/report/",{
            headers:{
                Authorization : `Bearer ${token}`
            }
        })
        const json = await res.json()
        setData(json)
      } catch (error) {
        console.error("Failed to fetch data", error)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [])

  const table = useReactTable({
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    state: {
      sorting,
      columnFilters,
      columnVisibility,
      rowSelection,
    },
  })

  return (
    &amp;lt;div className="w-full"&amp;gt;
      &amp;lt;div className="flex items-center py-4"&amp;gt;
        &amp;lt;Input
          placeholder="Filter emails..."
          value={(table.getColumn("nama_customer")?.getFilterValue() as string) ?? ""}
          onChange={(event) =&amp;gt;
            table.getColumn("nama_customer")?.setFilterValue(event.target.value)
          }
          className="max-w-sm"
        /&amp;gt;
        &amp;lt;DropdownMenu&amp;gt;
          &amp;lt;DropdownMenuTrigger asChild&amp;gt;
            &amp;lt;Button variant="outline" className="ml-auto"&amp;gt;
              Columns &amp;lt;ChevronDown /&amp;gt;
            &amp;lt;/Button&amp;gt;
          &amp;lt;/DropdownMenuTrigger&amp;gt;
          &amp;lt;DropdownMenuContent align="end"&amp;gt;
            {table
              .getAllColumns()
              .filter((column) =&amp;gt; column.getCanHide())
              .map((column) =&amp;gt; (
                &amp;lt;DropdownMenuCheckboxItem
                  key={column.id}
                  className="capitalize"
                  checked={column.getIsVisible()}
                  onCheckedChange={(value) =&amp;gt;
                    column.toggleVisibility(!!value)
                  }
                &amp;gt;
                  {column.id}
                &amp;lt;/DropdownMenuCheckboxItem&amp;gt;
              ))}
          &amp;lt;/DropdownMenuContent&amp;gt;
        &amp;lt;/DropdownMenu&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className="rounded-md border"&amp;gt;
        &amp;lt;Table&amp;gt;
          &amp;lt;TableHeader&amp;gt;
            {table.getHeaderGroups().map((headerGroup) =&amp;gt; (
              &amp;lt;TableRow key={headerGroup.id}&amp;gt;
                {headerGroup.headers.map((header) =&amp;gt; (
                  &amp;lt;TableHead key={header.id}&amp;gt;
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                  &amp;lt;/TableHead&amp;gt;
                ))}
              &amp;lt;/TableRow&amp;gt;
            ))}
          &amp;lt;/TableHeader&amp;gt;
          &amp;lt;TableBody&amp;gt;
            {loading ? (
              &amp;lt;TableRow&amp;gt;
                &amp;lt;TableCell colSpan={columns.length} className="h-24 text-center"&amp;gt;
                  Loading...
                &amp;lt;/TableCell&amp;gt;
              &amp;lt;/TableRow&amp;gt;
            ) : table.getRowModel().rows?.length ? (
              table.getRowModel().rows.map((row) =&amp;gt; (
                &amp;lt;TableRow
                  key={row.id}
                  data-state={row.getIsSelected() &amp;amp;&amp;amp; "selected"}
                &amp;gt;
                  {row.getVisibleCells().map((cell) =&amp;gt; (
                    &amp;lt;TableCell key={cell.id}&amp;gt;
                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
                    &amp;lt;/TableCell&amp;gt;
                  ))}
                &amp;lt;/TableRow&amp;gt;
              ))
            ) : (
              &amp;lt;TableRow&amp;gt;
                &amp;lt;TableCell
                  colSpan={columns.length}
                  className="h-24 text-center"
                &amp;gt;
                  No results.
                &amp;lt;/TableCell&amp;gt;
              &amp;lt;/TableRow&amp;gt;
            )}
          &amp;lt;/TableBody&amp;gt;
        &amp;lt;/Table&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;div className="flex items-center justify-end space-x-2 py-4"&amp;gt;
        &amp;lt;div className="flex-1 text-sm text-muted-foreground"&amp;gt;
          {table.getFilteredSelectedRowModel().rows.length} of{" "}
          {table.getFilteredRowModel().rows.length} row(s) selected.
        &amp;lt;/div&amp;gt;
        &amp;lt;div className="space-x-2"&amp;gt;
          &amp;lt;Button
            variant="outline"
            size="sm"
            onClick={() =&amp;gt; table.previousPage()}
            disabled={!table.getCanPreviousPage()}
          &amp;gt;
            Previous
          &amp;lt;/Button&amp;gt;
          &amp;lt;Button
            variant="outline"
            size="sm"
            onClick={() =&amp;gt; table.nextPage()}
            disabled={!table.getCanNextPage()}
          &amp;gt;
            Next
          &amp;lt;/Button&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>nextjs</category>
    </item>
    <item>
      <title>NextJS with ShadCN build Web for Consume API?</title>
      <dc:creator>Vytroo</dc:creator>
      <pubDate>Mon, 07 Apr 2025 14:13:37 +0000</pubDate>
      <link>https://dev.to/vytroo_e77e89f49d4188b8fa/nextjs-with-shadcn-build-web-for-consume-api-1jph</link>
      <guid>https://dev.to/vytroo_e77e89f49d4188b8fa/nextjs-with-shadcn-build-web-for-consume-api-1jph</guid>
      <description>&lt;p&gt;install nextjs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx create-next-app@latest

# install shadcn and pnpm

npm i -g pnpm

pnpm dlx shadcn@latest init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;install sidebar default layout for dashboard&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pnpm dlx shadcn@latest add sidebar&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return (
    &amp;lt;SidebarProvider&amp;gt;
      &amp;lt;AppSidebar /&amp;gt;
      &amp;lt;SidebarInset className="m-5"&amp;gt;
        &amp;lt;Separator
          orientation="vertical"
          className="mr-2 data-[orientation=vertical]:h-4"
        /&amp;gt;
        &amp;lt;SidebarTrigger className="-ml-1" /&amp;gt;
        &amp;lt;Separator
          orientation="vertical"
          className="mr-2 data-[orientation=vertical]:h-4"
        /&amp;gt;
        {children}
      &amp;lt;/SidebarInset&amp;gt;
    &amp;lt;/SidebarProvider&amp;gt;
  );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;build the landing page&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ArrowUpRight, CirclePlay } from "lucide-react";
import React from "react";
import Login02Page from "./login";

const Hero04 = () =&amp;gt; {
  return (
    &amp;lt;div className="min-h-screen flex items-center justify-center overflow-hidden"&amp;gt;
      &amp;lt;div className="max-w-screen-xl w-full mx-auto grid lg:grid-cols-2 gap-12 px-6 py-12 lg:py-0"&amp;gt;
        &amp;lt;div className="my-auto"&amp;gt;
          &amp;lt;Badge className="bg-gradient-to-br via-70% from-primary via-muted/30 to-primary rounded-full py-1 border-none"&amp;gt;
            Just released v1.0.0
          &amp;lt;/Badge&amp;gt;
          &amp;lt;h1 className="mt-6 max-w-[17ch] text-4xl md:text-5xl lg:text-[2.75rem] xl:text-5xl font-bold !leading-[1.2] tracking-tight"&amp;gt;
            Customized Shadcn UI Blocks &amp;amp; Components
          &amp;lt;/h1&amp;gt;
          &amp;lt;p className="mt-6 max-w-[60ch] text-lg"&amp;gt;
            Explore a collection of Shadcn UI blocks and components, ready to
            preview and copy. Streamline your development workflow with
            easy-to-implement examples.
          &amp;lt;/p&amp;gt;
          &amp;lt;div className="mt-12 flex items-center gap-4"&amp;gt;
            &amp;lt;Button size="lg" className="rounded-full text-base"&amp;gt;
              Get Started &amp;lt;ArrowUpRight className="!h-5 !w-5" /&amp;gt;
            &amp;lt;/Button&amp;gt;
            &amp;lt;Button
              variant="outline"
              size="lg"
              className="rounded-full text-base shadow-none"
            &amp;gt;
              &amp;lt;CirclePlay className="!h-5 !w-5" /&amp;gt; Watch Demo
            &amp;lt;/Button&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div className="w-full aspect-video lg:aspect-auto lg:w-[1000px] lg:h-[calc(100vh-4rem)] bg-accent rounded-xl" &amp;gt;
          &amp;lt;Login02Page/&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Hero04;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;login.tsx&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

const formSchema = z.object({
  username: z.string(),
  password: z.string().min(5, "Password must be at least 8 characters long"),
});

const Login02Page = () =&amp;gt; {
  const [errorMessage,setErrorMessage]=useState("")
  const router = useRouter()
  const form = useForm&amp;lt;z.infer&amp;lt;typeof formSchema&amp;gt;&amp;gt;({
    defaultValues: {
      username: "",
      password: "",
    },
    resolver: zodResolver(formSchema),
  });

  const onSubmit = async (data: z.infer&amp;lt;typeof formSchema&amp;gt;) =&amp;gt; {
    setErrorMessage("")
    try{
      const response = await fetch('http://127.0.0.1:8000/api/login/',{
        method:'POST',
        headers:{'Content-Type':'application/json'},
        credentials: "include",
        body:JSON.stringify(data)
      })
      const result = await response.json()
      if(!response.ok){
        throw new Error(result.message || "Login Failed")
      }
      localStorage.setItem("adminToken",result.access)
      router.push("/menu")
      console.log(result.access );

    }
    catch(err:any){
      setErrorMessage(err.message)
    }


  };

  return (
    &amp;lt;div className="min-h-screen flex items-center justify-center"&amp;gt;
      &amp;lt;div className="max-w-sm w-full flex flex-col items-center border rounded-lg p-6 shadow-sm"&amp;gt;
        &amp;lt;p className="mt-4 mb-10 text-xl font-bold tracking-tight"&amp;gt;
          Log in to Shadcn UI Blocks
        &amp;lt;/p&amp;gt;



        &amp;lt;Form {...form}&amp;gt;
          &amp;lt;form
            className="w-full space-y-4"
            onSubmit={form.handleSubmit(onSubmit)}
          &amp;gt;
            &amp;lt;FormField
              control={form.control}
              name="username"
              render={({ field }) =&amp;gt; (
                &amp;lt;FormItem&amp;gt;
                  &amp;lt;FormLabel&amp;gt;Username&amp;lt;/FormLabel&amp;gt;
                  &amp;lt;FormControl&amp;gt;
                    &amp;lt;Input
                      type="text"
                      placeholder="Username"
                      className="w-full"
                      {...field}
                    /&amp;gt;
                  &amp;lt;/FormControl&amp;gt;
                  &amp;lt;FormMessage /&amp;gt;
                &amp;lt;/FormItem&amp;gt;
              )}
            /&amp;gt;
            &amp;lt;FormField
              control={form.control}
              name="password"
              render={({ field }) =&amp;gt; (
                &amp;lt;FormItem&amp;gt;
                  &amp;lt;FormLabel&amp;gt;Password&amp;lt;/FormLabel&amp;gt;
                  &amp;lt;FormControl&amp;gt;
                    &amp;lt;Input
                      type="password"
                      placeholder="Password"
                      className="w-full"
                      {...field}
                    /&amp;gt;
                  &amp;lt;/FormControl&amp;gt;
                  &amp;lt;FormMessage /&amp;gt;
                &amp;lt;/FormItem&amp;gt;
              )}
            /&amp;gt;
            &amp;lt;Button type="submit" className="mt-4 w-full"&amp;gt;
              Login
             &amp;lt;/Button&amp;gt;
          &amp;lt;/form&amp;gt;
        &amp;lt;/Form&amp;gt;

        &amp;lt;div className="mt-5 space-y-5"&amp;gt;
          &amp;lt;Link
            href="#"
            className="text-sm block underline text-muted-foreground text-center"
          &amp;gt;
            Forgot your password?
          &amp;lt;/Link&amp;gt;
          &amp;lt;p className="text-sm text-center"&amp;gt;
            Don&amp;amp;apos;t have an account?
            &amp;lt;Link href="#" className="ml-1 underline text-muted-foreground"&amp;gt;
              Create account
            &amp;lt;/Link&amp;gt;
          &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};



export default Login02Page;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>nextjs</category>
    </item>
    <item>
      <title>Validasi Error &amp; datepicker</title>
      <dc:creator>Vytroo</dc:creator>
      <pubDate>Mon, 07 Apr 2025 14:02:55 +0000</pubDate>
      <link>https://dev.to/vytroo_e77e89f49d4188b8fa/rest-api-crud-with-django-ep7</link>
      <guid>https://dev.to/vytroo_e77e89f49d4188b8fa/rest-api-crud-with-django-ep7</guid>
      <description>&lt;p&gt;validation&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User = get_user_model()

class LoginSerializer(serializers.Serializer):
    email = serializers.EmailField()
    password = serializers.CharField(write_only=True)

    def validate(self, data):
        email = data.get('email')
        password = data.get('password')

        try:
            user = User.objects.get(email=email)
        except User.DoesNotExist:
            raise serializers.ValidationError("Email tidak ditemukan")

        if not user.check_password(password):
            raise serializers.ValidationError("Password salah")

        refresh = RefreshToken.for_user(user)

        return {
            'refresh': str(refresh),
            'access': str(refresh.access_token),
            'user': UserSerializer(user).data
        }



register=
@api_view(['POST'])
def register(request):
    try:
        username = request.data.get('username')
        password = request.data.get('password')
        email = request.data.get('email')
        no_hp = request.data.get('no_hp')
        alamat = request.data.get('alamat')

        if User.objects.filter(email=email).exists():
            return Response({'error': 'Email sudah terdaftar'}, status=400)

        if User.objects.filter(username=username).exists():
            return Response({'error': 'Username sudah terdaftar'}, status=400)

        user = User.objects.create_user(
            username=username,
            email=email,
            no_hp=no_hp,
            alamat=alamat
        )
        user.set_password(password)
        user.save()

        refresh = RefreshToken.for_user(user)
        return Response({
            'refresh': str(refresh),
            'access': str(refresh.access_token),
            'user': {
                'id': user.id,
                'username': user.username,
                'email': user.email,
                'status': user.status
            }
        })

    except Exception as e:
        return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "sonner"; // Import Sonner

const formSchema = z.object({
  username: z.string(),
  password: z.string().min(5, "Password must be at least 5 characters long"),
});

const Login02Page = () =&amp;gt; {
  const [errorMessage, setErrorMessage] = useState("");
  const router = useRouter();
  const form = useForm&amp;lt;z.infer&amp;lt;typeof formSchema&amp;gt;&amp;gt;({
    defaultValues: {
      username: "",
      password: "",
    },
    resolver: zodResolver(formSchema),
  });

  const onSubmit = async (data: z.infer&amp;lt;typeof formSchema&amp;gt;) =&amp;gt; {
    setErrorMessage("");
    try {
      const response = await fetch("http://127.0.0.1:8000/api/login/", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      const result = await response.json();

      if (response.ok) {
        localStorage.setItem("adminToken", result.access);
        toast.success("Login successful!"); // Success toast
        router.push("/menu");
      } else {
        // Handle different error cases
        if (result.detail) {
          toast.error(result.detail); // Error toast from API
        } else {
          toast.error("Failed to login. Please check your credentials."); // Generic error
        }
      }
    } catch (err: any) {
      toast.error(err.message || "An unexpected error occurred"); // Network error
    }
  };

  return (
    &amp;lt;div className="min-h-screen flex items-center justify-center"&amp;gt;
      &amp;lt;div className="max-w-sm w-full flex flex-col items-center border rounded-lg p-6 shadow-sm"&amp;gt;
        &amp;lt;p className="mt-4 mb-10 text-xl font-bold tracking-tight"&amp;gt;
          Log in to Restoran SMK
        &amp;lt;/p&amp;gt;

        &amp;lt;Form {...form}&amp;gt;
          &amp;lt;form
            className="w-full space-y-4"
            onSubmit={form.handleSubmit(onSubmit)}
          &amp;gt;
            &amp;lt;FormField
              control={form.control}
              name="username"
              render={({ field }) =&amp;gt; (
                &amp;lt;FormItem&amp;gt;
                  &amp;lt;FormLabel&amp;gt;Username&amp;lt;/FormLabel&amp;gt;
                  &amp;lt;FormControl&amp;gt;
                    &amp;lt;Input
                      type="text"
                      placeholder="Username"
                      className="w-full"
                      {...field}
                    /&amp;gt;
                  &amp;lt;/FormControl&amp;gt;
                  &amp;lt;FormMessage /&amp;gt;
                &amp;lt;/FormItem&amp;gt;
              )}
            /&amp;gt;
            &amp;lt;FormField
              control={form.control}
              name="password"
              render={({ field }) =&amp;gt; (
                &amp;lt;FormItem&amp;gt;
                  &amp;lt;FormLabel&amp;gt;Password&amp;lt;/FormLabel&amp;gt;
                  &amp;lt;FormControl&amp;gt;
                    &amp;lt;Input
                      type="password"
                      placeholder="Password"
                      className="w-full"
                      {...field}
                    /&amp;gt;
                  &amp;lt;/FormControl&amp;gt;
                  &amp;lt;FormMessage /&amp;gt;
                &amp;lt;/FormItem&amp;gt;
              )}
            /&amp;gt;
            &amp;lt;Button type="submit" className="mt-4 w-full"&amp;gt;
              Login
            &amp;lt;/Button&amp;gt;
          &amp;lt;/form&amp;gt;
        &amp;lt;/Form&amp;gt;

        &amp;lt;div className="mt-5 space-y-5"&amp;gt;
          &amp;lt;Link
            href="#"
            className="text-sm block underline text-muted-foreground text-center"
          &amp;gt;
            Forgot your password?
          &amp;lt;/Link&amp;gt;
          &amp;lt;p className="text-sm text-center"&amp;gt;
            Don&amp;amp;apos;t have an account?
            &amp;lt;Link href="#" className="ml-1 underline text-muted-foreground"&amp;gt;
              Create account
            &amp;lt;/Link&amp;gt;
          &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Login02Page;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;layout&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Toaster } from "sonner";

// Di dalam komponen Layout Anda:
return (
  &amp;lt;html&amp;gt;
    &amp;lt;body&amp;gt;
      {/* ... konten lainnya ... */}
      &amp;lt;Toaster richColors position="top-center" /&amp;gt;
    &amp;lt;/body&amp;gt;
  &amp;lt;/html&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# views.py
from django.db.models import Sum, Count, Case, When, Value, CharField
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.utils.dateparse import parse_date
from .models import DetailTransaksi, Transaksi
from datetime import datetime

@api_view(['GET'])
def top_5_menu_terlaris(request):
    try:
        # Tambahkan filter tanggal jika diperlukan
        start_date = request.GET.get('start_date')
        end_date = request.GET.get('end_date')

        queryset = DetailTransaksi.objects.all()

        if start_date and end_date:
            start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
            end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
            queryset = queryset.filter(
                transaksi__created_at__date__range=[start_date, end_date]
            )

        queryset = (
            queryset
            .values('menu__nama_menu')
            .annotate(total_dipesan=Sum('qty'))
            .order_by('-total_dipesan')[:5]
        )

        return Response({
            'success': True,
            'data': queryset,
            'message': 'Data top 5 menu berhasil diambil'
        })
    except Exception as e:
        return Response({
            'success': False,
            'error': str(e)
        }, status=400)

@api_view(['GET'])
def total_pendapatan(request):
    try:
        start_date = request.GET.get('start_date', None)
        end_date = request.GET.get('end_date', None)

        if not start_date or not end_date:
            return Response({
                'success': False,
                'error': 'Parameter start_date dan end_date diperlukan'
            }, status=400)

        start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
        end_date = datetime.strptime(end_date, '%Y-%m-%d').date()

        # Hitung total pendapatan dari transaksi lunas
        total_lunas = (
            Transaksi.objects
            .filter(kekurangan__lte=0, created_at__date__range=[start_date, end_date])
            .aggregate(total_pendapatan=Sum('total_bayar'))['total_pendapatan'] or 0
        )

        # Hitung laba dari transaksi yang kurang (jika ada)
        laba_transaksi_kurang = (
            Transaksi.objects
            .filter(kekurangan__gt=0, created_at__date__range=[start_date, end_date])
            .aggregate(total_laba=Sum('total_bayar') - Sum('kekurangan'))['total_laba'] or 0
        )

        total_pendapatan = total_lunas + laba_transaksi_kurang

        return Response({
            'success': True,
            'data': {
                'total_pendapatan': total_pendapatan,
                'detail': {
                    'pendapatan_lunas': total_lunas,
                    'laba_transaksi_kurang': laba_transaksi_kurang
                }
            }
        })
    except Exception as e:
        return Response({
            'success': False,
            'error': str(e)
        }, status=400)

@api_view(['GET'])
def ringkasan_status_pembayaran(request):
    try:
        start_date = request.GET.get('start_date', None)
        end_date = request.GET.get('end_date', None)

        if not start_date or not end_date:
            return Response({
                'success': False,
                'error': 'Parameter start_date dan end_date diperlukan'
            }, status=400)

        start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
        end_date = datetime.strptime(end_date, '%Y-%m-%d').date()

        queryset = (
            Transaksi.objects
            .filter(created_at__date__range=[start_date, end_date])
            .annotate(
                payment_status=Case(
                    When(status_pembayaran__in=['settlement', 'paid'], then=Value('Paid')),
                    When(status_pembayaran__in=['pending', 'unpaid'], then=Value('Unpaid')),
                    default=Value('Other'),
                    output_field=CharField(),
                )
            )
            .values('payment_status')
            .annotate(jumlah_transaksi=Count('id'))
            .order_by('payment_status')
        )

        return Response({
            'success': True,
            'data': queryset,
            'periode': f"{start_date} hingga {end_date}"
        })
    except Exception as e:
        return Response({
            'success': False,
            'error': str(e)
        }, status=400)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;reporting&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @tanstack/react-table date-fns


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;/app/reports/page.tsx&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/app/api/reports/route.ts

typescript
Copy
import { NextResponse } from 'next/server';

const API_BASE_URL = 'http://localhost:8000'; // Sesuaikan dengan URL Django Anda

export async function GET() {
  const startDate = '2025-04-10';
  const endDate = '2025-04-30';

  try {
    // Fetch data paralel dari semua endpoint
    const [topMenuRes, pendapatanRes, statusRes] = await Promise.all([
      fetch(`${API_BASE_URL}/report/top-menu/?start_date=${startDate}&amp;amp;end_date=${endDate}`),
      fetch(`${API_BASE_URL}/report/pendapatan/?start_date=${startDate}&amp;amp;end_date=${endDate}`),
      fetch(`${API_BASE_URL}/report/status-pembayaran/?start_date=${startDate}&amp;amp;end_date=${endDate}`)
    ]);

    if (!topMenuRes.ok || !pendapatanRes.ok || !statusRes.ok) {
      throw new Error('Gagal mengambil data dari server');
    }

    const [topMenu, pendapatan, statusPembayaran] = await Promise.all([
      topMenuRes.json(),
      pendapatanRes.json(),
      statusRes.json()
    ]);

    return NextResponse.json({
      success: true,
      data: {
        topMenu: topMenu.data,
        pendapatan: pendapatan.data,
        statusPembayaran: statusPembayaran.data
      },
      periode: {
        startDate,
        endDate
      }
    });
  } catch (error) {
    return NextResponse.json({
      success: false,
      error: error instanceof Error ? error.message : 'Terjadi kesalahan'
    }, { status: 500 });
  }
}
2. Buat Komponen Report Page
/app/reports/page.tsx

tsx
Copy
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { format } from "date-fns";

async function getReportData() {
  const res = await fetch('http://localhost:3000/api/reports', {
    next: { revalidate: 3600 } // Revalidate setiap 1 jam
  });

  if (!res.ok) {
    throw new Error('Gagal mengambil data laporan');
  }

  return res.json();
}

export default async function ReportsPage() {
  const reportData = await getReportData();

  if (!reportData.success) {
    return &amp;lt;div&amp;gt;Error: {reportData.error}&amp;lt;/div&amp;gt;;
  }

  const { topMenu, pendapatan, statusPembayaran, periode } = reportData.data;
  const startDate = new Date(periode.startDate);
  const endDate = new Date(periode.endDate);

  return (
    &amp;lt;div className="container mx-auto py-8"&amp;gt;
      &amp;lt;h1 className="text-2xl font-bold mb-6"&amp;gt;Laporan Periode: {format(startDate, 'dd MMM yyyy')} - {format(endDate, 'dd MMM yyyy')}&amp;lt;/h1&amp;gt;

      &amp;lt;div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8"&amp;gt;
        &amp;lt;Card&amp;gt;
          &amp;lt;CardHeader&amp;gt;
            &amp;lt;CardTitle&amp;gt;Total Pendapatan&amp;lt;/CardTitle&amp;gt;
          &amp;lt;/CardHeader&amp;gt;
          &amp;lt;CardContent&amp;gt;
            &amp;lt;div className="text-3xl font-bold"&amp;gt;
              {new Intl.NumberFormat('id-ID', { 
                style: 'currency', 
                currency: 'IDR',
                minimumFractionDigits: 0
              }).format(pendapatan.total_pendapatan)}
            &amp;lt;/div&amp;gt;
            &amp;lt;div className="text-sm text-muted-foreground mt-2"&amp;gt;
              &amp;lt;p&amp;gt;Lunas: {new Intl.NumberFormat('id-ID', { 
                style: 'currency', 
                currency: 'IDR',
                minimumFractionDigits: 0
              }).format(pendapatan.detail.pendapatan_lunas)}&amp;lt;/p&amp;gt;
              &amp;lt;p&amp;gt;Laba Transaksi Kurang: {new Intl.NumberFormat('id-ID', { 
                style: 'currency', 
                currency: 'IDR',
                minimumFractionDigits: 0
              }).format(pendapatan.detail.laba_transaksi_kurang)}&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/CardContent&amp;gt;
        &amp;lt;/Card&amp;gt;

        &amp;lt;Card&amp;gt;
          &amp;lt;CardHeader&amp;gt;
            &amp;lt;CardTitle&amp;gt;Status Pembayaran&amp;lt;/CardTitle&amp;gt;
          &amp;lt;/CardHeader&amp;gt;
          &amp;lt;CardContent&amp;gt;
            &amp;lt;div className="space-y-2"&amp;gt;
              {statusPembayaran.map((item: any) =&amp;gt; (
                &amp;lt;div key={item.payment_status} className="flex justify-between"&amp;gt;
                  &amp;lt;span&amp;gt;{item.payment_status}:&amp;lt;/span&amp;gt;
                  &amp;lt;span&amp;gt;{item.jumlah_transaksi} transaksi&amp;lt;/span&amp;gt;
                &amp;lt;/div&amp;gt;
              ))}
            &amp;lt;/div&amp;gt;
          &amp;lt;/CardContent&amp;gt;
        &amp;lt;/Card&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;Card&amp;gt;
        &amp;lt;CardHeader&amp;gt;
          &amp;lt;CardTitle&amp;gt;5 Menu Terlaris&amp;lt;/CardTitle&amp;gt;
        &amp;lt;/CardHeader&amp;gt;
        &amp;lt;CardContent&amp;gt;
          &amp;lt;Table&amp;gt;
            &amp;lt;TableHeader&amp;gt;
              &amp;lt;TableRow&amp;gt;
                &amp;lt;TableHead&amp;gt;No&amp;lt;/TableHead&amp;gt;
                &amp;lt;TableHead&amp;gt;Nama Menu&amp;lt;/TableHead&amp;gt;
                &amp;lt;TableHead className="text-right"&amp;gt;Total Dipesan&amp;lt;/TableHead&amp;gt;
              &amp;lt;/TableRow&amp;gt;
            &amp;lt;/TableHeader&amp;gt;
            &amp;lt;TableBody&amp;gt;
              {topMenu.map((menu: any, index: number) =&amp;gt; (
                &amp;lt;TableRow key={menu.nama_menu || index}&amp;gt;
                  &amp;lt;TableCell&amp;gt;{index + 1}&amp;lt;/TableCell&amp;gt;
                  &amp;lt;TableCell&amp;gt;{menu.nama_menu || 'Menu Tidak Diketahui'}&amp;lt;/TableCell&amp;gt;
                  &amp;lt;TableCell className="text-right"&amp;gt;{menu.total_dipesan || 0}&amp;lt;/TableCell&amp;gt;
                &amp;lt;/TableRow&amp;gt;
              ))}
            &amp;lt;/TableBody&amp;gt;
          &amp;lt;/Table&amp;gt;
        &amp;lt;/CardContent&amp;gt;
      &amp;lt;/Card&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
3. Tambahkan Loading State (Optional)
Buat file /app/reports/loading.tsx untuk menampilkan skeleton loader:

tsx
Copy
import { Skeleton } from "@/components/ui/skeleton";

export default function Loading() {
  return (
    &amp;lt;div className="container mx-auto py-8 space-y-6"&amp;gt;
      &amp;lt;Skeleton className="h-8 w-64" /&amp;gt;

      &amp;lt;div className="grid grid-cols-1 md:grid-cols-2 gap-6"&amp;gt;
        &amp;lt;Skeleton className="h-40" /&amp;gt;
        &amp;lt;Skeleton className="h-40" /&amp;gt;
      &amp;lt;/div&amp;gt;

      &amp;lt;Skeleton className="h-96" /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
4. Error Handling (Optional)
Buat file /app/reports/error.tsx untuk menangani error:

tsx
Copy
'use client';

import { useEffect } from "react";
import { Button } from "@/components/ui/button";

export default function Error({
  error,
  reset,
}: {
  error: Error &amp;amp; { digest?: string };
  reset: () =&amp;gt; void;
}) {
  useEffect(() =&amp;gt; {
    console.error(error);
  }, [error]);

  return (
    &amp;lt;div className="container mx-auto py-8 text-center"&amp;gt;
      &amp;lt;h2 className="text-xl font-bold mb-4"&amp;gt;Gagal memuat laporan&amp;lt;/h2&amp;gt;
      &amp;lt;p className="text-destructive mb-4"&amp;gt;{error.message}&amp;lt;/p&amp;gt;
      &amp;lt;Button onClick={() =&amp;gt; reset()}&amp;gt;Coba Lagi&amp;lt;/Button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;jalan terakhir&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'use client';

import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { format } from "date-fns";
import { useEffect, useState } from "react";

export default function TopMenuPage() {
  const [data, setData] = useState&amp;lt;any[]&amp;gt;([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState&amp;lt;string | null&amp;gt;(null);

  const startDate = '2025-04-10';
  const endDate = '2025-04-30';

  useEffect(() =&amp;gt; {
    const fetchData = async () =&amp;gt; {
      try {
        setLoading(true);
        const response = await fetch(
          `http://localhost:8000/report/top-menu/?start_date=${startDate}&amp;amp;end_date=${endDate}`
        );

        if (!response.ok) {
          throw new Error('Failed to fetch top menu data');
        }

        const result = await response.json();

        if (result.success) {
          setData(result.data || []);
        } else {
          throw new Error(result.error || 'Unknown error occurred');
        }
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An unknown error occurred');
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  if (loading) {
    return (
      &amp;lt;div className="container mx-auto py-8"&amp;gt;
        &amp;lt;Card&amp;gt;
          &amp;lt;CardHeader&amp;gt;
            &amp;lt;CardTitle&amp;gt;5 Menu Terlaris&amp;lt;/CardTitle&amp;gt;
            &amp;lt;p className="text-sm text-muted-foreground"&amp;gt;
              Periode: {format(new Date(startDate), 'dd MMM yyyy')} - {format(new Date(endDate), 'dd MMM yyyy')}
            &amp;lt;/p&amp;gt;
          &amp;lt;/CardHeader&amp;gt;
          &amp;lt;CardContent&amp;gt;
            &amp;lt;div className="space-y-4"&amp;gt;
              {[...Array(5)].map((_, i) =&amp;gt; (
                &amp;lt;div key={i} className="h-12 bg-muted/50 rounded animate-pulse"&amp;gt;&amp;lt;/div&amp;gt;
              ))}
            &amp;lt;/div&amp;gt;
          &amp;lt;/CardContent&amp;gt;
        &amp;lt;/Card&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }

  if (error) {
    return (
      &amp;lt;div className="container mx-auto py-8 text-center"&amp;gt;
        &amp;lt;Card className="bg-destructive/10 border-destructive"&amp;gt;
          &amp;lt;CardHeader&amp;gt;
            &amp;lt;CardTitle className="text-destructive"&amp;gt;Error&amp;lt;/CardTitle&amp;gt;
          &amp;lt;/CardHeader&amp;gt;
          &amp;lt;CardContent&amp;gt;
            &amp;lt;p&amp;gt;{error}&amp;lt;/p&amp;gt;
            &amp;lt;button 
              onClick={() =&amp;gt; window.location.reload()}
              className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded hover:bg-primary/90"
            &amp;gt;
              Coba Lagi
            &amp;lt;/button&amp;gt;
          &amp;lt;/CardContent&amp;gt;
        &amp;lt;/Card&amp;gt;
      &amp;lt;/div&amp;gt;
    );
  }

  return (
    &amp;lt;div className="container mx-auto py-8"&amp;gt;
      &amp;lt;Card&amp;gt;
        &amp;lt;CardHeader&amp;gt;
          &amp;lt;CardTitle&amp;gt;5 Menu Terlaris&amp;lt;/CardTitle&amp;gt;
          &amp;lt;p className="text-sm text-muted-foreground"&amp;gt;
            Periode: {format(new Date(startDate), 'dd MMM yyyy')} - {format(new Date(endDate), 'dd MMM yyyy')}
          &amp;lt;/p&amp;gt;
        &amp;lt;/CardHeader&amp;gt;
        &amp;lt;CardContent&amp;gt;
          &amp;lt;Table&amp;gt;
            &amp;lt;TableHeader&amp;gt;
              &amp;lt;TableRow&amp;gt;
                &amp;lt;TableHead&amp;gt;Peringkat&amp;lt;/TableHead&amp;gt;
                &amp;lt;TableHead&amp;gt;Menu&amp;lt;/TableHead&amp;gt;
                &amp;lt;TableHead className="text-right"&amp;gt;Total Dipesan&amp;lt;/TableHead&amp;gt;
              &amp;lt;/TableRow&amp;gt;
            &amp;lt;/TableHeader&amp;gt;
            &amp;lt;TableBody&amp;gt;
              {data.length &amp;gt; 0 ? (
                data.map((item, index) =&amp;gt; (
                  &amp;lt;TableRow key={item.nama_menu || index}&amp;gt;
                    &amp;lt;TableCell&amp;gt;#{index + 1}&amp;lt;/TableCell&amp;gt;
                    &amp;lt;TableCell className="font-medium"&amp;gt;{item.nama_menu || 'Menu Tidak Diketahui'}&amp;lt;/TableCell&amp;gt;
                    &amp;lt;TableCell className="text-right"&amp;gt;{item.total_dipesan || 0}&amp;lt;/TableCell&amp;gt;
                  &amp;lt;/TableRow&amp;gt;
                ))
              ) : (
                &amp;lt;TableRow&amp;gt;
                  &amp;lt;TableCell colSpan={3} className="text-center py-4"&amp;gt;
                    Tidak ada data menu
                  &amp;lt;/TableCell&amp;gt;
                &amp;lt;/TableRow&amp;gt;
              )}
            &amp;lt;/TableBody&amp;gt;
          &amp;lt;/Table&amp;gt;
        &amp;lt;/CardContent&amp;gt;
      &amp;lt;/Card&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;top menu&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";

import { useEffect, useState } from "react";

interface MenuTerlaris {
  menu_id__nama_menu: string;
  total_dipesan: number;
}

export default function TopMenuTerlaris() {
  const [data, setData] = useState&amp;lt;MenuTerlaris[]&amp;gt;([]);
  const [loading, setLoading] = useState(true);

  useEffect(() =&amp;gt; {
    fetch("http://localhost:8000/api/top-5-menu-terlaris/") // Ganti URL jika beda
      .then((res) =&amp;gt; res.json())
      .then((json) =&amp;gt; {
        if (json.success) {
          setData(json.data);
        }
        setLoading(false);
      })
      .catch((err) =&amp;gt; {
        console.error("Gagal ambil data:", err);
        setLoading(false);
      });
  }, []);

  return (
    &amp;lt;div className="p-6 bg-white rounded-xl shadow-md w-full max-w-xl mx-auto mt-8"&amp;gt;
      &amp;lt;h2 className="text-2xl font-bold mb-4 text-center"&amp;gt;Top 5 Menu Terlaris&amp;lt;/h2&amp;gt;
      {loading ? (
        &amp;lt;p className="text-center"&amp;gt;Memuat...&amp;lt;/p&amp;gt;
      ) : data.length === 0 ? (
        &amp;lt;p className="text-center"&amp;gt;Belum ada data.&amp;lt;/p&amp;gt;
      ) : (
        &amp;lt;ul className="space-y-3"&amp;gt;
          {data.map((item, index) =&amp;gt; (
            &amp;lt;li
              key={index}
              className="flex justify-between bg-gray-100 p-3 rounded-lg hover:bg-gray-200 transition"
            &amp;gt;
              &amp;lt;span className="font-medium"&amp;gt;{item.menu_id__nama_menu}&amp;lt;/span&amp;gt;
              &amp;lt;span className="text-sm text-gray-600"&amp;gt;{item.total_dipesan}x&amp;lt;/span&amp;gt;
            &amp;lt;/li&amp;gt;
          ))}
        &amp;lt;/ul&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}





from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.db.models import Sum

from .models import TransaksiDetail  # atau sesuaikan path import kalau beda

@api_view(['GET'])
def top_5_menu_terlaris(request):
    queryset = (
        TransaksiDetail.objects
        .values('menu_id__nama_menu')  # gunakan double underscore untuk FK
        .annotate(total_dipesan=Sum('qty'))
        .order_by('-total_dipesan')[:5]
    )

    return Response({
        'success': True,
        'data': queryset,
        'message': 'Data top 5 menu berhasil diambil'
    })

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>django</category>
      <category>python</category>
    </item>
  </channel>
</rss>
