From a935ffd9f5692845acc10afe4927f63b439b3b46 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 18 Dec 2025 11:33:18 +0700 Subject: [PATCH] fix(FE): fixing floating button & revert require auth component --- src/components/FloatingActionsButton.tsx | 4 +- src/components/Navbar.tsx | 7 +- src/components/dropdown/Dropdown.tsx | 166 ++++++++-------- src/components/helper/RequireAuth.tsx | 238 ++++++----------------- 4 files changed, 154 insertions(+), 261 deletions(-) diff --git a/src/components/FloatingActionsButton.tsx b/src/components/FloatingActionsButton.tsx index c9ca3454..2e4eed07 100644 --- a/src/components/FloatingActionsButton.tsx +++ b/src/components/FloatingActionsButton.tsx @@ -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') => { diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index bee92a57..918122d2 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -54,7 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
@@ -62,7 +63,9 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
} - contentClassName='w-52 mt-3' + className={{ + content: 'w-52 mt-3', + }} > diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx index 4489231d..5bfa7a7d 100644 --- a/src/components/dropdown/Dropdown.tsx +++ b/src/components/dropdown/Dropdown.tsx @@ -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(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 ( +
+ {trigger} + {open && !close && ( +
+ {children} +
+ )} +
+ ); + } + return ( -
- {/* Trigger Button */} -
+
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleDropdown(); + } + }} + > {trigger}
- - {/* Dropdown Content - Only render when open */} - {isOpen && ( -
setIsOpen(false)} // Close on item click - > + {!close && ( +
{children}
)} diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..65adf48c 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -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( - '/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, + 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 ( - //
- // - //
- // ); - // } + // 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 ( +
+ +
+ ); + } + + if (userErrorResponse) { + return ( +
+

Authentication Failed

+

+ Please try refreshing the page or contact support if the problem + persists. +

+ +
+ ); + } + + return <>{isResponseSuccess(userResponse) && user && children}; }; export default RequireAuth;