diff --git a/public/sim-config.json b/public/sim-config.json index 37f4871..53cdd39 100644 --- a/public/sim-config.json +++ b/public/sim-config.json @@ -7,6 +7,7 @@ "general": ["hmpt3fxBvpWrkZxq5H5uWjZ2BgHRMJs2hKHiWJDoqD7am1xPs"] }, "genesisUserTiers": { - "hmpt3fxBvpWrkZxq5H5uWjZ2BgHRMJs2hKHiWJDoqD7am1xPs": "Citizen" + "hmpt3fxBvpWrkZxq5H5uWjZ2BgHRMJs2hKHiWJDoqD7am1xPs": "Citizen", + "hmspQwDydCF6eGz3bdbFQNwDZiJeKCATcB1jC82trofPRfChB": "Consul" } } diff --git a/src/app/AppSidebar.tsx b/src/app/AppSidebar.tsx index 659dfd4..471954e 100644 --- a/src/app/AppSidebar.tsx +++ b/src/app/AppSidebar.tsx @@ -40,7 +40,9 @@ const AppSidebar: React.FC = ({ 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); @@ -52,7 +54,6 @@ const AppSidebar: React.FC = ({ 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 }, @@ -115,6 +116,14 @@ const AppSidebar: React.FC = ({ children }) => { {settingsOpen && (
+ setMobileNavOpen(false)} + > + = ({ + 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 ( + + + {shortAddress(address, size)} + + {showCopy ? ( + + ) : null} + + ); +}; diff --git a/src/lib/profileUi.ts b/src/lib/profileUi.ts index 067187c..a66179e 100644 --- a/src/lib/profileUi.ts +++ b/src/lib/profileUi.ts @@ -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)}`; diff --git a/src/pages/chambers/Chamber.tsx b/src/pages/chambers/Chamber.tsx index fc91d46..579f074 100644 --- a/src/pages/chambers/Chamber.tsx +++ b/src/pages/chambers/Chamber.tsx @@ -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, @@ -713,9 +714,11 @@ const Chamber: React.FC = () => { key={entry.address} className="flex flex-wrap items-center justify-between gap-2" > - - {entry.address} - + LCM {entry.lcm} · MCM {entry.mcm} · ACM{" "} {entry.acm} @@ -744,9 +747,11 @@ const Chamber: React.FC = () => { key={`${entry.address}-${entry.submittedAt}`} className="flex flex-col gap-1" > - - {entry.address} - + M × {entry.multiplier} ·{" "} {formatDate(entry.submittedAt)} diff --git a/src/pages/factions/Faction.tsx b/src/pages/factions/Faction.tsx index 50220c4..a141574 100644 --- a/src/pages/factions/Faction.tsx +++ b/src/pages/factions/Faction.tsx @@ -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 { @@ -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" >
-

- {membership.address} -

+

Joined {formatDateTime(membership.joinedAt)}

@@ -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" >
-

- {invite.address} -

-

- Invited by {invite.invitedBy} ·{" "} - {formatDateTime(invite.invitedAt)} -

+ +
+ Invited by + + · {formatDateTime(invite.invitedAt)} +
{invite.status} diff --git a/src/pages/human-nodes/HumanNodes.tsx b/src/pages/human-nodes/HumanNodes.tsx index fbaf9c4..f4f56b7 100644 --- a/src/pages/human-nodes/HumanNodes.tsx +++ b/src/pages/human-nodes/HumanNodes.tsx @@ -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, @@ -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(null); const [factionsById, setFactionsById] = useState>( @@ -295,6 +299,7 @@ const HumanNodes: React.FC = () => {
{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)); @@ -356,13 +361,27 @@ const HumanNodes: React.FC = () => {
-

{node.name}

-
- {node.role} - - {shortAddress(node.id)} - -
+ {nameIsAddress ? ( + + ) : ( +

{node.name}

+ )} + {!nameIsAddress ? ( +
+ + + +
+ ) : null}
{tileItems.map((item) => ( @@ -402,45 +421,63 @@ const HumanNodes: React.FC = () => {
) : (
- {filtered.map((node) => ( - - -
-
-

{node.name}

-

{node.role}

-
-
- - {factionsById[node.factionId]?.name ?? "—"} - - - - ACM - {" "} - {node.cmTotals?.acm ?? node.acm} - - - LCM {node.cmTotals?.lcm ?? 0} - - - MCM {node.cmTotals?.mcm ?? 0} - - {node.formationCapable && ( + {filtered.map((node) => { + const nameIsAddress = isLikelyAddress(node.name); + return ( + + +
+
+ {nameIsAddress ? ( + + ) : ( +

+ {node.name} +

+ )} +

+ + {node.tier.charAt(0).toUpperCase() + + node.tier.slice(1)} + +

+
+
- Formation + {factionsById[node.factionId]?.name ?? "—"} - )} -
-
- + + + ACM + {" "} + {node.cmTotals?.acm ?? node.acm} + + + LCM {node.cmTotals?.lcm ?? 0} + + + MCM {node.cmTotals?.mcm ?? 0} + + {node.formationCapable && ( + + Formation + + )} +
+
+ +
-
- - - ))} + + + ); + })}
)}