Skip to content

Commit dae035b

Browse files
committed
add landing page, update installation tutorial
1 parent 563a11a commit dae035b

22 files changed

Lines changed: 462 additions & 325 deletions

packages/dashboard/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
"lucide-react": "^1.7.0",
2424
"motion": "^12.38.0",
2525
"react": "^19.1.0",
26-
"recharts": "^2.15.0",
2726
"react-dom": "^19.1.0",
2827
"react-router": "^7.6.0",
28+
"recharts": "^2.15.0",
29+
"sonner": "^2.0.7",
2930
"tailwind-merge": "^3.5.0",
3031
"tw-animate-css": "^1.4.0",
3132
"viem": "^2.47.0",

packages/dashboard/src/App.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import { ChannelDetailPage } from "./pages/channel-detail";
88
import { DeviceDetailPage } from "./pages/device-detail";
99
import { DeviceGroupsPage } from "./pages/device-groups";
1010
import { GroupDetailPage } from "./pages/group-detail";
11+
import { HomePage } from "./pages/home";
1112
import { OverviewPage } from "./pages/overview";
1213
import { SkillsPage } from "./pages/skills";
1314
import { InstallationPage } from "./pages/installation";
15+
import { Toaster } from "./components/ui/sonner";
1416

1517
const queryClient = new QueryClient();
1618

@@ -20,18 +22,20 @@ export function App() {
2022
<QueryClientProvider client={queryClient}>
2123
<BrowserRouter>
2224
<Routes>
25+
<Route index element={<HomePage />} />
2326
<Route element={<AppLayout />}>
24-
<Route index element={<OverviewPage />} />
27+
<Route path="overview" element={<OverviewPage />} />
2528
<Route path="groups" element={<DeviceGroupsPage />} />
2629
<Route path="groups/:address" element={<GroupDetailPage />} />
2730
<Route path="devices/:address" element={<DeviceDetailPage />} />
2831
<Route path="channels/:address" element={<ChannelDetailPage />} />
2932
<Route path="agents" element={<AgentsPage />} />
3033
<Route path="skills" element={<SkillsPage />} />
31-
<Route path="installation" element={<InstallationPage />} />
34+
<Route path="setup" element={<InstallationPage />} />
3235
</Route>
3336
</Routes>
3437
</BrowserRouter>
38+
<Toaster />
3539
</QueryClientProvider>
3640
</WagmiProvider>
3741
);
Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { Outlet } from "react-router";
22
import { Header } from "./header";
3+
import { HeaderActionsProvider } from "./header-context";
34
import { Sidebar } from "./sidebar";
45

56
export function AppLayout() {
67
return (
7-
<div className="flex h-screen bg-background">
8-
<Sidebar />
9-
<div className="flex flex-1 flex-col overflow-hidden">
10-
<Header />
11-
<main className="flex-1 overflow-y-auto p-4 pb-24 md:pb-4">
12-
<Outlet />
13-
</main>
8+
<HeaderActionsProvider>
9+
<div className="flex h-screen bg-background">
10+
<Sidebar />
11+
<div className="flex flex-1 flex-col overflow-hidden">
12+
<Header />
13+
<main className="flex-1 overflow-y-auto p-3 pb-24 md:pb-3">
14+
<Outlet />
15+
</main>
16+
</div>
1417
</div>
15-
</div>
18+
</HeaderActionsProvider>
1619
);
1720
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createContext, useContext, useState, type ReactNode } from "react";
2+
3+
const HeaderActionsContext = createContext<{
4+
actions: ReactNode;
5+
setActions: (node: ReactNode) => void;
6+
}>({ actions: null, setActions: () => {} });
7+
8+
export function HeaderActionsProvider({ children }: { children: ReactNode }) {
9+
const [actions, setActions] = useState<ReactNode>(null);
10+
return (
11+
<HeaderActionsContext.Provider value={{ actions, setActions }}>
12+
{children}
13+
</HeaderActionsContext.Provider>
14+
);
15+
}
16+
17+
export function useHeaderActions() {
18+
return useContext(HeaderActionsContext);
19+
}
Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,50 @@
1+
import { Home, LayoutDashboard, Box, Cpu, Wrench, Rocket, Radio, Monitor } from "lucide-react";
2+
import { useLocation } from "react-router";
13
import logoSvg from "@/assets/logo.svg";
4+
import { useHeaderActions } from "./header-context";
5+
6+
const routes: { path: string; label: string; icon: React.ElementType }[] = [
7+
{ path: "/", label: "Home", icon: Home },
8+
{ path: "/overview", label: "Overview", icon: LayoutDashboard },
9+
{ path: "/groups", label: "Device Groups", icon: Box },
10+
{ path: "/agents", label: "Agents", icon: Cpu },
11+
{ path: "/skills", label: "Skills", icon: Wrench },
12+
{ path: "/setup", label: "Setup", icon: Rocket },
13+
{ path: "/channels", label: "Channel", icon: Radio },
14+
{ path: "/devices", label: "Device", icon: Monitor },
15+
];
16+
17+
function getRouteInfo(pathname: string) {
18+
return (
19+
routes.find((r) => r.path === pathname) ||
20+
routes.find((r) => r.path !== "/" && pathname.startsWith(r.path))
21+
);
22+
}
223

324
export function Header() {
25+
const location = useLocation();
26+
const route = getRouteInfo(location.pathname);
27+
const Icon = route?.icon;
28+
const { actions } = useHeaderActions();
29+
430
return (
5-
<header className="flex items-center h-14 px-6 md:hidden">
6-
<div className="flex items-center gap-2.5">
31+
<header className="flex items-center justify-between h-12 pl-4 pr-2 border-b border-border shrink-0 bg-background/80 backdrop-blur-xs">
32+
{/* Mobile: logo */}
33+
<div className="flex items-center gap-2.5 md:hidden">
734
<img src={logoSvg} alt="SmartClaws" className="h-5 w-5" />
835
<span className="font-semibold text-sm tracking-tight">SmartClaws</span>
936
</div>
37+
38+
{/* Desktop: icon + page name */}
39+
{route && (
40+
<div className="hidden md:flex items-center gap-2 text-sm">
41+
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
42+
<span className="font-medium">{route.label}</span>
43+
</div>
44+
)}
45+
46+
{/* Right side: page-specific actions */}
47+
{actions && <div className="hidden md:flex items-center">{actions}</div>}
1048
</header>
1149
);
1250
}

packages/dashboard/src/components/layout/sidebar.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChevronRight, Download, Wrench } from "lucide-react";
1+
import { ChevronRight, LayoutDashboard, Rocket, Wrench } from "lucide-react";
22
import { useState } from "react";
33
import type { Address } from "viem";
44
import { AddressAvatar } from "@/components/shared/address-avatar";
@@ -108,12 +108,26 @@ export function Sidebar() {
108108
const { groups, isLoading } = useDeviceGroups();
109109

110110
return (
111-
<aside className="hidden md:flex w-56 flex-col bg-card rounded-2xl m-2 mr-0 overflow-y-auto">
112-
<Link to="/" className="flex items-center gap-2 px-4 pt-4 pb-2">
111+
<aside className="hidden md:flex w-56 flex-col border-r border-border">
112+
<Link to="/" className="flex items-center gap-2 px-4 h-12 border-b border-border shrink-0">
113113
<img src={logoSvg} alt="SmartClaws" className="h-4 w-4" />
114114
<span className="font-medium text-sm tracking-tight" style={{ color: "#FFD7DA" }}>SmartClaws</span>
115115
</Link>
116-
<nav className="flex-1 px-2 pb-3">
116+
<nav className="flex-1 px-2 pb-3 overflow-y-auto">
117+
<div className="pt-2">
118+
<Link
119+
to="/overview"
120+
className={cn(
121+
"flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
122+
location.pathname === "/overview"
123+
? "bg-accent text-accent-foreground font-medium"
124+
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
125+
)}
126+
>
127+
<LayoutDashboard className="h-4 w-4 shrink-0" />
128+
Overview
129+
</Link>
130+
</div>
117131
<SectionLabel>Device Groups</SectionLabel>
118132
<div className="space-y-0.5">
119133
{isLoading ? (
@@ -148,16 +162,16 @@ export function Sidebar() {
148162
All Skills
149163
</Link>
150164
<Link
151-
to="/installation"
165+
to="/setup"
152166
className={cn(
153167
"flex items-center gap-2.5 rounded-md px-3 py-1.5 text-sm transition-colors",
154-
location.pathname === "/installation"
168+
location.pathname === "/setup"
155169
? "bg-accent text-accent-foreground font-medium"
156170
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
157171
)}
158172
>
159-
<Download className="h-4 w-4 shrink-0" />
160-
Installation
173+
<Rocket className="h-4 w-4 shrink-0" />
174+
Setup
161175
</Link>
162176
</nav>
163177
</aside>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { MessageBubble } from "@/components/ui/message-bubble";
2+
3+
function StepNumber({ n }: { n: number }) {
4+
return (
5+
<span
6+
className="inline-flex items-center justify-center h-5 w-5 shrink-0 rounded-full text-[10px] font-bold text-black align-middle mr-1.5"
7+
style={{ background: "linear-gradient(178deg, #ffd4d7, #ff8594)" }}
8+
>
9+
{n}
10+
</span>
11+
);
12+
}
13+
14+
export function SetupDialog() {
15+
return (
16+
<div className="flex flex-col gap-3">
17+
{/* Step 1: Install skills */}
18+
<MessageBubble variant="secondary" clickable={false}>
19+
<StepNumber n={1} /> Install the skills:
20+
</MessageBubble>
21+
<MessageBubble variant="primary">
22+
Install smartclaws-producer and smartclaws-reader skills from ClawHub
23+
</MessageBubble>
24+
25+
{/* Step 2: Set up SmartClaws */}
26+
<MessageBubble variant="secondary" clickable={false}>
27+
<StepNumber n={2} /> Initialize the CLI and generate a wallet:
28+
</MessageBubble>
29+
<MessageBubble variant="primary">
30+
Set up SmartClaws and create a new wallet
31+
</MessageBubble>
32+
33+
<div className="flex items-center gap-4 my-1">
34+
<div className="flex-1 h-[1.5px]" style={{ backgroundImage: "repeating-linear-gradient(to right, var(--muted-foreground) 0, var(--muted-foreground) 4px, transparent 4px, transparent 12px)", opacity: 0.4 }} />
35+
<span className="text-xs text-muted-foreground shrink-0">Transfer CREDITS to the wallet</span>
36+
<div className="flex-1 h-[1.5px]" style={{ backgroundImage: "repeating-linear-gradient(to right, var(--muted-foreground) 0, var(--muted-foreground) 4px, transparent 4px, transparent 12px)", opacity: 0.4 }} />
37+
</div>
38+
39+
{/* Step 3: Register device group */}
40+
<MessageBubble variant="secondary" clickable={false}>
41+
<StepNumber n={3} /> Fund the wallet and register:
42+
</MessageBubble>
43+
<MessageBubble variant="primary">
44+
Wallet funded. Register a new device group: my-sensors.
45+
</MessageBubble>
46+
47+
{/* Step 4: Set up sensor */}
48+
<MessageBubble variant="secondary" clickable={false}>
49+
<StepNumber n={4} /> Connect a sensor and start publishing data:
50+
</MessageBubble>
51+
<MessageBubble variant="primary">
52+
Set up a temperature sensor on my Raspberry Pi and start publishing data
53+
</MessageBubble>
54+
55+
{/* Step 5: Query data */}
56+
<MessageBubble variant="secondary" clickable={false}>
57+
<StepNumber n={5} /> Query your on-chain data:
58+
</MessageBubble>
59+
<MessageBubble variant="primary">
60+
What's the current temperature? Show me the trend for the last hour
61+
</MessageBubble>
62+
</div>
63+
);
64+
}

packages/dashboard/src/components/shared/channel-view.tsx

Lines changed: 5 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ChevronRight, Database, Hash, Loader2, MessageSquare } from "lucide-react";
2-
import { type ReactNode, useEffect, useRef, useState } from "react";
1+
import { ChevronRight, Database, Hash, MessageSquare } from "lucide-react";
2+
import { type ReactNode, useState } from "react";
33
import type { Address } from "viem";
44
import { EmptyState } from "@/components/shared/empty-state";
55
import { StatCard } from "@/components/shared/stat-card";
@@ -13,7 +13,7 @@ import {
1313
TableHeader,
1414
TableRow,
1515
} from "@/components/ui/table";
16-
import { type DecodedMessage, useChannelMessages } from "@/hooks/use-channel-messages";
16+
import { useChannelMessages } from "@/hooks/use-channel-messages";
1717
import { SensorCharts } from "@/components/shared/sensor-charts";
1818

1919
function formatBytes(bytes: bigint): string {
@@ -92,48 +92,13 @@ function highlightJson(json: string): ReactNode[] {
9292
return parts;
9393
}
9494

95-
export interface ChannelData {
96-
messages: DecodedMessage[];
97-
messageCount: bigint | undefined;
98-
maxCapacity: bigint | undefined;
99-
totalBytes: bigint | undefined;
100-
isLoading: boolean;
101-
hasMore: boolean;
102-
isLoadingMore: boolean;
103-
loadMore: () => void;
104-
}
105-
10695
interface ChannelViewProps {
10796
address: Address;
108-
data?: ChannelData;
10997
}
11098

111-
export function ChannelView({ address, data }: ChannelViewProps) {
112-
const hookData = useChannelMessages(address);
113-
const { messages, messageCount, maxCapacity, totalBytes, isLoading, hasMore, isLoadingMore, loadMore } =
114-
data ?? hookData;
99+
export function ChannelView({ address }: ChannelViewProps) {
100+
const { messages, messageCount, maxCapacity, totalBytes, isLoading } = useChannelMessages(address);
115101
const [expanded, setExpanded] = useState<Set<string>>(new Set());
116-
const sentinelRef = useRef<HTMLTableRowElement>(null);
117-
118-
// Clear expanded rows when switching channels
119-
useEffect(() => {
120-
setExpanded(new Set());
121-
}, [address]);
122-
123-
useEffect(() => {
124-
const el = sentinelRef.current;
125-
if (!el || !hasMore) return;
126-
const observer = new IntersectionObserver(
127-
([entry]) => {
128-
if (entry.isIntersecting && !isLoadingMore) {
129-
loadMore();
130-
}
131-
},
132-
{ threshold: 0.1 },
133-
);
134-
observer.observe(el);
135-
return () => observer.disconnect();
136-
}, [hasMore, isLoadingMore, loadMore]);
137102

138103
const toggleExpand = (key: string) => {
139104
setExpanded((prev) => {
@@ -278,17 +243,6 @@ export function ChannelView({ address, data }: ChannelViewProps) {
278243
</>
279244
);
280245
})}
281-
{(hasMore || isLoadingMore) && (
282-
<TableRow ref={sentinelRef} className="hover:bg-transparent">
283-
<TableCell colSpan={6} className="py-3 text-center">
284-
{isLoadingMore ? (
285-
<Loader2 className="h-4 w-4 animate-spin inline-block text-muted-foreground" />
286-
) : (
287-
<span className="text-xs text-muted-foreground">Scroll for older messages</span>
288-
)}
289-
</TableCell>
290-
</TableRow>
291-
)}
292246
</TableBody>
293247
</Table>
294248
</div>

packages/dashboard/src/components/shared/page-header.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ interface PageHeaderProps {
66

77
export function PageHeader({ title, description, children }: PageHeaderProps) {
88
return (
9-
<div className="flex items-start justify-between mb-6">
9+
<div className="flex items-start justify-between mb-8">
1010
<div>
11-
<h1 className="text-lg font-semibold">{title}</h1>
11+
<h1 className="text-lg font-semibold tracking-tight">{title}</h1>
1212
{description && (
13-
<p className="text-muted-foreground/80 mt-1 text-xs">{description}</p>
13+
<p className="text-muted-foreground mt-1 text-xs">{description}</p>
1414
)}
1515
</div>
1616
{children && <div className="flex items-center gap-2">{children}</div>}

packages/dashboard/src/components/ui/badge.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { cva, type VariantProps } from "class-variance-authority";
55
import { cn } from "@/lib/utils";
66

77
const badgeVariants = cva(
8-
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-3xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
8+
"group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2.5 py-1 text-xs font-normal whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
99
{
1010
variants: {
1111
variant: {
1212
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13-
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
13+
secondary: "bg-muted/60 text-muted-foreground [a]:hover:bg-muted/80",
1414
destructive:
1515
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
1616
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",

0 commit comments

Comments
 (0)