Add Linked wallets page in account layout (#4542)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates account pages with new components, refactors UI, and adds wallet linking functionality.

### Detailed summary
- Updated components for account teams and wallets
- Added wallet linking feature
- Refactored UI for better user experience

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
MananTank
2024-09-11 23:01:52 +00:00
parent adbac73065
commit c3800d6c07
8 changed files with 212 additions and 35 deletions
@@ -15,7 +15,8 @@ export function SidebarLayout(props: {
links={sidebarLinks}
triggerClassName={props.mobileSidebarClassName}
/>
<main className="grow max-sm:w-full flex flex-col lg:pt-8 pb-10 overflow-hidden min-w-0">
{/* min-w-0 is enough to ensure the content inside does not stretch this container */}
<main className="grow max-sm:w-full flex flex-col lg:pt-8 pb-10 min-w-0">
{children}
</main>
</div>
@@ -0,0 +1,20 @@
import { Input } from "@/components/ui/input";
import { SearchIcon } from "lucide-react";
export function SearchInput(props: {
placeholder: string;
value: string;
onValueChange: (value: string) => void;
}) {
return (
<div className="relative">
<Input
placeholder={props.placeholder}
value={props.value}
onChange={(e) => props.onValueChange(e.target.value)}
className="pl-9 bg-muted/50"
/>
<SearchIcon className="size-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
</div>
);
}
@@ -9,12 +9,13 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { EllipsisIcon, SearchIcon } from "lucide-react";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { EllipsisIcon, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { TeamPlanBadge } from "../../components/TeamPlanBadge";
import { getValidTeamPlan } from "../../team/components/TeamHeader/getValidTeamPlan";
import { SearchInput } from "../components/SearchInput";
export function AccountTeamsUI(props: {
teamsWithRole: {
@@ -33,10 +34,21 @@ export function AccountTeamsUI(props: {
return (
<div>
<h2 className="font-semibold text-xl tracking-tight mb-0.5">Teams</h2>
<p className="text-muted-foreground text-sm">
The teams that are associated with your thirdweb account
</p>
<div className="flex items-start gap-4 flex-col lg:flex-row lg:justify-between">
<div>
<h2 className="font-semibold text-xl tracking-tight mb-0.5">Teams</h2>
<p className="text-muted-foreground text-sm">
The teams that are associated with your thirdweb account
</p>
</div>
<ToolTipLabel label="Coming Soon">
<Button variant="primary" disabled className="max-sm:w-full gap-2">
<PlusIcon className="size-4" />
Create a Team
</Button>
</ToolTipLabel>
</div>
<div className="h-4" />
@@ -66,9 +78,12 @@ export function AccountTeamsUI(props: {
<div className="p-4 h-[200px] justify-center flex items-center">
<div className="text-center">
<p className="mb-3 text-sm">No teams found</p>
<p className="text-sm text-muted-foreground">
Your search for {`"${teamSearchValue}"`} did not match any teams
</p>
{teamSearchValue && (
<p className="text-sm text-muted-foreground">
Your search for {`"${teamSearchValue}"`} did not match any
teams
</p>
)}
</div>
</div>
)}
@@ -77,24 +92,6 @@ export function AccountTeamsUI(props: {
);
}
function SearchInput(props: {
placeholder: string;
value: string;
onValueChange: (value: string) => void;
}) {
return (
<div className="relative">
<Input
placeholder={props.placeholder}
value={props.value}
onChange={(e) => props.onValueChange(e.target.value)}
className="pl-9 bg-muted/50"
/>
<SearchIcon className="size-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
</div>
);
}
function TeamRow(props: {
team: Team;
role: TeamAccountRole;
+2 -2
View File
@@ -1,5 +1,5 @@
import { getTeams } from "../../@/api/team";
import { AccountTeamsUI } from "./components/AccountTeamsUI";
import { getTeams } from "@/api/team";
import { AccountTeamsUI } from "./overview/AccountTeamsUI";
export default async function Page() {
const teams = await getTeams();
@@ -0,0 +1,94 @@
"use client";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Button } from "@/components/ui/button";
import { MinusIcon, PlusIcon } from "lucide-react";
import { useState } from "react";
import { SearchInput } from "../components/SearchInput";
export function LinkWalletUI(props: {
wallets: string[];
}) {
const [searchValue, setSerchValue] = useState("");
const walletsToShow = !searchValue
? props.wallets
: props.wallets.filter((v) => {
return v.toLowerCase().includes(searchValue.toLowerCase());
});
return (
<div>
<div className="flex items-start gap-4 flex-col lg:flex-row lg:justify-between">
<div>
<h2 className="font-semibold text-xl tracking-tight mb-0.5">
Linked Wallets
</h2>
<p className="text-muted-foreground text-sm">
The wallets that are linked to your thirdweb account
</p>
</div>
{/* TODO - handle linking */}
<Button variant="primary" className="max-sm:w-full gap-2">
<PlusIcon className="size-4" />
Link a Wallet
</Button>
</div>
<div className="h-4" />
<SearchInput
placeholder="Search wallet address"
value={searchValue}
onValueChange={setSerchValue}
/>
<div className="h-4" />
<ul className="bg-muted/50 border rounded-lg">
{walletsToShow.map((v) => {
return (
<li key={v} className="border-b border-border p-4 last:border-b-0">
<WalletRow address={v} />
</li>
);
})}
{/* No Result Found */}
{walletsToShow.length === 0 && (
<div className="p-4 h-[200px] justify-center flex items-center">
<div className="text-center">
<p className="mb-3 text-sm">No Wallets Found</p>
{searchValue && (
<p className="text-sm text-muted-foreground">
Your search for {`"${searchValue}"`} did not match any wallets
</p>
)}
</div>
</div>
)}
</ul>
</div>
);
}
function WalletRow(props: {
address: string;
}) {
return (
<div className="flex gap-2 items-center justify-between">
{/* start */}
<WalletAddress
address={props.address}
className="text-muted-foreground"
/>
{/* end */}
{/* TODO - handle unlink */}
<Button size="sm" variant="outline" className="gap-2">
<MinusIcon className="size-4" />
Unlink
</Button>
</div>
);
}
@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ThirdwebProvider } from "thirdweb/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { LinkWalletUI } from "./LinkWalletUI";
const meta = {
title: "Account/LinkWallets",
component: Variants,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Variants>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {
type: "desktop",
},
};
export const Mobile: Story = {
args: {
type: "mobile",
},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Variants() {
return (
<ThirdwebProvider>
<div className="container py-10 max-w-[1100px] mx-auto w-full flex flex-col gap-10">
<BadgeContainer label="4 wallets">
<LinkWalletUI
wallets={[
"0x51696930092b42243dee1077c8dd237074fb28d4", // jns.eth - ens
"0xd8da6bf26964af9d7eed9e03e53415d37aa96045", // vitalik.eth - ens, farcaster, lens
"0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37", // no ens
]}
/>
</BadgeContainer>
<BadgeContainer label="1 wallet">
<LinkWalletUI
wallets={[
"0x51696930092b42243dee1077c8dd237074fb28d4", // jns.eth
]}
/>
</BadgeContainer>
<BadgeContainer label="0 wallets">
<LinkWalletUI wallets={[]} />
</BadgeContainer>
</div>
</ThirdwebProvider>
);
}
@@ -1,9 +1,12 @@
export default function Page() {
import { LinkWalletUI } from "./LinkWalletUI";
export default async function Page() {
return (
<div className="flex items-center justify-center grow">
<h1 className="text-3xl font-medium text-muted-foreground tracking-tight">
Account Wallets
</h1>
<div className="grow">
<LinkWalletUI
// TODO - fetch the wallets of current user
wallets={[]}
/>
</div>
);
}