merge: fix/ux-leaf-e-crosscut into integration/ux-28-gaps (resolve conflicts on tasks-kanban-view, tasks-list-view, task-detail-panel)
This commit is contained in:
@@ -226,6 +226,7 @@ export default function NotesPage() {
|
|||||||
}
|
}
|
||||||
++saveVersion.current;
|
++saveVersion.current;
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
|
const deletedNote = { ...noteToDelete };
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/domains/${domainId}/notes/${noteToDelete.id}`, { method: 'DELETE' });
|
const response = await fetch(`/api/domains/${domainId}/notes/${noteToDelete.id}`, { method: 'DELETE' });
|
||||||
if (!response.ok) throw new Error('Unable to delete note.');
|
if (!response.ok) throw new Error('Unable to delete note.');
|
||||||
@@ -234,7 +235,30 @@ export default function NotesPage() {
|
|||||||
selected?.id === noteToDelete.id ? notes.find((note) => note.id !== noteToDelete.id) || null : selected
|
selected?.id === noteToDelete.id ? notes.find((note) => note.id !== noteToDelete.id) || null : selected
|
||||||
);
|
);
|
||||||
setNoteToDelete(null);
|
setNoteToDelete(null);
|
||||||
toast.success('Note deleted');
|
toast.success('Note deleted', {
|
||||||
|
action: {
|
||||||
|
label: 'Undo',
|
||||||
|
onClick: async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/domains/${domainId}/notes`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: deletedNote.title,
|
||||||
|
content: deletedNote.content,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
|
const restored = await res.json();
|
||||||
|
setNotes((current) => [restored, ...current]);
|
||||||
|
setSelectedNote(restored);
|
||||||
|
toast.success('Note restored');
|
||||||
|
} catch {
|
||||||
|
toast.error('Unable to restore note');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete note:', error);
|
console.error('Failed to delete note:', error);
|
||||||
toast.error('Unable to delete note');
|
toast.error('Unable to delete note');
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Bell, Clock, CheckCircle2, PlusCircle, MessageSquare, Flame } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
|
||||||
|
interface ActivityItem {
|
||||||
|
id: string;
|
||||||
|
actor: string;
|
||||||
|
action: string;
|
||||||
|
entityType: string;
|
||||||
|
entityId: string;
|
||||||
|
changes: Record<string, unknown> | null;
|
||||||
|
workspaceId: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionIcons: Record<string, React.ReactNode> = {
|
||||||
|
created: <PlusCircle className="h-3.5 w-3.5 text-green-500" />,
|
||||||
|
completed: <CheckCircle2 className="h-3.5 w-3.5 text-blue-500" />,
|
||||||
|
logged: <Flame className="h-3.5 w-3.5 text-orange-500" />,
|
||||||
|
commented: <MessageSquare className="h-3.5 w-3.5 text-purple-500" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatTimeAgo(dateStr: string): string {
|
||||||
|
const now = Date.now();
|
||||||
|
const then = new Date(dateStr).getTime();
|
||||||
|
const diffMs = now - then;
|
||||||
|
const diffMin = Math.floor(diffMs / 60000);
|
||||||
|
if (diffMin < 1) return 'just now';
|
||||||
|
if (diffMin < 60) return `${diffMin}m ago`;
|
||||||
|
const diffH = Math.floor(diffMin / 60);
|
||||||
|
if (diffH < 24) return `${diffH}h ago`;
|
||||||
|
const diffD = Math.floor(diffH / 24);
|
||||||
|
if (diffD < 7) return `${diffD}d ago`;
|
||||||
|
return new Date(dateStr).toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionLabel(item: ActivityItem): string {
|
||||||
|
const entity = item.entityType.charAt(0).toUpperCase() + item.entityType.slice(1);
|
||||||
|
switch (item.action) {
|
||||||
|
case 'created': return `${entity} created`;
|
||||||
|
case 'completed': return `${entity} completed`;
|
||||||
|
case 'logged': return `${entity} logged`;
|
||||||
|
case 'commented': return `Comment on ${entity}`;
|
||||||
|
default: return `${item.action} ${item.entityType}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationBell() {
|
||||||
|
const [domainId, setDomainId] = useState<string | null>(null);
|
||||||
|
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const fetchedRef = useRef(false);
|
||||||
|
|
||||||
|
// Auto-detect first domain
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/domains?sort=sort_order')
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
const items = data.items || [];
|
||||||
|
if (items.length > 0) setDomainId(items[0].id);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || fetchedRef.current) return;
|
||||||
|
fetchActivities();
|
||||||
|
}, [open, domainId]);
|
||||||
|
|
||||||
|
async function fetchActivities() {
|
||||||
|
if (!domainId) return;
|
||||||
|
setLoading(true);
|
||||||
|
fetchedRef.current = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/domains/${domainId}/activity?limit=10`);
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
|
const data = await res.json();
|
||||||
|
setActivities(data.items || []);
|
||||||
|
} catch {
|
||||||
|
// Silently fail — non-critical UI
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" aria-label="Notifications">
|
||||||
|
<Bell className="h-5 w-5" aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-80 p-0" align="end">
|
||||||
|
<div className="flex items-center justify-between border-b px-4 py-3">
|
||||||
|
<span className="text-sm font-semibold">Activity</span>
|
||||||
|
{activities.length > 0 && (
|
||||||
|
<span className="text-xs text-muted-foreground">{activities.length} items</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="max-h-80">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<span className="text-sm text-muted-foreground">Loading...</span>
|
||||||
|
</div>
|
||||||
|
) : activities.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<span className="text-sm text-muted-foreground">No recent activity</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y">
|
||||||
|
{activities.map((item) => (
|
||||||
|
<div key={item.id} className="flex items-start gap-3 px-4 py-3">
|
||||||
|
<div className="mt-0.5 shrink-0">
|
||||||
|
{actionIcons[item.action] || <Clock className="h-3.5 w-3.5 text-muted-foreground" />}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm">{actionLabel(item)}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{formatTimeAgo(item.createdAt)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'pe_notification_prefs';
|
||||||
|
|
||||||
|
const defaultPrefs: Record<string, boolean> = {
|
||||||
|
task_created: true,
|
||||||
|
task_completed: true,
|
||||||
|
habit_logged: true,
|
||||||
|
comment_added: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefLabels: Record<string, string> = {
|
||||||
|
task_created: 'Task created',
|
||||||
|
task_completed: 'Task completed',
|
||||||
|
habit_logged: 'Habit logged',
|
||||||
|
comment_added: 'Comment added',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NotificationPrefs() {
|
||||||
|
const [prefs, setPrefs] = useState<Record<string, boolean>>(defaultPrefs);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
setPrefs({ ...defaultPrefs, ...JSON.parse(stored) });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function toggle(key: string) {
|
||||||
|
const updated = { ...prefs, [key]: !prefs[key] };
|
||||||
|
setPrefs(updated);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
||||||
|
} catch {
|
||||||
|
// Storage may be full
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Object.entries(prefLabels).map(([key, label]) => (
|
||||||
|
<div key={key} className="flex items-center justify-between">
|
||||||
|
<Label htmlFor={`notif-${key}`} className="text-sm font-normal">
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
id={`notif-${key}`}
|
||||||
|
checked={prefs[key]}
|
||||||
|
onCheckedChange={() => toggle(key)}
|
||||||
|
aria-label={`Toggle ${label}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { NotificationPrefs } from '@/components/notifications/notification-prefs';
|
||||||
|
|
||||||
export function SettingsShortcuts() {
|
export function SettingsShortcuts() {
|
||||||
const { enabled, shortcuts, setEnabled, resetShortcuts } = useKeyboardShortcutsStore();
|
const { enabled, shortcuts, setEnabled, resetShortcuts } = useKeyboardShortcutsStore();
|
||||||
@@ -49,6 +50,15 @@ export function SettingsShortcuts() {
|
|||||||
<Button variant="outline" onClick={resetShortcuts}>
|
<Button variant="outline" onClick={resetShortcuts}>
|
||||||
Reset to defaults
|
Reset to defaults
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Notification Preferences */}
|
||||||
|
<div className="pt-4 border-t">
|
||||||
|
<h3 className="font-semibold mb-1">Notification Preferences</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Choose which events trigger in-app notifications.
|
||||||
|
</p>
|
||||||
|
<NotificationPrefs />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -413,6 +413,62 @@ export function TaskDetailPanel({
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
|
{/* Time Tracking */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>Time Tracking</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Tracked: {task.trackedMinutes || 0}m / {task.estimatedMinutes || 0}m
|
||||||
|
</span>
|
||||||
|
{task.estimatedMinutes && task.estimatedMinutes > 0 && (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{Math.min(100, Math.round(((task.trackedMinutes || 0) / task.estimatedMinutes) * 100))}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{task.estimatedMinutes && task.estimatedMinutes > 0 && (
|
||||||
|
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-primary transition-all"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, Math.round(((task.trackedMinutes || 0) / task.estimatedMinutes) * 100))}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{timerRunning ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleStopTimer}
|
||||||
|
className="gap-1"
|
||||||
|
>
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
Stop timer
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-mono text-primary">
|
||||||
|
{formatDuration(elapsedSeconds)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleStartTimer}
|
||||||
|
className="gap-1"
|
||||||
|
>
|
||||||
|
<Play className="h-4 w-4" />
|
||||||
|
Start timer
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
{task.tags && task.tags.length > 0 && (
|
{task.tags && task.tags.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Calendar, GripVertical, Link2, Loader2, Plus } from 'lucide-react';
|
import { Calendar, Clock, GripVertical, Link2, Loader2, Plus } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { TaskDetailPanel } from './task-detail-panel';
|
import { TaskDetailPanel } from './task-detail-panel';
|
||||||
import { useRealtimeContext } from '@/components/realtime-provider';
|
import { useRealtimeContext } from '@/components/realtime-provider';
|
||||||
@@ -120,6 +120,15 @@ function TaskCard({
|
|||||||
{new Date(task.dueDate).toLocaleDateString()}
|
{new Date(task.dueDate).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{task.estimatedMinutes && (
|
||||||
|
<span
|
||||||
|
className="flex items-center gap-1 text-xs text-muted-foreground"
|
||||||
|
title={`Estimated: ${task.estimatedMinutes}m`}
|
||||||
|
>
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{task.estimatedMinutes}m
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{task.tags?.length > 0 && (
|
{task.tags?.length > 0 && (
|
||||||
<div className="flex gap-1 flex-wrap">
|
<div className="flex gap-1 flex-wrap">
|
||||||
{task.tags.slice(0, 3).map((tag) => (
|
{task.tags.slice(0, 3).map((tag) => (
|
||||||
|
|||||||
@@ -173,11 +173,15 @@ export function TasksListView({
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [domainMap, setDomainMap] = useState<Map<string, string>>(new Map());
|
const [domainMap, setDomainMap] = useState<Map<string, string>>(new Map());
|
||||||
|
const LIMIT = 50;
|
||||||
const { subscribe } = useRealtimeContext();
|
const { subscribe } = useRealtimeContext();
|
||||||
|
|
||||||
// Filter/sort state from URL params
|
// Filter/sort state from URL params
|
||||||
@@ -224,10 +228,13 @@ export function TasksListView({
|
|||||||
[searchParams, pathname, router]
|
[searchParams, pathname, router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchTasks = useCallback(async () => {
|
const fetchTasks = useCallback(async (appendOffset?: number) => {
|
||||||
if (!domainId) return;
|
if (!domainId) return;
|
||||||
|
if (appendOffset === undefined) setLoading(true);
|
||||||
|
else setLoadingMore(true);
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ sort, order, limit: '200' });
|
const currentOffset = appendOffset ?? 0;
|
||||||
|
const params = new URLSearchParams({ sort, order, limit: String(LIMIT), offset: String(currentOffset) });
|
||||||
if (statusFilter.length > 0) params.set('status', statusFilter.join(','));
|
if (statusFilter.length > 0) params.set('status', statusFilter.join(','));
|
||||||
if (priorityFilter.length > 0) params.set('priority', priorityFilter.join(','));
|
if (priorityFilter.length > 0) params.set('priority', priorityFilter.join(','));
|
||||||
if (searchQuery) params.set('search', searchQuery);
|
if (searchQuery) params.set('search', searchQuery);
|
||||||
@@ -235,15 +242,28 @@ export function TasksListView({
|
|||||||
const response = await fetch(`/api/domains/${domainId}/tasks?${params.toString()}`);
|
const response = await fetch(`/api/domains/${domainId}/tasks?${params.toString()}`);
|
||||||
if (!response.ok) throw new Error('Unable to load tasks');
|
if (!response.ok) throw new Error('Unable to load tasks');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setTasks(data.items || []);
|
if (appendOffset !== undefined) {
|
||||||
|
setTasks(prev => [...prev, ...(data.items || [])]);
|
||||||
|
} else {
|
||||||
|
setTasks(data.items || []);
|
||||||
|
setOffset(0);
|
||||||
|
}
|
||||||
|
setTotalCount(data.totalItems || 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch tasks:', error);
|
console.error('Failed to fetch tasks:', error);
|
||||||
toast.error('Unable to load tasks');
|
toast.error('Unable to load tasks');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setLoadingMore(false);
|
||||||
}
|
}
|
||||||
}, [domainId, sort, order, statusFilter, priorityFilter, searchQuery]);
|
}, [domainId, sort, order, statusFilter, priorityFilter, searchQuery]);
|
||||||
|
|
||||||
|
function loadMore() {
|
||||||
|
const newOffset = offset + LIMIT;
|
||||||
|
setOffset(newOffset);
|
||||||
|
fetchTasks(newOffset);
|
||||||
|
}
|
||||||
|
|
||||||
const fetchDomains = useCallback(async () => {
|
const fetchDomains = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/domains?sort=sort_order');
|
const res = await fetch('/api/domains?sort=sort_order');
|
||||||
@@ -619,6 +639,19 @@ export function TasksListView({
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 text-sm text-muted-foreground">
|
||||||
|
<span>Showing {tasks.length} of {totalCount} tasks</span>
|
||||||
|
{tasks.length < totalCount && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={loadMore}
|
||||||
|
disabled={loadingMore}
|
||||||
|
>
|
||||||
|
{loadingMore ? "Loading..." : "Load more"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<ScrollBar orientation="horizontal" />
|
<ScrollBar orientation="horizontal" />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { useSidebarStore } from '@/lib/stores/use-sidebar-store';
|
import { useSidebarStore } from '@/lib/stores/use-sidebar-store';
|
||||||
import { useCreateDialogStore } from '@/lib/stores/use-create-dialog-store';
|
import { useCreateDialogStore } from '@/lib/stores/use-create-dialog-store';
|
||||||
import { CommandPalette } from '@/components/command-palette';
|
import { CommandPalette } from '@/components/command-palette';
|
||||||
|
import { NotificationBell } from '@/components/notifications/notification-bell';
|
||||||
|
|
||||||
export function TopBar() {
|
export function TopBar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -42,9 +43,7 @@ export function TopBar() {
|
|||||||
<Plus className="mr-1 h-4 w-4" aria-hidden="true" />
|
<Plus className="mr-1 h-4 w-4" aria-hidden="true" />
|
||||||
{label}
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" aria-label="Notifications">
|
<NotificationBell />
|
||||||
<Bell className="h-5 w-5" aria-hidden="true" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user