Skip to content

Commit 48cf00c

Browse files
committed
Commit soon and commit often
1 parent ef0e24e commit 48cf00c

25 files changed

Lines changed: 1759 additions & 134 deletions

web/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@
1313
"dependencies": {
1414
"@hookform/resolvers": "^5.2.2",
1515
"@radix-ui/react-accordion": "^1.2.12",
16+
"@radix-ui/react-avatar": "^1.1.10",
1617
"@radix-ui/react-dialog": "^1.1.15",
18+
"@radix-ui/react-dropdown-menu": "^2.1.16",
19+
"@radix-ui/react-icons": "^1.3.2",
1720
"@radix-ui/react-label": "^2.1.7",
1821
"@radix-ui/react-popover": "^1.1.15",
1922
"@radix-ui/react-progress": "^1.1.7",
2023
"@radix-ui/react-select": "^2.2.6",
2124
"@radix-ui/react-slot": "^1.2.3",
25+
"@radix-ui/themes": "^3.2.1",
2226
"@solana/kit": "^3.0.3",
2327
"@solana/react": "^3.0.3",
28+
"@solana/wallet-adapter-react-ui": "^0.9.39",
2429
"@tailwindcss/vite": "^4.1.12",
2530
"@wallet-standard/react": "^1.0.1",
2631
"appwrite": "^20.0.0",
@@ -35,6 +40,7 @@
3540
"react": "^19.1.1",
3641
"react-day-picker": "8.10.1",
3742
"react-dom": "^19.1.1",
43+
"react-error-boundary": "^6.0.0",
3844
"react-hook-form": "^7.62.0",
3945
"react-router-dom": "^7.8.2",
4046
"solana-kite": "^1.7.1",

web/src/appwrite/storage.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export function getImageUrl(fileId: string): string {
2121
bucketId: BUCKET_ID,
2222
fileId: fileId,
2323
});
24-
console.log(url);
2524
return url;
2625
} catch (error) {
2726
console.error('Error getting image URL:', error);

web/src/components/sections/Nav.tsx

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import { Link } from 'react-router-dom';
22
import logo from '@/assets/logo-black.svg';
33
import hamburger from '@/assets/hamburger.svg';
4-
// import wallet from '@/assets/wallet.svg';
5-
6-
import { useState } from 'react';
7-
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
4+
import wallet from '@/assets/wallet.svg';
5+
import { ConnectWalletMenu } from '@/components/solana/ConnectWalletMenu';
86

97
function Nav() {
10-
const [hover, setHover] = useState(false);
11-
128
return (
139
<header
14-
className='absolute z-50 top-[2%] inset-x-0 left-1/2 -translate-x-1/2 flex justify-between items-center bg-main
15-
rounded-[1rem] border-2 p-4 items-center min-w-lg md:min-w-2xl lg:min-w-3xl xl:min-w-5xl mx-auto'
10+
className='fixed z-50 top-[2%] mx-auto left-8 right-8
11+
md:left-16 md:right-16 lg:left-32 lg:right-32
12+
2xl:left-64 2xl:right-64
13+
flex justify-between items-center bg-main
14+
rounded-[1rem] border-2 p-4'
1615
>
1716
<div>
1817
<Link to='/'>
@@ -35,26 +34,11 @@ function Nav() {
3534
{/* All this tomfoolery is to give the button a hover effect */}
3635
<div
3736
className='hidden md:inline font-black button-holder'
38-
onMouseEnter={() => setHover(true)}
39-
onMouseLeave={() => setHover(false)}
4037
>
41-
<WalletMultiButton
42-
style={{
43-
backgroundColor: hover ? 'var(--background)' : 'var(--main)',
44-
border: '2px solid var(--border)',
45-
borderRadius: '10px',
46-
padding: '1rem 1rem 0.85rem 1rem',
47-
fontFamily: 'inherit',
48-
fontWeight: 500,
49-
lineHeight: '.25rem',
50-
height: 'auto',
51-
color: 'var(--border)',
52-
transition: 'all 0.3s ease',
53-
}}
54-
>
55-
{/* <img src={wallet} alt='Wallet' className='inline h-5 mr-1' />
56-
<span className='mt-1'>Connect Wallet</span> */}
57-
</WalletMultiButton>
38+
<ConnectWalletMenu>
39+
<img src={wallet} alt='Wallet' className='inline h-5 mr-1' />
40+
<p className='mt-1'>Connect Wallet</p>
41+
</ConnectWalletMenu>
5842
</div>
5943

6044
<div className='md:hidden block border-2 rounded-md p-1'>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
2+
import { StandardConnect, StandardDisconnect } from "@wallet-standard/core";
3+
import type { UiWallet } from "@wallet-standard/react";
4+
import { uiWalletAccountBelongsToUiWallet, useWallets } from "@wallet-standard/react";
5+
import { useContext, useRef, useState } from "react";
6+
import { Button } from "@/components/ui/button";
7+
import {
8+
DropdownMenu,
9+
DropdownMenuContent,
10+
DropdownMenuTrigger,
11+
} from "@/components/ui/dropdown-menu";
12+
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
13+
14+
import { SelectedWalletAccountContext } from "@/context/SelectedWalletAccountContext";
15+
import { ConnectWalletMenuItem } from "@/components/solana/ConnectWalletMenuItem";
16+
import { ErrorDialog } from "@/components/solana/ErrorDialog";
17+
import { WalletAccountIcon } from "@/components/solana/WalletAccountIcon";
18+
19+
type Props = Readonly<{
20+
children: React.ReactNode;
21+
}>;
22+
23+
export function ConnectWalletMenu({ children }: Props) {
24+
const { current: NO_ERROR } = useRef(Symbol());
25+
const wallets = useWallets();
26+
const [selectedWalletAccount, setSelectedWalletAccount] = useContext(SelectedWalletAccountContext);
27+
const [error, setError] = useState(NO_ERROR);
28+
const [forceClose, setForceClose] = useState(false);
29+
function renderItem(wallet: UiWallet) {
30+
return (
31+
<ConnectWalletMenuItem
32+
onAccountSelect={(account) => {
33+
setSelectedWalletAccount(account);
34+
setForceClose(true);
35+
}}
36+
onDisconnect={(wallet) => {
37+
if (selectedWalletAccount && uiWalletAccountBelongsToUiWallet(selectedWalletAccount, wallet)) {
38+
setSelectedWalletAccount(undefined);
39+
}
40+
}}
41+
onError={setError}
42+
wallet={wallet}
43+
key={wallet.name}
44+
/>
45+
);
46+
}
47+
const supportedWallets = [];
48+
for (const wallet of wallets) {
49+
if (wallet.features.includes(StandardConnect)
50+
&& wallet.features.includes(StandardDisconnect)
51+
&& wallet.chains.includes('solana:devnet')) {
52+
supportedWallets.push(wallet);
53+
}
54+
}
55+
return (
56+
<div className="relative">
57+
<DropdownMenu open={forceClose ? false : undefined} onOpenChange={setForceClose.bind(null, false)}>
58+
<DropdownMenuTrigger asChild>
59+
<Button
60+
size='sm'
61+
variant='noShadow'
62+
className='hover:bg-[var(--main-dark)]'
63+
>
64+
{selectedWalletAccount ? (
65+
<>
66+
<WalletAccountIcon account={selectedWalletAccount} width="18" height="18" />
67+
{selectedWalletAccount.address.slice(0, 8)}
68+
</>
69+
) : (
70+
children
71+
)}
72+
</Button>
73+
</DropdownMenuTrigger>
74+
<DropdownMenuContent>
75+
{supportedWallets.length === 0 ? (
76+
<Alert color="orange">
77+
<ExclamationTriangleIcon />
78+
<AlertTitle>No Wallets Found</AlertTitle>
79+
<AlertDescription>This browser has no wallets installed that support solana devnet.</AlertDescription>
80+
</Alert>
81+
) : (
82+
<>
83+
{supportedWallets.map(renderItem)}
84+
</>
85+
)}
86+
</DropdownMenuContent>
87+
</DropdownMenu>
88+
{error !== NO_ERROR ? <ErrorDialog error={error} onClose={() => setError(NO_ERROR)} /> : null}
89+
</div>
90+
);
91+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
DropdownMenuSub,
3+
DropdownMenuSubContent,
4+
DropdownMenuSubTrigger,
5+
DropdownMenuLabel,
6+
DropdownMenuSeparator,
7+
DropdownMenuRadioGroup,
8+
DropdownMenuRadioItem,
9+
DropdownMenuItem,
10+
} from "@/components/ui/dropdown-menu";
11+
12+
import type { UiWallet, UiWalletAccount } from "@wallet-standard/react";
13+
import { uiWalletAccountsAreSame, useConnect, useDisconnect } from "@wallet-standard/react";
14+
import { useCallback, useContext } from "react";
15+
16+
import { SelectedWalletAccountContext } from "@/context/SelectedWalletAccountContext";
17+
import { WalletMenuItemContent } from "@/components/solana/WalletMenuItemContent";
18+
19+
type Props = Readonly<{
20+
onAccountSelect(account: UiWalletAccount | undefined): void;
21+
onDisconnect(wallet: UiWallet): void;
22+
onError(error: unknown): void;
23+
wallet: UiWallet;
24+
}>;
25+
26+
export function ConnectWalletMenuItem({ onAccountSelect, onDisconnect, onError, wallet }: Props) {
27+
const [isConnecting, connect] = useConnect(wallet);
28+
const [isDisconnecting, disconnect] = useDisconnect(wallet);
29+
const isPending = isConnecting || isDisconnecting;
30+
const isConnected = wallet.accounts.length > 0;
31+
const [selectedWalletAccount] = useContext(SelectedWalletAccountContext);
32+
const handleConnectClick = useCallback(async () => {
33+
try {
34+
const existingAccounts = [...wallet.accounts];
35+
const nextAccounts = await connect();
36+
// Try to choose the first never-before-seen account.
37+
for (const nextAccount of nextAccounts) {
38+
if (!existingAccounts.some((existingAccount) => uiWalletAccountsAreSame(nextAccount, existingAccount))) {
39+
onAccountSelect(nextAccount);
40+
return;
41+
}
42+
}
43+
// Failing that, choose the first account in the list.
44+
if (nextAccounts[0]) {
45+
onAccountSelect(nextAccounts[0]);
46+
}
47+
} catch (error) {
48+
onError(error);
49+
}
50+
}, [connect, onAccountSelect, onError, wallet.accounts]);
51+
return (
52+
<DropdownMenuSub open={!isConnected ? false : undefined}>
53+
<DropdownMenuSubTrigger
54+
disabled={isPending}
55+
onClick={!isConnected ? handleConnectClick : undefined}
56+
>
57+
<WalletMenuItemContent wallet={wallet} />
58+
</DropdownMenuSubTrigger>
59+
<DropdownMenuSubContent>
60+
<DropdownMenuLabel>Accounts</DropdownMenuLabel>
61+
<DropdownMenuRadioGroup value={selectedWalletAccount?.address}>
62+
{wallet.accounts.map((account) => (
63+
<DropdownMenuRadioItem
64+
key={account.address}
65+
value={account.address}
66+
onSelect={() => {
67+
onAccountSelect(account);
68+
}}
69+
>
70+
{account.address.slice(0, 8)}&hellip;
71+
</DropdownMenuRadioItem>
72+
))}
73+
</DropdownMenuRadioGroup>
74+
<DropdownMenuSeparator />
75+
<DropdownMenuItem
76+
onSelect={async (event) => {
77+
event.preventDefault();
78+
await handleConnectClick();
79+
}}
80+
>
81+
Connect More
82+
</DropdownMenuItem>
83+
<DropdownMenuItem
84+
color="red"
85+
onSelect={async (event) => {
86+
event.preventDefault();
87+
try {
88+
await disconnect();
89+
onDisconnect(wallet);
90+
} catch (error) {
91+
onError(error);
92+
}
93+
}}
94+
>
95+
Disconnect
96+
</DropdownMenuItem>
97+
</DropdownMenuSubContent>
98+
</DropdownMenuSub>
99+
);
100+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useEffect, useRef } from "react";
2+
import { getErrorMessage } from "@/lib/errors";
3+
4+
type Props = Readonly<{
5+
error: unknown;
6+
onClose?(): false | void;
7+
title?: string;
8+
}>;
9+
10+
export function ErrorDialog({ error, onClose, title }: Props) {
11+
const dialogRef = useRef<HTMLDialogElement>(null);
12+
13+
useEffect(() => {
14+
const dialog = dialogRef.current;
15+
if (dialog && !dialog.open) {
16+
dialog.showModal();
17+
}
18+
}, []);
19+
20+
return (
21+
<dialog
22+
ref={dialogRef}
23+
onClose={() => {
24+
if (!onClose || onClose() !== false) {
25+
dialogRef.current?.close();
26+
}
27+
}}
28+
style={{
29+
padding: '24px',
30+
borderRadius: '8px',
31+
border: '1px solid #ccc',
32+
maxWidth: '90vw',
33+
maxHeight: '90vh',
34+
overflow: 'auto'
35+
}}
36+
>
37+
<h2 style={{ color: 'red', margin: '0 0 16px 0' }}>{title ?? "We encountered the following error"}</h2>
38+
<div>
39+
<blockquote style={{
40+
borderLeft: '4px solid #ccc',
41+
margin: '8px 0',
42+
padding: '8px 16px',
43+
fontStyle: 'italic'
44+
}}>{getErrorMessage(error, "Unknown")}</blockquote>
45+
</div>
46+
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '16px' }}>
47+
<button
48+
onClick={() => dialogRef.current?.close()}
49+
style={{
50+
padding: '8px 16px',
51+
border: 'none',
52+
borderRadius: '4px',
53+
backgroundColor: '#f5f5f5',
54+
cursor: 'pointer'
55+
}}
56+
>
57+
Close
58+
</button>
59+
</div>
60+
</dialog>
61+
);
62+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { UiWalletAccount } from "@wallet-standard/react";
2+
import { uiWalletAccountBelongsToUiWallet, useWallets } from "@wallet-standard/react";
3+
import React from "react";
4+
5+
type Props = React.ComponentProps<"img"> &
6+
Readonly<{
7+
account: UiWalletAccount;
8+
}>;
9+
10+
export function WalletAccountIcon({ account, ...imgProps }: Props) {
11+
const wallets = useWallets();
12+
let icon;
13+
if (account.icon) {
14+
icon = account.icon;
15+
} else {
16+
for (const wallet of wallets) {
17+
if (uiWalletAccountBelongsToUiWallet(account, wallet)) {
18+
icon = wallet.icon;
19+
break;
20+
}
21+
}
22+
}
23+
return icon ? <img src={icon} {...imgProps} /> : null;
24+
}

0 commit comments

Comments
 (0)