Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion public/sim-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"general": ["hmpt3fxBvpWrkZxq5H5uWjZ2BgHRMJs2hKHiWJDoqD7am1xPs"]
},
"genesisUserTiers": {
"hmpt3fxBvpWrkZxq5H5uWjZ2BgHRMJs2hKHiWJDoqD7am1xPs": "Citizen"
"hmpt3fxBvpWrkZxq5H5uWjZ2BgHRMJs2hKHiWJDoqD7am1xPs": "Citizen",
"hmspQwDydCF6eGz3bdbFQNwDZiJeKCATcB1jC82trofPRfChB": "Consul"
}
}
13 changes: 11 additions & 2 deletions src/app/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ const AppSidebar: React.FC<React.PropsWithChildren> = ({ children }) => {
const [settingsOpen, setSettingsOpen] = useState(false);
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const location = useLocation();
const settingsRouteActive = location.pathname.startsWith("/app/settings");
const settingsRouteActive =
location.pathname.startsWith("/app/settings") ||
location.pathname.startsWith("/app/profile");

useEffect(() => {
setMobileNavOpen(false);
Expand All @@ -52,7 +54,6 @@ const AppSidebar: React.FC<React.PropsWithChildren> = ({ children }) => {

const navItems: NavItem[] = [
{ to: "/app/feed", label: "Feed", Icon: Activity },
{ to: "/app/profile", label: "My profile", Icon: User },
{ to: "/app/my-governance", label: "My governance", Icon: Gavel },
{ to: "/app/proposals", label: "Proposals", Icon: FileText },
{ to: "/app/chambers", label: "Chambers", Icon: Lightbulb },
Expand Down Expand Up @@ -115,6 +116,14 @@ const AppSidebar: React.FC<React.PropsWithChildren> = ({ children }) => {
</button>
{settingsOpen && (
<div className="pt-1 pl-4">
<NavLink
className={nestedNavClass}
to="/app/profile"
onClick={() => setMobileNavOpen(false)}
>
<User className="sidebar__icon" aria-hidden="true" />
<span>My profile</span>
</NavLink>
<NavLink
className={nestedNavClass}
to="/app/settings"
Expand Down
64 changes: 64 additions & 0 deletions src/components/AddressInline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useState } from "react";
import { Check, Copy } from "lucide-react";
import { shortAddress } from "@/lib/profileUi";

type AddressInlineProps = {
address: string;
size?: number;
className?: string;
textClassName?: string;
showCopy?: boolean;
};

export const AddressInline: React.FC<AddressInlineProps> = ({
address,
size = 4,
className,
textClassName,
showCopy = true,
}) => {
const [copied, setCopied] = useState(false);

useEffect(() => {
if (!copied) return;
const timer = window.setTimeout(() => setCopied(false), 1400);
return () => window.clearTimeout(timer);
}, [copied]);

const copy = async () => {
try {
await navigator.clipboard?.writeText(address);
setCopied(true);
} catch {
// Ignore clipboard errors; the UI still shows the formatted address.
}
};

return (
<span
className={`inline-flex min-w-0 items-center gap-1 ${className ?? ""}`.trim()}
>
<span
title={address}
className={`min-w-0 truncate font-mono text-xs ${textClassName ?? ""}`.trim()}
>
{shortAddress(address, size)}
</span>
{showCopy ? (
<button
type="button"
className="hover:bg-surface-alt inline-flex h-6 w-6 items-center justify-center rounded-full text-muted transition hover:text-text"
aria-label={copied ? "Copied" : "Copy address"}
title={copied ? "Copied" : "Copy address"}
onClick={() => void copy()}
>
{copied ? (
<Check className="h-3.5 w-3.5" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</button>
) : null}
</span>
);
};
2 changes: 1 addition & 1 deletion src/lib/profileUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const normalizeDetailValue = (label: string, value: string) => {
return formatDateTime(value);
};

export const shortAddress = (value: string, size = 6) => {
export const shortAddress = (value: string, size = 4) => {
if (!value) return value;
if (value.length <= size * 2 + 3) return value;
return `${value.slice(0, size)}…${value.slice(-size)}`;
Expand Down
17 changes: 11 additions & 6 deletions src/pages/chambers/Chamber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { PageHeader } from "@/components/PageHeader";
import { TierLabel } from "@/components/TierLabel";
import { PipelineList } from "@/components/PipelineList";
import { StatGrid, makeChamberStats } from "@/components/StatGrid";
import { AddressInline } from "@/components/AddressInline";
import type {
ChamberChatPeerDto,
ChamberChatSignalDto,
Expand Down Expand Up @@ -713,9 +714,11 @@ const Chamber: React.FC = () => {
key={entry.address}
className="flex flex-wrap items-center justify-between gap-2"
>
<span className="min-w-0 flex-1 [overflow-wrap:anywhere] break-words">
{entry.address}
</span>
<AddressInline
address={entry.address}
className="min-w-0 flex-1"
textClassName="[overflow-wrap:anywhere] break-words"
/>
<span className="text-xs text-muted">
LCM {entry.lcm} · MCM {entry.mcm} · ACM{" "}
{entry.acm}
Expand Down Expand Up @@ -744,9 +747,11 @@ const Chamber: React.FC = () => {
key={`${entry.address}-${entry.submittedAt}`}
className="flex flex-col gap-1"
>
<span className="min-w-0 font-semibold [overflow-wrap:anywhere] break-words">
{entry.address}
</span>
<AddressInline
address={entry.address}
className="min-w-0"
textClassName="font-semibold [overflow-wrap:anywhere] break-words"
/>
<span className="text-xs text-muted">
M × {entry.multiplier} ·{" "}
{formatDate(entry.submittedAt)}
Expand Down
26 changes: 16 additions & 10 deletions src/pages/factions/Faction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Link, useParams, useSearchParams } from "react-router";
import { Kicker } from "@/components/Kicker";
import { NoDataYetBar } from "@/components/NoDataYetBar";
import { PageHint } from "@/components/PageHint";
import { AddressInline } from "@/components/AddressInline";
import { Badge } from "@/components/primitives/badge";
import { Button } from "@/components/primitives/button";
import {
Expand Down Expand Up @@ -388,9 +389,11 @@ const Faction: React.FC = () => {
className="flex flex-col gap-2 rounded-md border border-border px-3 py-2 sm:flex-row sm:items-center sm:justify-between"
>
<div className="min-w-0">
<p className="text-sm font-semibold [overflow-wrap:anywhere] break-words text-text">
{membership.address}
</p>
<AddressInline
address={membership.address}
className="text-text"
textClassName="text-sm font-semibold [overflow-wrap:anywhere] break-words"
/>
<p className="text-xs text-muted">
Joined {formatDateTime(membership.joinedAt)}
</p>
Expand Down Expand Up @@ -443,13 +446,16 @@ const Faction: React.FC = () => {
className="flex flex-col gap-2 rounded-md border border-border px-3 py-2 sm:flex-row sm:items-center sm:justify-between"
>
<div className="min-w-0">
<p className="text-sm font-semibold [overflow-wrap:anywhere] break-words text-text">
{invite.address}
</p>
<p className="text-xs text-muted">
Invited by {invite.invitedBy} ·{" "}
{formatDateTime(invite.invitedAt)}
</p>
<AddressInline
address={invite.address}
className="text-text"
textClassName="text-sm font-semibold [overflow-wrap:anywhere] break-words"
/>
<div className="flex flex-wrap items-center gap-1 text-xs text-muted">
<span>Invited by</span>
<AddressInline address={invite.invitedBy} />
<span>· {formatDateTime(invite.invitedAt)}</span>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline">{invite.status}</Badge>
Expand Down
133 changes: 85 additions & 48 deletions src/pages/human-nodes/HumanNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@ import { Kicker } from "@/components/Kicker";
import { TierLabel } from "@/components/TierLabel";
import { ToggleGroup } from "@/components/ToggleGroup";
import { NoDataYetBar } from "@/components/NoDataYetBar";
import {
DETAIL_TILE_CLASS,
normalizeDetailValue,
shortAddress,
} from "@/lib/profileUi";
import { AddressInline } from "@/components/AddressInline";
import { DETAIL_TILE_CLASS, normalizeDetailValue } from "@/lib/profileUi";
import {
apiChambers,
apiFactions,
Expand All @@ -36,6 +33,13 @@ import type {
HumanNodeDto,
} from "@/types/api";

const isLikelyAddress = (value: string) => {
const normalized = value.trim().toLowerCase();
if (!normalized.startsWith("hmp")) return false;
if (normalized.length < 24) return false;
return /^[a-z0-9]+$/.test(normalized);
};

const HumanNodes: React.FC = () => {
const [nodes, setNodes] = useState<HumanNodeDto[] | null>(null);
const [factionsById, setFactionsById] = useState<Record<string, FactionDto>>(
Expand Down Expand Up @@ -295,6 +299,7 @@ const HumanNodes: React.FC = () => {
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{filtered.map((node) => {
const factionName = factionsById[node.factionId]?.name ?? "—";
const nameIsAddress = isLikelyAddress(node.name);
const formationProjects = (node.formationProjectIds ?? [])
.map((projectId) => formationProjectsById[projectId]?.title)
.filter((title): title is string => Boolean(title));
Expand Down Expand Up @@ -356,13 +361,27 @@ const HumanNodes: React.FC = () => {
<Card key={node.id}>
<CardContent className="flex flex-col gap-4 pt-4">
<div>
<h3 className="text-lg font-semibold">{node.name}</h3>
<div className="flex flex-wrap items-center gap-2 text-sm text-muted">
<span>{node.role}</span>
<Badge size="sm" variant="muted" title={node.id}>
{shortAddress(node.id)}
</Badge>
</div>
{nameIsAddress ? (
<AddressInline
address={node.id}
className="mx-auto flex w-full items-center justify-center"
textClassName="text-2xl font-semibold tracking-wide sm:text-3xl"
/>
) : (
<h3 className="text-lg font-semibold">{node.name}</h3>
)}
{!nameIsAddress ? (
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm text-muted">
<Badge
size="sm"
variant="muted"
title={node.id}
className="max-w-[220px] pr-1"
>
<AddressInline address={node.id} />
</Badge>
</div>
) : null}
</div>
<div className="grid auto-rows-fr grid-cols-1 gap-3 sm:grid-cols-2">
{tileItems.map((item) => (
Expand Down Expand Up @@ -402,45 +421,63 @@ const HumanNodes: React.FC = () => {
</div>
) : (
<div className="flex flex-col gap-4">
{filtered.map((node) => (
<Card key={node.id}>
<CardContent className="pt-4 pb-3">
<div className="flex flex-wrap items-center gap-4">
<div className="min-w-0 flex-1">
<h4 className="text-base font-semibold">{node.name}</h4>
<p className="text-sm text-muted">{node.role}</p>
</div>
<div className="flex flex-wrap gap-2">
<Badge size="sm" variant="outline">
{factionsById[node.factionId]?.name ?? "—"}
</Badge>
<Badge size="sm">
<HintLabel termId="acm" className="mr-1">
ACM
</HintLabel>{" "}
{node.cmTotals?.acm ?? node.acm}
</Badge>
<Badge size="sm" variant="outline">
LCM {node.cmTotals?.lcm ?? 0}
</Badge>
<Badge size="sm" variant="outline">
MCM {node.cmTotals?.mcm ?? 0}
</Badge>
{node.formationCapable && (
{filtered.map((node) => {
const nameIsAddress = isLikelyAddress(node.name);
return (
<Card key={node.id}>
<CardContent className="pt-4 pb-3">
<div className="flex flex-wrap items-center gap-4">
<div className="min-w-0 flex-1">
{nameIsAddress ? (
<AddressInline
address={node.id}
className="max-w-full"
textClassName="text-lg font-semibold tracking-wide sm:text-xl"
/>
) : (
<h4 className="text-base font-semibold">
{node.name}
</h4>
)}
<p className="text-sm text-muted">
<TierLabel tier={node.tier}>
{node.tier.charAt(0).toUpperCase() +
node.tier.slice(1)}
</TierLabel>
</p>
</div>
<div className="flex flex-wrap gap-2">
<Badge size="sm" variant="outline">
Formation
{factionsById[node.factionId]?.name ?? "—"}
</Badge>
)}
</div>
<div className="ml-auto">
<Button asChild size="sm">
<Link to={`/app/human-nodes/${node.id}`}>Open</Link>
</Button>
<Badge size="sm">
<HintLabel termId="acm" className="mr-1">
ACM
</HintLabel>{" "}
{node.cmTotals?.acm ?? node.acm}
</Badge>
<Badge size="sm" variant="outline">
LCM {node.cmTotals?.lcm ?? 0}
</Badge>
<Badge size="sm" variant="outline">
MCM {node.cmTotals?.mcm ?? 0}
</Badge>
{node.formationCapable && (
<Badge size="sm" variant="outline">
Formation
</Badge>
)}
</div>
<div className="ml-auto">
<Button asChild size="sm">
<Link to={`/app/human-nodes/${node.id}`}>Open</Link>
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</CardContent>
</Card>
);
})}
</div>
)}
</CardContent>
Expand Down