Files
qapi/src/pages/AppShell.tsx
Victor Oliva 0a97404be2 feat: teleport integration with Paraspell (#153)
* integrate teleport with paraspell

* add some style

* finish up styles

* format token decimals, token input component

* fill in genesis to paraspell map

* add link to paraspell, fix invalid address on destination chain check

* codesplit teleport page

* add button to setup wallet

* chore: update dependencies

* add hash params state

* remove console logs, remove support for negative values in token input
2026-06-11 11:32:44 +02:00

275 lines
7.5 KiB
TypeScript

import {
GlobalCommandPalette,
type NavigationItem,
} from "@/components/GlobalCommandPalette"
import { GithubIcon } from "@/components/Icons"
import SliderToggle from "@/components/Toggle"
import { Button } from "@/components/ui/button"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { HistoryDrawer, HistoryDrawerTrigger } from "@/components/Workspace"
import { Link } from "@/hashParams"
import { cn } from "@/lib/utils"
import { changeTheme, useTheme } from "@/ThemeProvider"
import {
ArrowRightLeft,
BookOpenText,
Cable,
DatabaseSearch,
GitGraph,
Menu,
MoonStar,
Send,
ServerCog,
SquareEqual,
SquareFunction,
UserRound,
} from "lucide-react"
import { FC, PropsWithChildren, useState } from "react"
import { useLocation } from "react-router-dom"
import { twMerge } from "tailwind-merge"
import { NetworkSwitcher } from "./Network/Network"
const navigationGroups: Array<{
label: string
items: NavigationItem[]
}> = [
{
label: "Dev Tools",
items: [
{
path: "/explorer",
label: "Explorer",
// Alts GitFork, GitBranch
icon: GitGraph,
},
{
path: "/storage",
label: "Storage",
icon: DatabaseSearch,
},
{
path: "/extrinsics",
label: "Extrinsics",
icon: Send,
},
{
path: "/runtimeCalls",
label: "Runtime Calls",
// Alt SquareTerminal
icon: ServerCog,
},
{
path: "/viewFns",
label: "View Functions",
icon: SquareFunction,
},
{
path: "/constants",
label: "Constants",
// Alt Braces
icon: SquareEqual,
},
{
path: "/metadata",
label: "Metadata",
// Alt Boxes
icon: BookOpenText,
},
{
path: "/rpcCalls",
label: "RPC Calls",
// Alt RadioTower
icon: Cable,
},
],
},
{
label: "Utilities",
items: [
{
path: "/accounts",
label: "Accounts",
// Alt Wallet
icon: UserRound,
},
{
path: "/teleport",
label: "Teleport",
icon: ArrowRightLeft,
},
],
},
]
const navigationItems = navigationGroups.flatMap((group) => group.items)
export const AppShell: FC<PropsWithChildren> = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(false)
return (
<div className="flex h-screen w-full overflow-hidden bg-background text-foreground">
<aside className="hidden w-60 shrink-0 border-r bg-card/30 lg:flex">
<SidebarContent />
</aside>
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
<SheetContent side="left" className="w-72 bg-background p-0">
<SidebarContent onNavigate={() => setSidebarOpen(false)} mobile />
</SheetContent>
</Sheet>
<div className="flex min-w-0 flex-1 flex-col">
<TopBar onOpenSidebar={() => setSidebarOpen(true)} />
{children}
</div>
<HistoryDrawer />
</div>
)
}
export const CenteredScrollContainer: FC<
PropsWithChildren<{
className?: string
}>
> = ({ className, children }) => (
<div className="relative min-h-0 flex-1 overflow-auto">
<div
className={cn(
"mx-auto h-full w-full max-w-(--breakpoint-xl) @container",
className,
)}
>
{children}
</div>
</div>
)
const SidebarContent: FC<{ mobile?: boolean; onNavigate?: () => void }> = ({
mobile,
onNavigate,
}) => (
<div className="flex h-full w-full flex-col">
<div className="flex h-16 shrink-0 items-center gap-3 border-b px-4">
<img
className="h-10 w-10 shrink-0 hidden dark:inline-block"
src="/papi_logo-dark.svg"
alt="papi logo"
/>
<img
className="h-10 w-10 shrink-0 dark:hidden"
src="/papi_logo-light.svg"
alt="papi logo"
/>
<div className="min-w-0 leading-tight">
<div className="truncate text-base">
<span className="poppins-regular">papi</span>{" "}
<span className="poppins-extralight">console</span>
</div>
<div className="text-xs text-muted-foreground">beta</div>
</div>
</div>
{mobile ? (
<div className="border-b px-3 py-3">
<NetworkSwitcher className="flex w-full md:hidden" />
</div>
) : null}
<nav className="flex-1 overflow-y-auto px-3 py-4">
{navigationGroups.map((group) => (
<div key={group.label} className="mb-5 last:mb-0">
<div className="px-3 pb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
{group.label}
</div>
<div className="space-y-1">
{group.items.map((item) => (
<SidebarLink key={item.path} item={item} onClick={onNavigate} />
))}
</div>
</div>
))}
</nav>
<div className="shrink-0 border-t p-3">
<ThemeToggle />
<a
href="https://github.com/polkadot-api/papi-console"
target="_blank"
rel="noreferrer"
className="mt-2 flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
<GithubIcon size={16} />
GitHub
</a>
</div>
</div>
)
const TopBar: FC<{
onOpenSidebar: () => void
}> = ({ onOpenSidebar }) => {
return (
<header className="flex h-16 shrink-0 items-center border-b bg-background/95 px-3 lg:px-4">
<Button
variant="ghost"
size="icon"
className="mr-2 shrink-0 text-foreground hover:bg-accent lg:hidden"
onClick={onOpenSidebar}
aria-label="Open navigation"
>
<Menu className="h-5 w-5" />
</Button>
<div className="flex-1 flex gap-4 justify-between">
<div className="ml-3 hidden sm:block">
<NetworkSwitcher className="w-55" />
</div>
<div className="flex min-w-0 flex-1 items-center justify-end gap-2">
<GlobalCommandPalette navigationItems={navigationItems} />
<HistoryDrawerTrigger />
</div>
</div>
</header>
)
}
const SidebarLink: FC<{ item: NavigationItem; onClick?: () => void }> = ({
item,
onClick,
}) => {
const location = useLocation()
const active = isNavigationItemActive(location.pathname, item.path)
const Icon = item.icon
return (
<Link
to={item.path}
aria-current={active ? "page" : undefined}
className={twMerge(
"flex h-9 items-center gap-3 rounded-md px-3 text-sm transition-colors",
"text-muted-foreground hover:bg-accent hover:text-accent-foreground",
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
active && "bg-accent text-accent-foreground font-medium",
)}
onClick={onClick}
>
<Icon size={16} className="shrink-0" />
<span className="truncate">{item.label}</span>
</Link>
)
}
const isNavigationItemActive = (pathname: string, path: string) =>
pathname === path || pathname.startsWith(`${path}/`)
const ThemeToggle = () => {
const theme = useTheme()
return (
<label className="flex items-center justify-between rounded-md px-3 py-2 text-sm text-muted-foreground">
<span className="flex items-center gap-3">
<MoonStar size={16} className="shrink-0" />
<span>Dark mode</span>
</span>
<SliderToggle
isToggled={theme === "dark"}
toggle={() => changeTheme(theme === "dark" ? "light" : "dark")}
/>
</label>
)
}