Files
trackgo-fe/apps/web/src/apps/modules/layouts/components/bookmark/index.tsx
T

149 lines
4.8 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Drawer, Text, Stack, NavLink, ThemeIcon, ActionIcon, ScrollArea, Group } from '@repo/ui/components';
import { useAppEvent } from '@repo/core-events';
import { useTranslation } from '@repo/core-i18n';
import { Bookmark, Trash } from 'lucide-react';
import { LAYOUT_EVENTS } from '../../../../../core/constants/events';
import { appDatabase, AppDatabaseKey } from '../../../../../core/storage/local';
import { Link } from 'react-router-dom';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import type { SavedPageItem } from '../../types/saved-page.types';
// Setup relative time formatting
dayjs.extend(relativeTime);
export function BookmarkDrawer() {
const { t } = useTranslation();
const [opened, setOpened] = useState(false);
const [items, setItems] = useState<SavedPageItem[]>([]);
// Listen to the global event bus to toggle the drawer
useAppEvent(LAYOUT_EVENTS.TOGGLE_BOOKMARK_DRAWER, () => {
setOpened((prev) => !prev);
});
const loadData = async () => {
try {
const data = await appDatabase.getItem<SavedPageItem[]>(AppDatabaseKey.BOOKMARK_PAGE);
setItems(data || []);
} catch (e) {
console.error('Failed to load bookmark items', e);
setItems([]);
}
};
useEffect(() => {
if (opened) {
loadData();
}
}, [opened]);
const handleRemoveItem = async (id: string, e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
try {
const newItems = items.filter((item) => item.id !== id);
await appDatabase.setItem(AppDatabaseKey.BOOKMARK_PAGE, newItems);
setItems(newItems);
} catch (err) {
console.error('Failed to remove bookmark item', err);
}
};
const handleClearAll = async () => {
try {
await appDatabase.removeItem(AppDatabaseKey.BOOKMARK_PAGE);
setItems([]);
} catch (err) {
console.error('Failed to clear bookmarks', err);
}
};
return (
<Drawer
opened={opened}
onClose={() => setOpened(false)}
position="right"
size="sm"
title={
<Group gap="sm">
<Bookmark size={18} style={{ color: 'var(--mantine-color-text)' }} />
<Text fw={600} size="lg">
{t('bookmark:title')}
</Text>
</Group>
}
styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: 'calc(100vh - 60px)' } }}
>
{items.length === 0 ? (
<Text c="dimmed" size="sm" ta="center" mt="xl" p="md">
{t('bookmark:empty')}
</Text>
) : (
<>
<Group justify="flex-end" px="md" py="xs">
<Text
size="xs"
c="var(--mantine-color-error-6)"
style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '4px' }}
onClick={handleClearAll}
>
<Trash size={12} /> {t('bookmark:clearAll')}
</Text>
</Group>
<ScrollArea flex={1} type="scroll" px="xs" py="xs" pb={'xl'}>
<Stack gap={2}>
{items.map((item) => (
<NavLink
key={item.id}
component={Link as any}
to={item.path}
onClick={() => setOpened(false)}
label={
<Text size="sm" truncate="end" fw={500} lineClamp={1}>
{item.title}
</Text>
}
description={
<Stack gap={2}>
<Text size="xs" c="dimmed" truncate="end" mt={2}>
{item.path}
</Text>
</Stack>
}
leftSection={
<ThemeIcon variant="transparent" c="dimmed" size="sm">
<Bookmark size={16} />
</ThemeIcon>
}
rightSection={
<ActionIcon
variant="subtle"
color="var(--mantine-color-error-6)"
size="sm"
onClick={(e) => {
e.preventDefault();
handleRemoveItem(item.id, e);
}}
aria-label={t('bookmark:removeItem')}
>
<Trash size={14} />
</ActionIcon>
}
styles={{
root: {
padding: '8px 12px',
borderBottom: '1px solid var(--mantine-color-default-border)',
},
}}
/>
))}
</Stack>
</ScrollArea>
</>
)}
</Drawer>
);
}