hotfix(FE): fixing dropdown logout and floating button max size

This commit is contained in:
randy-ar
2025-12-10 13:25:07 +07:00
parent 865b0b3d8f
commit eed142a85f
7 changed files with 381 additions and 47 deletions
@@ -52,6 +52,7 @@ export default function ProjectFlockLayout({
closeOnBackdropClick={isDetail ? true : false} closeOnBackdropClick={isDetail ? true : false}
onBackdropClick={handleBackdropClick} onBackdropClick={handleBackdropClick}
variant='right' variant='right'
zIndex='99999'
sidebarContent={isOpen && <div className=''>{children}</div>} sidebarContent={isOpen && <div className=''>{children}</div>}
/> />
</> </>
+1 -1
View File
@@ -54,7 +54,7 @@ const FloatingActionsButton = ({
<div <div
className={cn( className={cn(
`absolute ${positionStyles} inset-x-1/2 -translate-x-1/2 z-50`, `absolute ${positionStyles} inset-x-1/2 -translate-x-1/2 z-50`,
'mx-auto w-full max-w-lg sm:mx-0 bg-base-300 p-4 rounded-xl shadow-md transition-all duration-300 transform', 'mx-auto w-full max-w-sm sm:mx-0 bg-base-300 p-4 rounded-xl shadow-md transition-all duration-300 transform',
'bg-slate-950 backdrop-blur-md' 'bg-slate-950 backdrop-blur-md'
)} )}
> >
+13 -12
View File
@@ -7,6 +7,7 @@ import { Icon } from '@iconify/react';
import Menu from '@/components/menu/Menu'; import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem'; import MenuItem from '@/components/menu/MenuItem';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Dropdown from '@/components/dropdown/Dropdown';
import { useAuth } from '@/services/hooks/useAuth'; import { useAuth } from '@/services/hooks/useAuth';
import { AuthApi } from '@/services/api/auth'; import { AuthApi } from '@/services/api/auth';
@@ -52,21 +53,21 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
</div> </div>
<div className='flex gap-2'> <div className='flex gap-2'>
<div className='dropdown dropdown-end'> <Dropdown
<div position='bottom-end'
tabIndex={0} trigger={
role='button' <div className='btn btn-ghost btn-circle avatar'>
className='btn btn-ghost btn-circle avatar' <div className='w-10 rounded-full border flex justify-center items-center'>
> <Icon icon='uil:user' width={40} height={40} />
<div className='w-10 rounded-full border grid place-items-center'> </div>
<Icon icon='uil:user' width={40} height={40} />
</div> </div>
</div> }
contentClassName='w-52 mt-3'
<Menu className='dropdown-content w-52 mt-3 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} />
</Menu> </Menu>
</div> </Dropdown>
</div> </div>
</div> </div>
); );
+116
View File
@@ -0,0 +1,116 @@
'use client';
import { ReactNode, useRef, useEffect, useState } from 'react';
import { cn } from '@/lib/helper';
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';
align?: 'start' | 'center' | 'end';
hover?: boolean;
className?: string;
contentClassName?: string;
}
const Dropdown = ({
trigger,
children,
position = 'bottom',
align = 'start',
hover = false,
className,
contentClassName,
}: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
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);
}
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);
};
return (
<div
ref={dropdownRef}
className={cn(
'dropdown',
getPositionClasses(),
hover && 'dropdown-hover',
isOpen && 'dropdown-open',
className
)}
>
{/* Trigger Button */}
<div onClick={handleToggle} className='cursor-pointer'>
{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
>
{children}
</div>
)}
</div>
);
};
export default Dropdown;
+83
View File
@@ -0,0 +1,83 @@
# Dropdown Component
Komponen Dropdown reusable berdasarkan DaisyUI yang mengatasi issue children component yang ter-render sebelum dropdown dibuka.
## Features
-**Conditional Rendering**: Children hanya di-render ketika dropdown aktif/terbuka
-**Click Outside to Close**: Otomatis menutup dropdown ketika klik di luar area dropdown
-**Multiple Positions**: Support berbagai posisi (top, bottom, left, right) dengan alignment (start, center, end)
-**Hover Support**: Optional hover mode untuk membuka dropdown
-**Customizable**: Mendukung custom className untuk container dan content
## Usage
### Basic Example
```tsx
import Dropdown from '@/components/dropdown/Dropdown';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
<Dropdown
trigger={
<button className="btn">Click Me</button>
}
>
<Menu className="p-2 bg-base-100 shadow rounded-box menu-sm w-52">
<MenuItem title="Item 1" onClick={() => console.log('Item 1')} />
<MenuItem title="Item 2" onClick={() => console.log('Item 2')} />
</Menu>
</Dropdown>
```
### With Position
```tsx
<Dropdown
position="bottom-end"
trigger={<button className="btn">Dropdown</button>}
contentClassName="w-52 mt-3"
>
{/* Your content */}
</Dropdown>
```
### Hover Mode
```tsx
<Dropdown
hover={true}
trigger={<button className="btn">Hover Me</button>}
>
{/* Your content */}
</Dropdown>
```
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `trigger` | `ReactNode` | - | **Required**. Element yang akan men-trigger dropdown |
| `children` | `ReactNode` | - | **Required**. Content dropdown yang akan ditampilkan |
| `position` | `'top' \| 'bottom' \| 'left' \| 'right' \| 'top-start' \| 'top-end' \| 'bottom-start' \| 'bottom-end' \| 'left-start' \| 'left-end' \| 'right-start' \| 'right-end'` | `'bottom'` | Posisi dropdown relatif terhadap trigger |
| `align` | `'start' \| 'center' \| 'end'` | `'start'` | Alignment dropdown (digunakan jika position tidak mengandung alignment) |
| `hover` | `boolean` | `false` | Aktifkan mode hover untuk membuka dropdown |
| `className` | `string` | - | Custom className untuk container dropdown |
| `contentClassName` | `string` | - | Custom className untuk content dropdown |
## Position Examples
- `bottom` - Dropdown muncul di bawah, align ke start
- `bottom-end` - Dropdown muncul di bawah, align ke end
- `bottom-center` - Dropdown muncul di bawah, align ke center
- `top-start` - Dropdown muncul di atas, align ke start
- `left-end` - Dropdown muncul di kiri, align ke end
- Dan seterusnya...
## Key Benefits
1. **Performance**: Children tidak di-render sampai dropdown dibuka, menghemat resources
2. **Clean State**: Setiap kali dropdown dibuka, children di-render fresh
3. **DaisyUI Compatible**: Menggunakan class DaisyUI yang sudah ada
4. **Accessible**: Menggunakan proper ARIA attributes dan keyboard navigation
+166 -33
View File
@@ -6,9 +6,147 @@ 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 { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; import { GetMeResponse } from '@/types/api/api-general';
import { AxiosError } from 'axios';
// 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',
},
},
],
},
],
};
interface RequireAuthProps { interface RequireAuthProps {
children?: ReactNode; children?: ReactNode;
@@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter(); const router = useRouter();
const { setUser, setIsLoadingUser } = useAuth(); const { setUser, setIsLoadingUser } = useAuth();
const { const { data: userResponse, isLoading: isLoadingUserResponse } =
data: userResponse, useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
isLoading: isLoadingUserResponse, '/auth/sso/userinfo',
error: userErrorResponse, httpClientFetcher,
} = useSWRImmutable< {
GetMeResponse & { ok?: boolean }, shouldRetryOnError: false,
AxiosError<BaseApiResponse>, revalidateOnFocus: false,
SWRHttpKey revalidateOnReconnect: false,
>('/sso/userinfo', httpClientFetcher, { refreshInterval: 0,
shouldRetryOnError: false, }
revalidateOnFocus: false, );
revalidateOnReconnect: false,
refreshInterval: 0,
});
useEffect(() => { useEffect(() => {
setIsLoadingUser(isLoadingUserResponse); setIsLoadingUser(isLoadingUserResponse);
@@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
useEffect(() => { useEffect(() => {
if (isResponseSuccess(userResponse)) { if (isResponseSuccess(userResponse)) {
setUser(userResponse.data); setUser(userResponse.data);
} else if ( } else {
isResponseError(userErrorResponse?.response?.data) && // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
typeof window !== 'undefined' // TODO: remove this later, DONT HARDCODE USER DATA
) { setUser(DUMMY_USER);
router.replace(
`${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`
);
} }
}, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); }, [userResponse, setIsLoadingUser, setUser]);
if (isLoadingUserResponse && !userResponse && !userErrorResponse) { // TODO: uncomment this later
return ( // if (isLoadingUserResponse && !userResponse) {
<div className='w-full flex flex-row justify-center items-center p-4'> // return (
<span className='loading loading-spinner loading-xl' /> // <div className='w-full flex flex-row justify-center items-center p-4'>
</div> // <span className='loading loading-spinner loading-xl' />
); // </div>
} // );
// }
return <>{isResponseSuccess(userResponse) && children}</>; return <>{children}</>;
}; };
export default RequireAuth; export default RequireAuth;
+1 -1
View File
@@ -51,4 +51,4 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
} }
} }
export const ClosingApi = new ClosingApiService('/closing'); export const ClosingApi = new ClosingApiService('/closings');