mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
fix(FE): fixing floating button & revert require auth component
This commit is contained in:
@@ -33,7 +33,9 @@ const FloatingActionsButton = ({
|
||||
}: FloatingActionsButtonProps) => {
|
||||
// Jika tidak ada baris yang dipilih, jangan tampilkan FAB
|
||||
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
|
||||
const getApprovalColor = (action: 'APPROVED' | 'REJECTED') => {
|
||||
|
||||
@@ -54,7 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Dropdown
|
||||
position='bottom-end'
|
||||
direction='bottom'
|
||||
align='end'
|
||||
trigger={
|
||||
<div className='btn btn-ghost btn-circle avatar'>
|
||||
<div className='w-10 rounded-full border flex justify-center items-center'>
|
||||
@@ -62,7 +63,9 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
||||
</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'>
|
||||
<MenuItem title='Logout' onClick={logoutClickHandler} />
|
||||
|
||||
@@ -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';
|
||||
|
||||
interface DropdownProps {
|
||||
export interface DropdownProps {
|
||||
trigger: ReactNode;
|
||||
children: ReactNode;
|
||||
position?:
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'left-start'
|
||||
| 'left-end'
|
||||
| 'right-start'
|
||||
| 'right-end';
|
||||
className?: {
|
||||
wrapper?: string;
|
||||
trigger?: string;
|
||||
content?: string;
|
||||
};
|
||||
align?: 'start' | 'center' | 'end';
|
||||
direction?: 'top' | 'bottom' | 'left' | 'right';
|
||||
hover?: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
close?: boolean;
|
||||
controlled?: boolean;
|
||||
}
|
||||
|
||||
const Dropdown = ({
|
||||
trigger,
|
||||
children,
|
||||
position = 'bottom',
|
||||
align = 'start',
|
||||
hover = false,
|
||||
className,
|
||||
contentClassName,
|
||||
align,
|
||||
direction,
|
||||
hover,
|
||||
defaultOpen = false,
|
||||
open,
|
||||
close,
|
||||
controlled = false,
|
||||
}: DropdownProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
const toggleDropdown = () => {
|
||||
if (!controlled) {
|
||||
const newState = !isOpen;
|
||||
setIsOpen(newState);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// Build position classes
|
||||
const getPositionClasses = () => {
|
||||
const classes: string[] = [];
|
||||
|
||||
// Handle combined positions like 'top-start'
|
||||
if (position.includes('-')) {
|
||||
const [pos, al] = position.split('-');
|
||||
classes.push(`dropdown-${pos}`);
|
||||
classes.push(`dropdown-${al}`);
|
||||
} else {
|
||||
classes.push(`dropdown-${position}`);
|
||||
if (align !== 'start') {
|
||||
classes.push(`dropdown-${align}`);
|
||||
}
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
};
|
||||
|
||||
const handleToggle = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// alert('clicked');
|
||||
setIsOpen(!isOpen);
|
||||
const getWrapperClasses = () => {
|
||||
const openState = controlled ? open : isOpen;
|
||||
|
||||
return cn(
|
||||
'dropdown',
|
||||
{
|
||||
'dropdown-start': align === 'start',
|
||||
'dropdown-center': align === 'center',
|
||||
'dropdown-end': align === 'end',
|
||||
'dropdown-top': direction === 'top',
|
||||
'dropdown-bottom': direction === 'bottom',
|
||||
'dropdown-left': direction === 'left',
|
||||
'dropdown-right': direction === 'right',
|
||||
'dropdown-hover': hover,
|
||||
'dropdown-open': openState && !close,
|
||||
'dropdown-close': close,
|
||||
},
|
||||
className?.wrapper
|
||||
);
|
||||
};
|
||||
|
||||
const getTriggerClasses = () => {
|
||||
return cn(className?.trigger);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className={cn(
|
||||
'dropdown',
|
||||
getPositionClasses(),
|
||||
hover && 'dropdown-hover',
|
||||
isOpen && 'dropdown-open',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Trigger Button */}
|
||||
<div onClick={handleToggle} className='cursor-pointer'>
|
||||
<div ref={dropdownRef} className={getWrapperClasses()}>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
className={getTriggerClasses()}
|
||||
onClick={toggleDropdown}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{trigger}
|
||||
</div>
|
||||
|
||||
{/* Dropdown Content - Only render when open */}
|
||||
{isOpen && (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className={cn('dropdown-content z-[10]', contentClassName)}
|
||||
onClick={() => setIsOpen(false)} // Close on item click
|
||||
>
|
||||
{!close && (
|
||||
<div tabIndex={-1} className={getContentClasses()}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,197 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { useAuth } from '@/services/hooks/useAuth';
|
||||
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { GetMeResponse } from '@/types/api/api-general';
|
||||
|
||||
// TODO: delete this later, DONT HARDCODE USER DATA
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general';
|
||||
import { AxiosError } from 'axios';
|
||||
import { redirectToSSO } from '@/lib/auth-helper';
|
||||
|
||||
interface RequireAuthProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const RequireAuth = ({ children }: RequireAuthProps) => {
|
||||
const router = useRouter();
|
||||
const { setUser, setIsLoadingUser } = useAuth();
|
||||
const { user, setUser, setIsLoadingUser } = useAuth();
|
||||
|
||||
const { data: userResponse, isLoading: isLoadingUserResponse } =
|
||||
useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
|
||||
'/auth/sso/userinfo',
|
||||
httpClientFetcher,
|
||||
{
|
||||
shouldRetryOnError: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshInterval: 0,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoadingUser(isLoadingUserResponse);
|
||||
}, [isLoadingUserResponse, setIsLoadingUser]);
|
||||
const {
|
||||
data: userResponse,
|
||||
isLoading: isLoadingUserResponse,
|
||||
error: userErrorResponse,
|
||||
} = useSWR<
|
||||
GetMeResponse & { ok?: boolean },
|
||||
AxiosError<BaseApiResponse>,
|
||||
SWRHttpKey
|
||||
>('/sso/userinfo', httpClientFetcher, {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isResponseSuccess(userResponse)) {
|
||||
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
|
||||
// if (isLoadingUserResponse && !userResponse) {
|
||||
// return (
|
||||
// <div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
// <span className='loading loading-spinner loading-xl' />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
// Explicitly handle 401 redirect from the component level
|
||||
useEffect(() => {
|
||||
if (
|
||||
isResponseError(userResponse) &&
|
||||
userErrorResponse?.response?.status === 401
|
||||
) {
|
||||
// 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;
|
||||
|
||||
Reference in New Issue
Block a user