Skip to content

Commit 0b3ad3b

Browse files
committed
feat(web): add quick actions bar to detail pages (task-22)
- Create QuickActionBar component with sticky positioning - One-click bookmark toggle with state indicator - Add to graph action with disabled state when already in graph - Add to list dropdown with catalogue navigation - Display status indicators for bookmarked/graph state - Use canonical icons from tabler-icons-react
1 parent c95c128 commit 0b3ad3b

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/**
2+
* Quick Actions Bar for Entity Detail Pages
3+
*
4+
* Sticky bar below header with one-click actions:
5+
* - Add to bookmarks
6+
* - Add to graph
7+
* - Add to list
8+
* - Show current state indicators
9+
*
10+
* @module components/entity-detail
11+
*/
12+
13+
import type { EntityType } from '@bibgraph/types';
14+
import { ActionIcon, Box, Group, Menu, Text, Tooltip } from '@mantine/core';
15+
import {
16+
IconBookmark,
17+
IconBookmarkFilled,
18+
IconList,
19+
IconListCheck,
20+
IconNetwork,
21+
IconPlus,
22+
} from '@tabler/icons-react';
23+
import { useNavigate } from '@tanstack/react-router';
24+
import { useCallback, useEffect, useState } from 'react';
25+
26+
import { useBookmarks } from '@/hooks/useBookmarks';
27+
import { useCatalogue } from '@/hooks/useCatalogue';
28+
import { useGraphList } from '@/hooks/useGraphList';
29+
30+
interface QuickActionBarProps {
31+
/** Entity ID */
32+
entityId: string;
33+
/** Entity type */
34+
entityType: EntityType;
35+
/** Display name */
36+
displayName: string;
37+
}
38+
39+
/**
40+
* QuickActionBar Component
41+
* @param root0
42+
* @param root0.entityId
43+
* @param root0.entityType
44+
* @param root0.displayName
45+
*/
46+
export const QuickActionBar: React.FC<QuickActionBarProps> = ({
47+
entityId,
48+
entityType,
49+
displayName,
50+
}) => {
51+
const navigate = useNavigate();
52+
const { bookmarks, addBookmark, removeBookmark } = useBookmarks();
53+
const { lists } = useCatalogue();
54+
const { addNode, nodes } = useGraphList();
55+
56+
const [localIsBookmarked, setLocalIsBookmarked] = useState(false);
57+
const [localIsInGraph, _setLocalIsInGraph] = useState(false);
58+
59+
// Check bookmark status
60+
useEffect(() => {
61+
const bookmarked = bookmarks.some((b) => b.id === entityId);
62+
setLocalIsBookmarked(bookmarked);
63+
}, [bookmarks, entityId]);
64+
65+
// Check graph status
66+
useEffect(() => {
67+
// Check if entity is in graph by checking nodes array
68+
const inGraph = nodes.some((n) => n.id === entityId);
69+
_setLocalIsInGraph(inGraph);
70+
}, [nodes, entityId]);
71+
72+
// Handle bookmark toggle
73+
const handleBookmarkToggle = useCallback(async () => {
74+
if (localIsBookmarked) {
75+
await removeBookmark(entityId);
76+
} else {
77+
await addBookmark({
78+
entityType,
79+
entityId,
80+
});
81+
}
82+
}, [localIsBookmarked, removeBookmark, addBookmark, entityId, entityType]);
83+
84+
// Handle add to graph
85+
const handleAddToGraph = useCallback(async () => {
86+
await addNode({
87+
entityId,
88+
entityType,
89+
label: displayName,
90+
provenance: 'user',
91+
});
92+
}, [addNode, entityId, entityType, displayName]);
93+
94+
// Handle add to list
95+
const handleAddToList = useCallback(
96+
(_listId: string) => {
97+
// Navigate to catalogue with list selection
98+
navigate({
99+
to: '/catalogue',
100+
});
101+
},
102+
[navigate]
103+
);
104+
105+
return (
106+
<Box
107+
style={{
108+
position: 'sticky',
109+
top: 0,
110+
zIndex: 100,
111+
backgroundColor: 'var(--mantine-color-body)',
112+
borderBottom: '1px solid var(--mantine-color-gray-3)',
113+
padding: '12px 16px',
114+
}}
115+
>
116+
<Group justify="space-between">
117+
<Group gap="xs">
118+
{/* Bookmark button */}
119+
<Tooltip label={localIsBookmarked ? 'Remove from bookmarks' : 'Add to bookmarks'}>
120+
<ActionIcon
121+
variant={localIsBookmarked ? 'filled' : 'subtle'}
122+
color="blue"
123+
size="lg"
124+
onClick={handleBookmarkToggle}
125+
>
126+
{localIsBookmarked ? <IconBookmarkFilled size={20} /> : <IconBookmark size={20} />}
127+
</ActionIcon>
128+
</Tooltip>
129+
130+
{/* Add to graph button */}
131+
<Tooltip label={localIsInGraph ? 'Already in graph' : 'Add to graph'}>
132+
<ActionIcon
133+
variant={localIsInGraph ? 'filled' : 'subtle'}
134+
color="green"
135+
size="lg"
136+
onClick={handleAddToGraph}
137+
disabled={localIsInGraph}
138+
>
139+
{localIsInGraph ? <IconListCheck size={20} /> : <IconNetwork size={20} />}
140+
</ActionIcon>
141+
</Tooltip>
142+
143+
{/* Add to list dropdown */}
144+
<Menu position="bottom-start">
145+
<Menu.Target>
146+
<ActionIcon variant="subtle" color="orange" size="lg">
147+
<IconList size={20} />
148+
</ActionIcon>
149+
</Menu.Target>
150+
151+
<Menu.Dropdown>
152+
<Menu.Label>
153+
<Text size="sm" fw={500}>
154+
Add to List
155+
</Text>
156+
</Menu.Label>
157+
{lists.length === 0 ? (
158+
<Menu.Item disabled>
159+
<Text size="sm" c="dimmed">
160+
No lists available. Create one in Catalogue.
161+
</Text>
162+
</Menu.Item>
163+
) : (
164+
lists.map((list) => (
165+
<Menu.Item
166+
key={list.id}
167+
leftSection={<IconList size={14} />}
168+
onClick={() => handleAddToList(list.id ?? '')}
169+
>
170+
<Text size="sm">{list.title}</Text>
171+
</Menu.Item>
172+
))
173+
)}
174+
<Menu.Divider />
175+
<Menu.Item
176+
leftSection={<IconPlus size={14} />}
177+
onClick={() => navigate({ to: '/catalogue' })}
178+
>
179+
<Text size="sm">Create New List</Text>
180+
</Menu.Item>
181+
</Menu.Dropdown>
182+
</Menu>
183+
</Group>
184+
185+
{/* Status indicator */}
186+
<Group gap="xs">
187+
{localIsBookmarked && (
188+
<Text size="xs" c="blue">
189+
<IconBookmarkFilled size={12} style={{ verticalAlign: 'middle' }} />
190+
{' '}
191+
Bookmarked
192+
</Text>
193+
)}
194+
{localIsInGraph && (
195+
<Text size="xs" c="green">
196+
<IconListCheck size={12} style={{ verticalAlign: 'middle' }} />
197+
{' '}
198+
In Graph
199+
</Text>
200+
)}
201+
</Group>
202+
</Group>
203+
</Box>
204+
);
205+
};

apps/web/src/components/entity-detail/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export { ErrorState } from './ErrorState';
1313
export { LoadingState } from './LoadingState';
1414
export { NavigationTrail } from './NavigationTrail';
1515
export { PublicationTimeline } from './PublicationTimeline';
16+
export { QuickActionBar } from './QuickActionBar';
1617
export { RelatedEntitiesSection } from './RelatedEntitiesSection';
1718
// Types can be imported directly from './NavigationTrail' if needed

0 commit comments

Comments
 (0)