fix(FE): fixing floating button & revert require auth component

This commit is contained in:
randy-ar
2025-12-18 11:33:18 +07:00
parent 918e85e0cc
commit a935ffd9f5
4 changed files with 154 additions and 261 deletions
+3 -1
View File
@@ -33,7 +33,9 @@ const FloatingActionsButton = ({
}: FloatingActionsButtonProps) => { }: FloatingActionsButtonProps) => {
// Jika tidak ada baris yang dipilih, jangan tampilkan FAB // Jika tidak ada baris yang dipilih, jangan tampilkan FAB
const positionStyles = const positionStyles =
selectedRowIds.length > 0 ? 'bottom-[10%]' : 'bottom-[-100%]'; selectedRowIds.length > 0
? 'bottom-[10%] opacity-100'
: 'bottom-[-10%] opacity-0';
// Helper untuk menentukan gaya warna tombol approval // Helper untuk menentukan gaya warna tombol approval
const getApprovalColor = (action: 'APPROVED' | 'REJECTED') => { const getApprovalColor = (action: 'APPROVED' | 'REJECTED') => {
+5 -2
View File
@@ -54,7 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
<div className='flex gap-2'> <div className='flex gap-2'>
<Dropdown <Dropdown
position='bottom-end' direction='bottom'
align='end'
trigger={ trigger={
<div className='btn btn-ghost btn-circle avatar'> <div className='btn btn-ghost btn-circle avatar'>
<div className='w-10 rounded-full border flex justify-center items-center'> <div className='w-10 rounded-full border flex justify-center items-center'>
@@ -62,7 +63,9 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
</div> </div>
</div> </div>
} }
contentClassName='w-52 mt-3' className={{
content: 'w-52 mt-3',
}}
> >
<Menu className='p-2 bg-base-100 shadow rounded-box menu-sm'> <Menu className='p-2 bg-base-100 shadow rounded-box menu-sm'>
<MenuItem title='Logout' onClick={logoutClickHandler} /> <MenuItem title='Logout' onClick={logoutClickHandler} />
+77 -79
View File
@@ -1,111 +1,109 @@
'use client'; import React, { ReactNode, useState, useRef } from 'react';
import { ReactNode, useRef, useEffect, useState } from 'react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
interface DropdownProps { export interface DropdownProps {
trigger: ReactNode; trigger: ReactNode;
children: ReactNode; children: ReactNode;
position?: className?: {
| 'top' wrapper?: string;
| 'bottom' trigger?: string;
| 'left' content?: string;
| 'right' };
| 'top-start'
| 'top-end'
| 'bottom-start'
| 'bottom-end'
| 'left-start'
| 'left-end'
| 'right-start'
| 'right-end';
align?: 'start' | 'center' | 'end'; align?: 'start' | 'center' | 'end';
direction?: 'top' | 'bottom' | 'left' | 'right';
hover?: boolean; hover?: boolean;
className?: string; defaultOpen?: boolean;
contentClassName?: string; open?: boolean;
close?: boolean;
controlled?: boolean;
} }
const Dropdown = ({ const Dropdown = ({
trigger, trigger,
children, children,
position = 'bottom',
align = 'start',
hover = false,
className, className,
contentClassName, align,
direction,
hover,
defaultOpen = false,
open,
close,
controlled = false,
}: DropdownProps) => { }: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(defaultOpen);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
// Handle click outside to close dropdown const toggleDropdown = () => {
useEffect(() => { if (!controlled) {
const handleClickOutside = (event: MouseEvent) => { const newState = !isOpen;
if ( setIsOpen(newState);
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
} }
}; };
if (isOpen) { const getWrapperClasses = () => {
document.addEventListener('mousedown', handleClickOutside); const openState = controlled ? open : isOpen;
}
return () => { return cn(
document.removeEventListener('mousedown', handleClickOutside); 'dropdown',
}; {
}, [isOpen]); 'dropdown-start': align === 'start',
'dropdown-center': align === 'center',
// Build position classes 'dropdown-end': align === 'end',
const getPositionClasses = () => { 'dropdown-top': direction === 'top',
const classes: string[] = []; 'dropdown-bottom': direction === 'bottom',
'dropdown-left': direction === 'left',
// Handle combined positions like 'top-start' 'dropdown-right': direction === 'right',
if (position.includes('-')) { 'dropdown-hover': hover,
const [pos, al] = position.split('-'); 'dropdown-open': openState && !close,
classes.push(`dropdown-${pos}`); 'dropdown-close': close,
classes.push(`dropdown-${al}`); },
} else { className?.wrapper
classes.push(`dropdown-${position}`); );
if (align !== 'start') {
classes.push(`dropdown-${align}`);
}
}
return classes.join(' ');
}; };
const handleToggle = (e: React.MouseEvent) => { const getTriggerClasses = () => {
e.preventDefault(); return cn(className?.trigger);
e.stopPropagation();
// alert('clicked');
setIsOpen(!isOpen);
}; };
const getContentClasses = () => {
return cn(
'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box',
className?.content
);
};
if (controlled) {
return (
<div className={getWrapperClasses()}>
{trigger}
{open && !close && (
<div tabIndex={-1} className={getContentClasses()}>
{children}
</div>
)}
</div>
);
}
return ( return (
<div ref={dropdownRef} className={getWrapperClasses()}>
<div <div
ref={dropdownRef} tabIndex={0}
className={cn( role='button'
'dropdown', className={getTriggerClasses()}
getPositionClasses(), onClick={toggleDropdown}
hover && 'dropdown-hover', onKeyDown={(e) => {
isOpen && 'dropdown-open', if (e.key === 'Enter' || e.key === ' ') {
className e.preventDefault();
)} toggleDropdown();
}
}}
> >
{/* Trigger Button */}
<div onClick={handleToggle} className='cursor-pointer'>
{trigger} {trigger}
</div> </div>
{!close && (
{/* Dropdown Content - Only render when open */} <div tabIndex={-1} className={getContentClasses()}>
{isOpen && (
<div
tabIndex={-1}
className={cn('dropdown-content z-[10]', contentClassName)}
onClick={() => setIsOpen(false)} // Close on item click
>
{children} {children}
</div> </div>
)} )}
+63 -173
View File
@@ -1,197 +1,87 @@
'use client'; 'use client';
import { ReactNode, useEffect } from 'react'; import { ReactNode, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import useSWR from 'swr';
import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth'; import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { GetMeResponse } from '@/types/api/api-general'; import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
import { AxiosError } from 'axios';
// TODO: delete this later, DONT HARDCODE USER DATA import { redirectToSSO } from '@/lib/auth-helper';
const DUMMY_USER = {
id: 1,
email: 'admin@mbugroup.id',
npk: '0001',
name: 'Super Admin',
image: null,
created_at: '2025-09-30T03:24:20.899229Z',
updated_at: '2025-09-30T03:24:20.899229Z',
roles: [
{
id: 1,
key: 'mbu.super_admin',
name: 'MBU Administrator',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
permissions: [
{
id: 1,
name: 'mbu:purchase:read',
action: 'read',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 2,
name: 'mbu:purchase:create',
action: 'create',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 3,
name: 'mbu:purchase:approve',
action: 'approve',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
],
},
{
id: 2,
key: 'lti.super_admin',
name: 'LTI Administrator',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
permissions: [
{
id: 4,
name: 'lti:purchase:read',
action: 'read',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 5,
name: 'lti:purchase:create',
action: 'create',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 6,
name: 'lti:purchase:approve',
action: 'approve',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
],
},
{
id: 3,
key: 'manbu.super_admin',
name: 'MANBU Administrator',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
permissions: [
{
id: 7,
name: 'manbu:purchase:read',
action: 'read',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 8,
name: 'manbu:purchase:create',
action: 'create',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 9,
name: 'manbu:purchase:approve',
action: 'approve',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
],
},
],
};
interface RequireAuthProps { interface RequireAuthProps {
children?: ReactNode; children?: ReactNode;
} }
const RequireAuth = ({ children }: RequireAuthProps) => { const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter(); const { user, setUser, setIsLoadingUser } = useAuth();
const { setUser, setIsLoadingUser } = useAuth();
const { data: userResponse, isLoading: isLoadingUserResponse } = const {
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>( data: userResponse,
'/auth/sso/userinfo', isLoading: isLoadingUserResponse,
httpClientFetcher, error: userErrorResponse,
{ } = useSWR<
GetMeResponse & { ok?: boolean },
AxiosError<BaseApiResponse>,
SWRHttpKey
>('/sso/userinfo', httpClientFetcher, {
shouldRetryOnError: false, shouldRetryOnError: false,
revalidateOnFocus: false, });
revalidateOnReconnect: false,
refreshInterval: 0,
}
);
useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse, setIsLoadingUser]);
useEffect(() => { useEffect(() => {
if (isResponseSuccess(userResponse)) { if (isResponseSuccess(userResponse)) {
setUser(userResponse.data); setUser(userResponse.data);
} else {
// router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
// TODO: remove this later, DONT HARDCODE USER DATA
setUser(DUMMY_USER);
} }
}, [userResponse, setIsLoadingUser, setUser]); }, [userResponse, setUser]);
// TODO: uncomment this later // Explicitly handle 401 redirect from the component level
// if (isLoadingUserResponse && !userResponse) { useEffect(() => {
// return ( if (
// <div className='w-full flex flex-row justify-center items-center p-4'> isResponseError(userResponse) &&
// <span className='loading loading-spinner loading-xl' /> userErrorResponse?.response?.status === 401
// </div> ) {
// ); // Clear cache to prevent stale data from rendering children
// } // mutate('/sso/userinfo', undefined, { revalidate: false }); // Optional: if using global mutate
setUser(undefined);
redirectToSSO();
}
}, [userErrorResponse, setUser, userResponse]);
return <>{children}</>; useEffect(() => {
setIsLoadingUser(isLoadingUserResponse);
}, [isLoadingUserResponse]);
if (
(isLoadingUserResponse && !userResponse && !userErrorResponse) ||
(!userResponse && !userErrorResponse)
) {
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (userErrorResponse) {
return (
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
<h2 className='text-2xl font-bold text-error'>Authentication Failed</h2>
<p className='text-gray-600'>
Please try refreshing the page or contact support if the problem
persists.
</p>
<button
className='btn btn-primary'
onClick={() => window.location.reload()}
>
Retry
</button>
</div>
);
}
return <>{isResponseSuccess(userResponse) && user && children}</>;
}; };
export default RequireAuth; export default RequireAuth;