feature/syasya/testlayout #8
@ -68,28 +68,40 @@ const AdminDashboard = () => {
 | 
			
		||||
  // near other refs
 | 
			
		||||
  const loggingRef = useRef<HTMLDivElement | null>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const checkAuth = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const res = await fetch('/api/auth/me', { credentials: 'include' });
 | 
			
		||||
        if (!res.ok) {
 | 
			
		||||
          router.replace('/login');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        const data = await res.json();
 | 
			
		||||
        if (!data.user) {
 | 
			
		||||
          router.replace('/login');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      } catch {
 | 
			
		||||
        router.replace('/login');
 | 
			
		||||
      } finally {
 | 
			
		||||
        setAuthChecked(true);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
 | 
			
		||||
 | 
			
		||||
    checkAuth();
 | 
			
		||||
  }, [router]);
 | 
			
		||||
useEffect(() => {
 | 
			
		||||
  let cancelled = false;
 | 
			
		||||
 | 
			
		||||
  const checkAuth = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await fetch(`${API}/auth/me`, {
 | 
			
		||||
        credentials: 'include',
 | 
			
		||||
        cache: 'no-store',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!res.ok) {
 | 
			
		||||
        router.replace('/login');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const user = await res.json().catch(() => null);
 | 
			
		||||
      if (!user?.id) {
 | 
			
		||||
        router.replace('/login');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      // authenticated
 | 
			
		||||
    } catch {
 | 
			
		||||
      router.replace('/login');
 | 
			
		||||
      return;
 | 
			
		||||
    } finally {
 | 
			
		||||
      if (!cancelled) setAuthChecked(true);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  checkAuth();
 | 
			
		||||
  return () => { cancelled = true; };
 | 
			
		||||
}, [router, API]);
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,30 +10,48 @@ export default function LoginPage() {
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const [ready, setReady] = useState(false); // gate to avoid UI flash
 | 
			
		||||
 | 
			
		||||
  // Use ONE client-exposed API env var everywhere
 | 
			
		||||
  const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let cancelled = false;
 | 
			
		||||
    const controller = new AbortController();
 | 
			
		||||
 | 
			
		||||
    (async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const res = await fetch('/api/auth/me', {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          cache: 'no-store',
 | 
			
		||||
          credentials: 'include', // safe even if same-origin
 | 
			
		||||
        const res = await fetch(`${API}/auth/me`, {
 | 
			
		||||
          credentials: 'include',
 | 
			
		||||
          cache: 'no-store',        // don't reuse a cached 401
 | 
			
		||||
          signal: controller.signal,
 | 
			
		||||
        });
 | 
			
		||||
        if (!cancelled && res.ok) {
 | 
			
		||||
 | 
			
		||||
        if (!res.ok) {
 | 
			
		||||
          if (!cancelled) setReady(true);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const user = await res.json().catch(() => null);
 | 
			
		||||
        if (user?.id) {
 | 
			
		||||
          // already logged in -> go straight to dashboard
 | 
			
		||||
          router.replace('/adminDashboard');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // not logged in -> show form
 | 
			
		||||
        if (!cancelled) setReady(true);
 | 
			
		||||
      } catch {
 | 
			
		||||
        // ignore errors; just show the form
 | 
			
		||||
        // network/error -> show form
 | 
			
		||||
        if (!cancelled) setReady(true);
 | 
			
		||||
      }
 | 
			
		||||
      if (!cancelled) setReady(true);
 | 
			
		||||
    })();
 | 
			
		||||
 | 
			
		||||
    return () => { cancelled = true; };
 | 
			
		||||
  }, [router]);
 | 
			
		||||
    return () => {
 | 
			
		||||
      cancelled = true;
 | 
			
		||||
      controller.abort();
 | 
			
		||||
    };
 | 
			
		||||
  }, [router, API]);
 | 
			
		||||
 | 
			
		||||
  if (!ready) return null; // or a small spinner if you prefer
 | 
			
		||||
  if (!ready) return null; // or a spinner/skeleton
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="relative min-h-screen overflow-hidden bg-[#060818] text-white">
 | 
			
		||||
@ -66,10 +84,8 @@ export default function LoginPage() {
 | 
			
		||||
                     bg-[linear-gradient(45deg,#fffbe6_0%,rgba(255,251,230,0)_25%,rgba(255,251,230,0)_75%,#fffbe6_100%)]
 | 
			
		||||
                     dark:bg-[linear-gradient(52.22deg,#facc15_0%,rgba(250,204,21,0)_20%,rgba(250,204,21,0)_80%,#facc15_100%)]"
 | 
			
		||||
        >
 | 
			
		||||
          {/* Inner card (glassmorphic effect) */}
 | 
			
		||||
          <div className="relative z-10 rounded-2xl bg-white/10 px-8 py-16 backdrop-blur-lg dark:bg-white/10 lg:min-h-[600px]">
 | 
			
		||||
            <div className="mx-auto w-full max-w-[440px] text-center">
 | 
			
		||||
              {/* Header */}
 | 
			
		||||
              <h1 className="text-4xl font-extrabold uppercase tracking-wide text-yellow-400 mb-2">
 | 
			
		||||
                Sign In
 | 
			
		||||
              </h1>
 | 
			
		||||
@ -77,10 +93,8 @@ export default function LoginPage() {
 | 
			
		||||
                Enter your email and password to access your account.
 | 
			
		||||
              </p>
 | 
			
		||||
 | 
			
		||||
              {/* Login form */}
 | 
			
		||||
              <ComponentsAuthLoginForm />
 | 
			
		||||
 | 
			
		||||
              {/* Footer link */}
 | 
			
		||||
              <div className="mt-6 text-sm text-gray-200 dark:text-gray-300">
 | 
			
		||||
                Don’t have an account?{' '}
 | 
			
		||||
                <Link
 | 
			
		||||
@ -98,3 +112,4 @@ export default function LoginPage() {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,31 +3,46 @@ import IconLockDots from '@/components/icon/icon-lock-dots';
 | 
			
		||||
import IconMail from '@/components/icon/icon-mail';
 | 
			
		||||
import { useRouter } from 'next/navigation';
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import toast from 'react-hot-toast';
 | 
			
		||||
 | 
			
		||||
type User = { id: string; email: string; is_active: boolean };
 | 
			
		||||
 | 
			
		||||
const ComponentsAuthLoginForm = () => {
 | 
			
		||||
  const [email, setEmail] = useState('');
 | 
			
		||||
  const [password, setPassword] = useState('');
 | 
			
		||||
  const [loading, setLoading] = useState(false);
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const API = process.env.NEXT_PUBLIC_FASTAPI_URL; // e.g. http://localhost:8000
 | 
			
		||||
 | 
			
		||||
  const submitForm = async (e: React.FormEvent) => {
 | 
			
		||||
  const submitForm = async (e: React.FormEvent<HTMLFormElement>) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await axios.post('/api/login', { email, password });
 | 
			
		||||
      toast.success(res.data?.message || 'Login successful!');
 | 
			
		||||
      const res = await fetch(`${API}/auth/login`, {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' },
 | 
			
		||||
        body: JSON.stringify({ email, password }),
 | 
			
		||||
        credentials: 'include', // cookie from FastAPI
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      let data: any = null;
 | 
			
		||||
      try {
 | 
			
		||||
        data = await res.json();
 | 
			
		||||
      } catch {
 | 
			
		||||
        // non-JSON error
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!res.ok) {
 | 
			
		||||
        const msg = data?.detail || data?.message || 'Invalid credentials';
 | 
			
		||||
        throw new Error(msg);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const user: User = data;
 | 
			
		||||
      toast.success(`Welcome ${user.email}`);
 | 
			
		||||
      router.push('/adminDashboard');
 | 
			
		||||
      router.refresh();
 | 
			
		||||
      // token cookie is already set by the server:
 | 
			
		||||
    } catch (err: any) {
 | 
			
		||||
      console.error('Login error:', err);
 | 
			
		||||
      const msg =
 | 
			
		||||
        err?.response?.data?.message ||
 | 
			
		||||
        err?.message ||
 | 
			
		||||
        'Invalid credentials';
 | 
			
		||||
      toast.error(msg);
 | 
			
		||||
      toast.error(err?.message ?? 'Login failed');
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoading(false);
 | 
			
		||||
    }
 | 
			
		||||
@ -52,6 +67,7 @@ const ComponentsAuthLoginForm = () => {
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="pb-2">
 | 
			
		||||
        <label htmlFor="Password" className="text-yellow-400 text-left">Password</label>
 | 
			
		||||
        <div className="relative text-white-dark">
 | 
			
		||||
@ -70,6 +86,7 @@ const ComponentsAuthLoginForm = () => {
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <button
 | 
			
		||||
        type="submit"
 | 
			
		||||
        disabled={loading}
 | 
			
		||||
@ -83,3 +100,4 @@ const ComponentsAuthLoginForm = () => {
 | 
			
		||||
 | 
			
		||||
export default ComponentsAuthLoginForm;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -33,12 +33,14 @@ export default function ComponentsAuthRegisterForm({ redirectTo = "/dashboard" }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const API = process.env.NEXT_PUBLIC_FASTAPI_URL; // e.g. http://localhost:8000
 | 
			
		||||
    try {
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
      const res = await fetch("/api/register", {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        headers: { "Content-Type": "application/json" },
 | 
			
		||||
      const res = await fetch(`${API}/auth/register`, {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        headers: { 'Content-Type': 'application/json' },
 | 
			
		||||
        body: JSON.stringify({ email, password }),
 | 
			
		||||
        credentials: 'include',
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const data = await res.json();
 | 
			
		||||
 | 
			
		||||
@ -3,19 +3,18 @@ import { useEffect, useState } from 'react';
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux';
 | 
			
		||||
import Link from 'next/link';
 | 
			
		||||
import { IRootState } from '@/store';
 | 
			
		||||
import { toggleTheme, toggleSidebar, toggleRTL } from '@/store/themeConfigSlice';
 | 
			
		||||
import { toggleTheme, toggleSidebar } from '@/store/themeConfigSlice';
 | 
			
		||||
import Image from 'next/image';
 | 
			
		||||
import Dropdown from '@/components/dropdown';
 | 
			
		||||
import IconMenu from '@/components/icon/icon-menu';
 | 
			
		||||
import IconSun from '@/components/icon/icon-sun';
 | 
			
		||||
import IconMoon from '@/components/icon/icon-moon';
 | 
			
		||||
import IconUser from '@/components/icon/icon-user';
 | 
			
		||||
import IconMail from '@/components/icon/icon-mail';
 | 
			
		||||
import IconLockDots from '@/components/icon/icon-lock-dots';
 | 
			
		||||
import IconLogout from '@/components/icon/icon-logout';
 | 
			
		||||
import { usePathname, useRouter } from 'next/navigation';
 | 
			
		||||
 | 
			
		||||
type UserData = { id: string; email: string; createdAt: string };
 | 
			
		||||
type UserData = { id: string; email: string; is_active: boolean };
 | 
			
		||||
 | 
			
		||||
export default function Header() {
 | 
			
		||||
  const pathname = usePathname();
 | 
			
		||||
@ -27,17 +26,16 @@ export default function Header() {
 | 
			
		||||
  const [user, setUser] = useState<UserData | null>(null);
 | 
			
		||||
  const [loadingUser, setLoadingUser] = useState(true);
 | 
			
		||||
 | 
			
		||||
  // Highlight active menu (your original effect)
 | 
			
		||||
  const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
 | 
			
		||||
 | 
			
		||||
  // highlight active menu
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const selector = document.querySelector(
 | 
			
		||||
      'ul.horizontal-menu a[href="' + window.location.pathname + '"]'
 | 
			
		||||
      `ul.horizontal-menu a[href="${window.location.pathname}"]`
 | 
			
		||||
    );
 | 
			
		||||
    if (selector) {
 | 
			
		||||
      document
 | 
			
		||||
        .querySelectorAll('ul.horizontal-menu .nav-link.active')
 | 
			
		||||
        .forEach((el) => el.classList.remove('active'));
 | 
			
		||||
      document
 | 
			
		||||
        .querySelectorAll('ul.horizontal-menu a.active')
 | 
			
		||||
        .querySelectorAll('ul.horizontal-menu .nav-link.active, ul.horizontal-menu a.active')
 | 
			
		||||
        .forEach((el) => el.classList.remove('active'));
 | 
			
		||||
      selector.classList.add('active');
 | 
			
		||||
      const ul: any = selector.closest('ul.sub-menu');
 | 
			
		||||
@ -48,16 +46,16 @@ export default function Header() {
 | 
			
		||||
    }
 | 
			
		||||
  }, [pathname]);
 | 
			
		||||
 | 
			
		||||
  async function loadUser() {
 | 
			
		||||
  async function loadUser(signal?: AbortSignal) {
 | 
			
		||||
  try {
 | 
			
		||||
    const res = await fetch('/api/auth/me', {
 | 
			
		||||
      method: 'GET',
 | 
			
		||||
      credentials: 'include', // send cookie
 | 
			
		||||
      cache: 'no-store',      // avoid stale cached responses
 | 
			
		||||
    const res = await fetch(`${API}/auth/me`, {
 | 
			
		||||
      credentials: 'include',
 | 
			
		||||
      cache: 'no-store',
 | 
			
		||||
      signal,
 | 
			
		||||
    });
 | 
			
		||||
    if (!res.ok) throw new Error();
 | 
			
		||||
    const data = await res.json();
 | 
			
		||||
    setUser(data.user);
 | 
			
		||||
    const data = await res.json().catch(() => null);
 | 
			
		||||
    setUser(data?.id ? (data as UserData) : null);
 | 
			
		||||
  } catch {
 | 
			
		||||
    setUser(null);
 | 
			
		||||
  } finally {
 | 
			
		||||
@ -65,44 +63,53 @@ export default function Header() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
useEffect(() => {
 | 
			
		||||
  setLoadingUser(true);
 | 
			
		||||
  loadUser();
 | 
			
		||||
  // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
}, [pathname]); // re-fetch on route change (after login redirect)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setLoadingUser(true);
 | 
			
		||||
    const controller = new AbortController();
 | 
			
		||||
    loadUser(controller.signal);
 | 
			
		||||
    return () => controller.abort();
 | 
			
		||||
  }, [pathname, API]);
 | 
			
		||||
 | 
			
		||||
  const handleLogout = async () => {
 | 
			
		||||
    await fetch('/api/logout', { method: 'POST' });
 | 
			
		||||
    try {
 | 
			
		||||
    await fetch(`${API}/auth/logout`, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      credentials: 'include',
 | 
			
		||||
      cache: 'no-store',
 | 
			
		||||
    });
 | 
			
		||||
  } catch (_) {
 | 
			
		||||
    // ignore
 | 
			
		||||
  } finally {
 | 
			
		||||
    setUser(null);
 | 
			
		||||
    router.push('/login'); // go to login
 | 
			
		||||
    window.location.href = '/login';
 | 
			
		||||
  }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <header className={`z-40 ${themeConfig.semidark && themeConfig.menu === 'horizontal' ? 'dark' : ''}`}>
 | 
			
		||||
      <div className="shadow-sm">
 | 
			
		||||
        <div className="relative flex w-full items-center bg-white px-5 py-2.5 dark:bg-rtgray-900">
 | 
			
		||||
          {/* Logo */}
 | 
			
		||||
          {/* Logo + mobile toggler */}
 | 
			
		||||
          <div className="horizontal-logo flex items-center justify-between ltr:mr-2 rtl:ml-2 lg:hidden">
 | 
			
		||||
            <div className="relative h-10 w-32 sm:h-11 sm:w-36 md:h-12 md:w-27 shrink-0 max-h-12">
 | 
			
		||||
                <Image
 | 
			
		||||
              <Image
 | 
			
		||||
                src="/assets/images/newfulllogo.png"
 | 
			
		||||
                alt="logo"
 | 
			
		||||
                fill
 | 
			
		||||
                className="object-cover"
 | 
			
		||||
                priority
 | 
			
		||||
                sizes="(max-width: 640px) 8rem, (max-width: 768px) 9rem, (max-width: 1024px) 10rem, 10rem"
 | 
			
		||||
                />
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <button
 | 
			
		||||
                type="button"
 | 
			
		||||
                onClick={() => dispatch(toggleSidebar())}
 | 
			
		||||
                className="collapse-icon flex p-2 rounded-full hover:bg-rtgray-200 dark:text-white dark:hover:bg-rtgray-700"
 | 
			
		||||
              type="button"
 | 
			
		||||
              onClick={() => dispatch(toggleSidebar())}
 | 
			
		||||
              className="collapse-icon flex p-2 rounded-full hover:bg-rtgray-200 dark:text-white dark:hover:bg-rtgray-700"
 | 
			
		||||
            >
 | 
			
		||||
                <IconMenu className="h-6 w-6" />
 | 
			
		||||
              <IconMenu className="h-6 w-6" />
 | 
			
		||||
            </button>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {/* Right-side actions */}
 | 
			
		||||
          <div className="flex items-center justify-end space-x-1.5 ltr:ml-auto rtl:mr-auto rtl:space-x-reverse dark:text-[#d0d2d6] lg:space-x-2">
 | 
			
		||||
@ -124,21 +131,21 @@ useEffect(() => {
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            {/* User dropdown */}
 | 
			
		||||
            <div className="dropdown flex shrink-0 ">
 | 
			
		||||
            <div className="dropdown flex shrink-0">
 | 
			
		||||
              {loadingUser ? (
 | 
			
		||||
                <div className="h-9 w-9 rounded-full animate-pulse bg-gray-300 dark:bg-rtgray-800" />
 | 
			
		||||
              ) : user ? (
 | 
			
		||||
            <Dropdown
 | 
			
		||||
                placement={isRtl ? 'bottom-start' : 'bottom-end'}
 | 
			
		||||
                btnClassName="relative group block"
 | 
			
		||||
                panelClassName="rounded-lg shadow-lg border border-white/10 bg-rtgray-100 dark:bg-rtgray-800 p-2" // ✅
 | 
			
		||||
                button={
 | 
			
		||||
                        <div className="h-9 w-9 rounded-full bg-rtgray-200 dark:bg-rtgray-800 flex items-center justify-center group-hover:bg-rtgray-300 dark:group-hover:bg-rtgray-700">
 | 
			
		||||
                        <IconUser className="h-5 w-5 text-gray-600 dark:text-gray-300" />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    }
 | 
			
		||||
                <Dropdown
 | 
			
		||||
                  placement={isRtl ? 'bottom-start' : 'bottom-end'}
 | 
			
		||||
                  btnClassName="relative group block"
 | 
			
		||||
                  panelClassName="rounded-lg shadow-lg border border-white/10 bg-rtgray-100 dark:bg-rtgray-800 p-2"
 | 
			
		||||
                  button={
 | 
			
		||||
                    <div className="h-9 w-9 rounded-full bg-rtgray-200 dark:bg-rtgray-800 flex items-center justify-center group-hover:bg-rtgray-300 dark:group-hover:bg-rtgray-700">
 | 
			
		||||
                      <IconUser className="h-5 w-5 text-gray-600 dark:text-gray-300" />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                <ul className="w-[230px] font-semibold text-dark"> {/* make sure this stays transparent */}
 | 
			
		||||
                  <ul className="w-[230px] font-semibold text-dark">
 | 
			
		||||
                    <li className="px-4 py-4 flex items-center">
 | 
			
		||||
                      <div className="truncate ltr:pl-1.5 rtl:pr-4">
 | 
			
		||||
                        <h4 className="text-sm text-left">{user.email}</h4>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user