feat: update API endpoint, add hardcoded auth token, and configure detail page highlight key
This commit is contained in:
+346
@@ -0,0 +1,346 @@
|
||||
import React, { ReactNode, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Title,
|
||||
TitleProps,
|
||||
Breadcrumbs,
|
||||
Anchor,
|
||||
Box,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Flex,
|
||||
Divider,
|
||||
ActionIcon,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { ChevronRight, LucideIcon, Maximize2, Minimize2 } from 'lucide-react'; // <-- Update Import Icon
|
||||
import { PageActions, PageActionsProps } from '../../../../components';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
type: 'text' | 'link';
|
||||
}
|
||||
|
||||
export interface ModulePageHeaderProps {
|
||||
title?: ReactNode;
|
||||
titleProps?: Omit<TitleProps, 'children'>;
|
||||
miniTitleProps?: Omit<TitleProps, 'children'>;
|
||||
description?: React.ReactNode;
|
||||
icon?: LucideIcon;
|
||||
|
||||
badges?: React.ReactNode;
|
||||
showBadges?: boolean;
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
|
||||
showPageHeader?: boolean;
|
||||
disableMinimize?: boolean;
|
||||
moduleKey: string;
|
||||
|
||||
actions?: PageActionsProps['actions'];
|
||||
customButtonProps?: PageActionsProps['customButtonProps'];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared transition style applied to all animated wrappers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TRANSITION_STYLE: React.CSSProperties = {
|
||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sub-components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface BreadcrumbBarProps {
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
function BreadcrumbBar({ breadcrumbs }: BreadcrumbBarProps) {
|
||||
return (
|
||||
<Breadcrumbs
|
||||
style={{ flexWrap: 'wrap' }}
|
||||
// visibleFrom="xs"
|
||||
separator={
|
||||
<ChevronRight
|
||||
size={12}
|
||||
strokeWidth={3}
|
||||
style={{
|
||||
color: 'light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3))',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{breadcrumbs.map((item, index) => {
|
||||
const isText = item.type === 'text';
|
||||
const isLast = index === breadcrumbs.length - 1;
|
||||
const sharedProps = {
|
||||
c: !isLast ? ('dimmed' as const) : undefined,
|
||||
size: 'xs' as const,
|
||||
fw: 500,
|
||||
style: {
|
||||
color: isLast ? 'var(--mantine-color-text)' : undefined,
|
||||
transition: 'color 0.2s ease',
|
||||
letterSpacing: '0.2px',
|
||||
},
|
||||
};
|
||||
|
||||
return !isText ? (
|
||||
<Anchor key={index} {...sharedProps} href={item.href}>
|
||||
{item.label}
|
||||
</Anchor>
|
||||
) : (
|
||||
<Text key={index} {...sharedProps}>
|
||||
{item.label}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Breadcrumbs>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HeaderToggle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface HeaderToggleProps {
|
||||
isMinimized: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
function HeaderToggle({ isMinimized, onToggle }: HeaderToggleProps) {
|
||||
// const ToggleIcon = isMinimized ? ChevronDown : ChevronUp;
|
||||
const ToggleIcon = isMinimized ? Maximize2 : Minimize2;
|
||||
|
||||
const label = isMinimized ? 'Expand header' : 'Collapse header';
|
||||
|
||||
return (
|
||||
<Tooltip label={label} position="bottom-end" withArrow openDelay={400}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
radius="md"
|
||||
onClick={onToggle}
|
||||
aria-label={label}
|
||||
style={{
|
||||
opacity: 0.6,
|
||||
...TRANSITION_STYLE,
|
||||
}}
|
||||
>
|
||||
<ToggleIcon size={13} strokeWidth={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
||||
const {
|
||||
title,
|
||||
titleProps,
|
||||
miniTitleProps,
|
||||
description,
|
||||
icon: Icon,
|
||||
badges,
|
||||
showBadges = true,
|
||||
|
||||
breadcrumbs,
|
||||
actions,
|
||||
customButtonProps,
|
||||
showPageHeader = true,
|
||||
disableMinimize = false,
|
||||
moduleKey,
|
||||
} = _props;
|
||||
|
||||
const [isMinimized, setIsMinimized] = useLocalStorage<boolean>({
|
||||
key: `page-header-minimized__${btoa(moduleKey)}`,
|
||||
defaultValue: false,
|
||||
getInitialValueInEffect: false,
|
||||
});
|
||||
|
||||
if (!showPageHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showToggle = !disableMinimize;
|
||||
const handleToggle = useCallback(() => setIsMinimized((v) => !v), [setIsMinimized]);
|
||||
const compactButtonProps = useMemo(
|
||||
() => (action: any) => {
|
||||
if (customButtonProps) return customButtonProps(action);
|
||||
return { size: 'xs' as const };
|
||||
},
|
||||
[customButtonProps],
|
||||
);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Compact / Toolbar mode
|
||||
// -------------------------------------------------------------------------
|
||||
if (isMinimized) {
|
||||
return (
|
||||
<Box mb={0} mt="xs" pb={6} style={TRANSITION_STYLE} className="module-page-header">
|
||||
<Flex
|
||||
direction="row"
|
||||
justify="space-between"
|
||||
align="center"
|
||||
gap="sm"
|
||||
wrap="nowrap"
|
||||
style={{ minHeight: 36, ...TRANSITION_STYLE }}
|
||||
>
|
||||
{/* Left cluster: title + badges */}
|
||||
<Flex gap="xs" align="center" style={{ flex: 1, minWidth: 0 }}>
|
||||
{title && (
|
||||
<Title
|
||||
fw={600}
|
||||
fz={{ base: 'lg', sm: 'xl' }}
|
||||
lineClamp={1}
|
||||
{...(miniTitleProps ?? {})}
|
||||
style={{
|
||||
color: 'var(--mantine-color-text)',
|
||||
letterSpacing: '-0.2px',
|
||||
lineHeight: 1.3,
|
||||
...TRANSITION_STYLE,
|
||||
...miniTitleProps?.style,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Title>
|
||||
)}
|
||||
{showBadges && badges && <Box style={{ flexShrink: 0 }}>{badges}</Box>}
|
||||
</Flex>
|
||||
|
||||
{/* Right: actions + inline toggle (separated by divider) */}
|
||||
<Flex align="center" gap="sm" style={{ flexShrink: 0 }}>
|
||||
{actions && <PageActions actions={actions} customButtonProps={compactButtonProps} />}
|
||||
{showToggle && (
|
||||
<>
|
||||
{actions && actions.length > 0 && (
|
||||
<Divider orientation="vertical" style={{ height: 20, opacity: 0.4 }} />
|
||||
)}
|
||||
<HeaderToggle isMinimized={isMinimized} onToggle={handleToggle} />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Expanded / Full mode
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Helper variable agar kode lebih bersih
|
||||
const hasBreadcrumbs = breadcrumbs && breadcrumbs.length > 0;
|
||||
|
||||
return (
|
||||
<Box mb={0} mt="xs" pb={18} style={TRANSITION_STYLE} className="module-page-header">
|
||||
{/* 1. Top Navigation Row: Breadcrumbs & Toggle */}
|
||||
{((breadcrumbs && breadcrumbs.length) || showToggle) && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
mb={{ base: showToggle ? 'xs' : 0, sm: hasBreadcrumbs || showToggle ? 'md' : 0 }}
|
||||
>
|
||||
<Box>{hasBreadcrumbs && <BreadcrumbBar breadcrumbs={breadcrumbs} />}</Box>
|
||||
{showToggle && (
|
||||
<Box ml="auto">
|
||||
<HeaderToggle isMinimized={isMinimized} onToggle={handleToggle} />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* 2. Main Header Row – 3-column layout */}
|
||||
<Flex
|
||||
direction="row"
|
||||
justify="space-between"
|
||||
align={{ base: 'stretch', sm: 'flex-start' }}
|
||||
gap={{ base: 'md', sm: 'md' }}
|
||||
wrap="nowrap"
|
||||
style={TRANSITION_STYLE}
|
||||
>
|
||||
<Flex gap="md" align={{ base: 'flex-start', sm: 'flex-start' }} style={{ flex: 1, minWidth: 0 }}>
|
||||
{Icon && (
|
||||
<ThemeIcon
|
||||
w={{ base: 40, sm: 48 }}
|
||||
h={{ base: 40, sm: 48 }}
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="brand"
|
||||
visibleFrom="sm"
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
border: '1px solid light-dark(var(--mantine-color-brand-1), var(--mantine-color-brand-6))',
|
||||
boxShadow: 'light-dark(0 4px 12px rgba(0,0,0,0.03), 0 4px 12px rgba(0,0,0,0.2))',
|
||||
...TRANSITION_STYLE,
|
||||
}}
|
||||
>
|
||||
<Icon size={24} strokeWidth={1.5} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
|
||||
{(title || badges || description) && (
|
||||
<Box style={{ flex: 1, minWidth: 0, ...TRANSITION_STYLE }}>
|
||||
<Flex gap="xs" align="center" wrap="wrap">
|
||||
{title && (
|
||||
<Title
|
||||
order={2}
|
||||
fw={600}
|
||||
lineClamp={1}
|
||||
fz={{ base: 20, sm: 24 }}
|
||||
lh={{ base: 1.3, sm: 1.2 }}
|
||||
{...(titleProps ?? {})}
|
||||
style={{
|
||||
color: 'var(--mantine-color-text)',
|
||||
letterSpacing: '-0.3px',
|
||||
...TRANSITION_STYLE,
|
||||
...titleProps?.style,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Title>
|
||||
)}
|
||||
{showBadges && badges && <Box>{badges}</Box>}
|
||||
</Flex>
|
||||
|
||||
{description && (
|
||||
<Text
|
||||
fz={{ base: 'xs', sm: 'sm' }}
|
||||
c="dimmed"
|
||||
fw={400}
|
||||
lineClamp={1}
|
||||
mt={4}
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
...TRANSITION_STYLE,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
gap="xs"
|
||||
mt={{ base: 'xs', sm: 0 }}
|
||||
ml={{ base: 'lg', sm: 0 }}
|
||||
style={{ flexShrink: 0, alignSelf: 'center' }}
|
||||
>
|
||||
{actions && <PageActions actions={actions} customButtonProps={customButtonProps} />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user