From eed142a85ffc38e29e06e49b75c13d51842dff2d Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 10 Dec 2025 13:25:07 +0700 Subject: [PATCH 1/2] hotfix(FE): fixing dropdown logout and floating button max size --- src/app/production/project-flock/layout.tsx | 1 + src/components/FloatingActionsButton.tsx | 2 +- src/components/Navbar.tsx | 25 +-- src/components/dropdown/Dropdown.tsx | 116 ++++++++++++ src/components/dropdown/README.md | 83 ++++++++ src/components/helper/RequireAuth.tsx | 199 ++++++++++++++++---- src/services/api/closing.ts | 2 +- 7 files changed, 381 insertions(+), 47 deletions(-) create mode 100644 src/components/dropdown/Dropdown.tsx create mode 100644 src/components/dropdown/README.md diff --git a/src/app/production/project-flock/layout.tsx b/src/app/production/project-flock/layout.tsx index 698064cf..b74ef612 100644 --- a/src/app/production/project-flock/layout.tsx +++ b/src/app/production/project-flock/layout.tsx @@ -52,6 +52,7 @@ export default function ProjectFlockLayout({ closeOnBackdropClick={isDetail ? true : false} onBackdropClick={handleBackdropClick} variant='right' + zIndex='99999' sidebarContent={isOpen &&
{children}
} /> diff --git a/src/components/FloatingActionsButton.tsx b/src/components/FloatingActionsButton.tsx index c0033d72..c9ca3454 100644 --- a/src/components/FloatingActionsButton.tsx +++ b/src/components/FloatingActionsButton.tsx @@ -54,7 +54,7 @@ const FloatingActionsButton = ({
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 973bf031..bee92a57 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -7,6 +7,7 @@ import { Icon } from '@iconify/react'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Button from '@/components/Button'; +import Dropdown from '@/components/dropdown/Dropdown'; import { useAuth } from '@/services/hooks/useAuth'; import { AuthApi } from '@/services/api/auth'; @@ -52,21 +53,21 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
-
-
-
- + +
+ +
-
- - + } + contentClassName='w-52 mt-3' + > + -
+
); diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx new file mode 100644 index 00000000..4489231d --- /dev/null +++ b/src/components/dropdown/Dropdown.tsx @@ -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(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 ( +
+ {/* Trigger Button */} +
+ {trigger} +
+ + {/* Dropdown Content - Only render when open */} + {isOpen && ( +
setIsOpen(false)} // Close on item click + > + {children} +
+ )} +
+ ); +}; + +export default Dropdown; diff --git a/src/components/dropdown/README.md b/src/components/dropdown/README.md new file mode 100644 index 00000000..e682682a --- /dev/null +++ b/src/components/dropdown/README.md @@ -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'; + +Click Me + } +> + + console.log('Item 1')} /> + console.log('Item 2')} /> + + +``` + +### With Position + +```tsx +Dropdown} + contentClassName="w-52 mt-3" +> + {/* Your content */} + +``` + +### Hover Mode + +```tsx +Hover Me} +> + {/* Your content */} + +``` + +## 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 diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index 119d74cb..dbd4b6bc 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; -import { AxiosError } from 'axios'; +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', + }, + }, + ], + }, + ], +}; interface RequireAuthProps { children?: ReactNode; @@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { - data: userResponse, - isLoading: isLoadingUserResponse, - error: userErrorResponse, - } = useSWRImmutable< - GetMeResponse & { ok?: boolean }, - AxiosError, - SWRHttpKey - >('/sso/userinfo', httpClientFetcher, { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - }); + const { data: userResponse, isLoading: isLoadingUserResponse } = + useSWRImmutable( + '/auth/sso/userinfo', + httpClientFetcher, + { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + } + ); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else if ( - isResponseError(userErrorResponse?.response?.data) && - typeof window !== 'undefined' - ) { - router.replace( - `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` - ); + } else { + // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); + // TODO: remove this later, DONT HARDCODE USER DATA + setUser(DUMMY_USER); } - }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); + }, [userResponse, setIsLoadingUser, setUser]); - if (isLoadingUserResponse && !userResponse && !userErrorResponse) { - return ( -
- -
- ); - } + // TODO: uncomment this later + // if (isLoadingUserResponse && !userResponse) { + // return ( + //
+ // + //
+ // ); + // } - return <>{isResponseSuccess(userResponse) && children}; + return <>{children}; }; export default RequireAuth; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 041108d0..dc0d804a 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -51,4 +51,4 @@ export class ClosingApiService extends BaseApiService { } } -export const ClosingApi = new ClosingApiService('/closing'); +export const ClosingApi = new ClosingApiService('/closings'); From f48cfca65058ab626ace3421acf5f575df90a99b Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 10 Dec 2025 13:35:42 +0700 Subject: [PATCH 2/2] fix(FE): revert require auth component --- src/components/dropdown/README.md | 83 ----------- src/components/helper/RequireAuth.tsx | 199 +++++--------------------- 2 files changed, 33 insertions(+), 249 deletions(-) delete mode 100644 src/components/dropdown/README.md diff --git a/src/components/dropdown/README.md b/src/components/dropdown/README.md deleted file mode 100644 index e682682a..00000000 --- a/src/components/dropdown/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# 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'; - -Click Me - } -> - - console.log('Item 1')} /> - console.log('Item 2')} /> - - -``` - -### With Position - -```tsx -Dropdown} - contentClassName="w-52 mt-3" -> - {/* Your content */} - -``` - -### Hover Mode - -```tsx -Hover Me} -> - {/* Your content */} - -``` - -## 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 diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..119d74cb 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,147 +6,9 @@ import useSWRImmutable from 'swr/immutable'; 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'; interface RequireAuthProps { children?: ReactNode; @@ -156,17 +18,20 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { data: userResponse, isLoading: isLoadingUserResponse } = - useSWRImmutable( - '/auth/sso/userinfo', - httpClientFetcher, - { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - } - ); + const { + data: userResponse, + isLoading: isLoadingUserResponse, + error: userErrorResponse, + } = useSWRImmutable< + GetMeResponse & { ok?: boolean }, + AxiosError, + SWRHttpKey + >('/sso/userinfo', httpClientFetcher, { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + }); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -175,23 +40,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => { 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); + } else if ( + isResponseError(userErrorResponse?.response?.data) && + typeof window !== 'undefined' + ) { + router.replace( + `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` + ); } - }, [userResponse, setIsLoadingUser, setUser]); + }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); - // TODO: uncomment this later - // if (isLoadingUserResponse && !userResponse) { - // return ( - //
- // - //
- // ); - // } + if (isLoadingUserResponse && !userResponse && !userErrorResponse) { + return ( +
+ +
+ ); + } - return <>{children}; + return <>{isResponseSuccess(userResponse) && children}; }; export default RequireAuth;