Skip to content

Commit 8a3f41a

Browse files
committed
refactor: rebuild sidebar
1 parent 540f87c commit 8a3f41a

File tree

9 files changed

+249
-72
lines changed

9 files changed

+249
-72
lines changed

app/queue/page.tsx

Whitespace-only changes.

app/transactions/queue/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function TransactionsQueuePage() {
2+
return <div>TransactionsQueuePage</div>;
3+
}

components/Breadcrumb/AppBreadcrumb.tsx

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import {
1313
} from '@/components/ui/breadcrumb';
1414
import { Separator } from '@/components/ui/separator';
1515

16-
const routeConfig: Record<string, { label: string }> = {
16+
const routeConfig: Record<string, { label: string; clickable?: boolean }> = {
1717
'/': { label: 'Home' },
1818
'/assets': { label: 'Assets' },
1919
'/wallets': { label: 'Wallets' },
2020
'/wallets/create': { label: 'Create Wallet' },
21-
'/transactions': { label: 'Transactions' },
21+
'/transactions': { label: 'Transactions', clickable: false },
22+
'/transactions/queue': { label: 'Queue' },
23+
'/transactions/history': { label: 'History' },
2224
'/settings': { label: 'Settings' },
2325
};
2426

@@ -35,15 +37,6 @@ function getPathSegments(pathname: string): string[] {
3537
return paths;
3638
}
3739

38-
function isHexAddress(segment: string): boolean {
39-
return /^0x[a-fA-F0-9]{40}$/.test(segment);
40-
}
41-
42-
function getLastSegment(path: string): string {
43-
const parts = path.split('/').filter(Boolean);
44-
return parts[parts.length - 1] || '';
45-
}
46-
4740
export function AppBreadcrumb() {
4841
const pathname = usePathname();
4942

@@ -53,7 +46,7 @@ export function AppBreadcrumb() {
5346

5447
const segments = getPathSegments(pathname);
5548

56-
// Build breadcrumb entries including dynamic segments
49+
// Build breadcrumb entries
5750
const items = segments.map((path, index) => {
5851
const isLast = index === segments.length - 1;
5952

@@ -64,45 +57,16 @@ export function AppBreadcrumb() {
6457
label: routeConfig[path].label,
6558
href: path,
6659
isLast,
67-
clickable: !isLast,
60+
clickable: !isLast && routeConfig[path].clickable !== false,
6861
};
6962
}
7063

71-
// Dynamic wallet address: /wallets/:address
64+
// Fallback: show the path segment
7265
const parts = path.split('/').filter(Boolean);
73-
if (parts.length >= 2 && parts[0] === 'wallets') {
74-
const last = parts[parts.length - 1];
75-
const second = parts[1];
76-
77-
// /wallets/:address
78-
if (parts.length === 2 && isHexAddress(second)) {
79-
const label = second; // future: replace with ENS when available
80-
return {
81-
key: path,
82-
label,
83-
href: path,
84-
isLast,
85-
clickable: !isLast,
86-
};
87-
}
88-
89-
// /wallets/:address/edit
90-
if (parts.length === 3 && isHexAddress(second) && last === 'edit') {
91-
return {
92-
key: path,
93-
label: 'Edit',
94-
href: path,
95-
isLast,
96-
clickable: false, // last crumb not clickable
97-
};
98-
}
99-
}
100-
101-
// Fallback: show the last segment text
102-
const fallbackLabel = getLastSegment(path);
66+
const fallbackLabel = parts[parts.length - 1] || path;
10367
return {
10468
key: path,
105-
label: fallbackLabel || path,
69+
label: fallbackLabel,
10670
href: path,
10771
isLast,
10872
clickable: !isLast,
@@ -117,12 +81,14 @@ export function AppBreadcrumb() {
11781
{items.map((item) => (
11882
<React.Fragment key={item.key}>
11983
<BreadcrumbItem>
120-
{item.isLast || !item.clickable ? (
84+
{item.isLast ? (
12185
<BreadcrumbPage>{item.label}</BreadcrumbPage>
122-
) : (
86+
) : item.clickable ? (
12387
<BreadcrumbLink asChild>
12488
<Link href={item.href}>{item.label}</Link>
12589
</BreadcrumbLink>
90+
) : (
91+
<BreadcrumbPage className='cursor-default text-muted-foreground'>{item.label}</BreadcrumbPage>
12692
)}
12793
</BreadcrumbItem>
12894
{!item.isLast && <BreadcrumbSeparator />}

components/Sidebar/AppSiderbarContent.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,19 @@ import {
99
SidebarGroupContent,
1010
SidebarMenuItem,
1111
} from '@/components/ui/sidebar';
12-
import { ArrowLeftRight, Coins, Gauge, HelpCircle, Search, Settings, Wallet } from 'lucide-react';
12+
import {
13+
ArrowLeftRight,
14+
Clock,
15+
Coins,
16+
Gauge,
17+
HelpCircle,
18+
History,
19+
ListChecks,
20+
ListTodo,
21+
Search,
22+
Settings,
23+
Wallet,
24+
} from 'lucide-react';
1325
import { usePathname, useRouter } from 'next/navigation';
1426
import SafeWalletGroup from './SafeWalletGroup';
1527

@@ -29,9 +41,17 @@ const baseItems = [
2941
url: '/assets',
3042
icon: Coins,
3143
},
44+
];
45+
46+
const transactionItems = [
47+
{
48+
title: 'Queue',
49+
url: '/transactions/queue',
50+
icon: ListChecks,
51+
},
3252
{
33-
title: 'Transactions',
34-
url: '/transactions',
53+
title: 'History',
54+
url: '/transactions/history',
3555
icon: ArrowLeftRight,
3656
},
3757
];
@@ -78,7 +98,24 @@ export function AppSidebarContent() {
7898
</SidebarMenu>
7999
</SidebarGroupContent>
80100
</SidebarGroup>
81-
<SafeWalletGroup />
101+
<SidebarGroup>
102+
<SidebarGroupLabel>Transactions</SidebarGroupLabel>
103+
<SidebarGroupContent className='cursor-pointer'>
104+
<SidebarMenu>
105+
{transactionItems.map((item) => (
106+
<SidebarMenuItem key={item.title}>
107+
<SidebarMenuButton asChild isActive={pathname === item.url} onClick={() => router.push(item.url)}>
108+
<a>
109+
<item.icon />
110+
<span>{item.title}</span>
111+
</a>
112+
</SidebarMenuButton>
113+
</SidebarMenuItem>
114+
))}
115+
</SidebarMenu>
116+
</SidebarGroupContent>
117+
</SidebarGroup>
118+
{/* <SafeWalletGroup /> */}
82119
</div>
83120

84121
<SidebarGroup className=''>
Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,12 @@
11
'use client';
22

3-
import { SidebarMenu, SidebarHeader, SidebarMenuItem, SidebarMenuButton } from '@/components/ui/sidebar';
4-
import { Radius, Shield } from 'lucide-react';
5-
import { useRouter } from 'next/navigation';
3+
import { SidebarHeader } from '@/components/ui/sidebar';
4+
import { SafeWalletSelect } from './SafeWalletSelect';
65

76
export function AppSidebarHeader() {
8-
const router = useRouter();
9-
107
return (
11-
<SidebarHeader className='h-12 border-b'>
12-
<SidebarMenu>
13-
<SidebarMenuItem>
14-
<SidebarMenuButton
15-
onClick={() => router.push('/')}
16-
className='font-semibold text-base flex items-center gap-2 hover:text-primary transition-colors
17-
18-
'
19-
>
20-
<Radius className='text-primary' style={{ width: '20px', height: '20px' }} />
21-
<span>Safe Desktop</span>
22-
</SidebarMenuButton>
23-
</SidebarMenuItem>
24-
</SidebarMenu>
8+
<SidebarHeader className='h-12'>
9+
<SafeWalletSelect />
2510
</SidebarHeader>
2611
);
2712
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { ChevronDown, Plus, Wallet, Shield } from 'lucide-react';
5+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
6+
import { Button } from '@/components/ui/button';
7+
import { cn } from '@/lib/utils';
8+
import { useRouter } from 'next/navigation';
9+
import { useSelectedSafeAddress, useOwnerSafeAddress } from '@/providers/SafeProvider';
10+
import { useWallet } from '@/hooks/useWallet';
11+
12+
export function SafeWalletSelect() {
13+
const { selectedSafeAddress, setSelectedSafeAddress } = useSelectedSafeAddress();
14+
const ownerSafeAddress = useOwnerSafeAddress();
15+
const { account: eoaAddress, isConnected } = useWallet();
16+
const router = useRouter();
17+
18+
const handleSafeSelect = async (safeAddress: string) => {
19+
setSelectedSafeAddress(safeAddress);
20+
};
21+
22+
const handleCreateSafe = () => {
23+
router.push('/wallets/create');
24+
};
25+
26+
const handleConnectWallet = () => {
27+
// TODO: Implement wallet connection logic
28+
console.log('Connect wallet clicked');
29+
};
30+
31+
const formatAddress = (address: string) => {
32+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
33+
};
34+
35+
const getAddressColor = (address: string) => {
36+
const hash = address.slice(2).toLowerCase();
37+
const colors = [
38+
'bg-red-500',
39+
'bg-blue-500',
40+
'bg-green-500',
41+
'bg-yellow-500',
42+
'bg-purple-500',
43+
'bg-pink-500',
44+
'bg-indigo-500',
45+
'bg-teal-500',
46+
'bg-orange-500',
47+
'bg-cyan-500',
48+
];
49+
const index = parseInt(hash.slice(0, 8), 16) % colors.length;
50+
return colors[index];
51+
};
52+
53+
const getAddressAvatar = (address: string) => {
54+
return address.slice(2, 4).toUpperCase();
55+
};
56+
57+
// EOA not connected state
58+
if (!isConnected) {
59+
return (
60+
<div className='h-14 px-4 flex items-center gap-2'>
61+
<Wallet className='w-4 h-4 text-muted-foreground' />
62+
<span className='text-sm text-muted-foreground'>Connect Wallet to Continue</span>
63+
</div>
64+
);
65+
}
66+
67+
// EOA connected but no Safe wallets
68+
if (ownerSafeAddress.length === 0) {
69+
return (
70+
<div className='h-14 px-4 flex items-center gap-3'>
71+
<div className='flex items-center gap-2'>
72+
<Shield className='w-4 h-4 text-muted-foreground' />
73+
<span className='text-sm text-muted-foreground'>No Safe Wallets</span>
74+
</div>
75+
<Button variant='outline' size='sm' onClick={handleCreateSafe} className='h-8 px-3'>
76+
<Plus className='w-3 h-3 mr-1' />
77+
Create
78+
</Button>
79+
</div>
80+
);
81+
}
82+
83+
// EOA connected and has Safe wallets
84+
return (
85+
<Select value={selectedSafeAddress || undefined} onValueChange={handleSafeSelect}>
86+
<SelectTrigger className='h-14 px-4 w-auto min-w-[200px] border-0 bg-transparent hover:bg-accent transition-colors'>
87+
<div className='flex items-center gap-3'>
88+
<div
89+
className={cn(
90+
'w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium text-white',
91+
getAddressColor(selectedSafeAddress || ownerSafeAddress[0])
92+
)}
93+
>
94+
{getAddressAvatar(selectedSafeAddress || ownerSafeAddress[0])}
95+
</div>
96+
<div className='flex flex-col items-start'>
97+
<span className='text-sm font-medium'>{formatAddress(selectedSafeAddress || ownerSafeAddress[0])}</span>
98+
<span className='text-xs text-muted-foreground'>Safe Wallet</span>
99+
</div>
100+
</div>
101+
</SelectTrigger>
102+
<SelectContent>
103+
{ownerSafeAddress.map((safeAddr) => (
104+
<SelectItem key={safeAddr} value={safeAddr}>
105+
<div className='flex items-center gap-3'>
106+
<div
107+
className={cn(
108+
'w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium text-white',
109+
getAddressColor(safeAddr)
110+
)}
111+
>
112+
{getAddressAvatar(safeAddr)}
113+
</div>
114+
<span>{formatAddress(safeAddr)}</span>
115+
</div>
116+
</SelectItem>
117+
))}
118+
<div className='border-t pt-2 mt-2'>
119+
<Button variant='ghost' size='sm' onClick={handleCreateSafe} className='w-full justify-start h-8'>
120+
<Plus className='w-3 h-3 mr-2' />
121+
Create New Safe
122+
</Button>
123+
</div>
124+
</SelectContent>
125+
</Select>
126+
);
127+
}

0 commit comments

Comments
 (0)