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
6 changes: 3 additions & 3 deletions src/lib/dtoParsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export function computeChamberMetrics(chambers: ChamberDto[]) {
return sum + acm;
}, 0);
// Governors can be members of multiple chambers; use the largest chamber roster
// as a stable approximation of global governors for the summary tile.
const governors = chambers.reduce((max, chamber) => {
// as a stable approximation of global active governors for the summary tile.
const activeGovernors = chambers.reduce((max, chamber) => {
const { governors } = getChamberNumericStats(chamber);
return Math.max(max, governors);
}, 0);
Expand All @@ -50,7 +50,7 @@ export function computeChamberMetrics(chambers: ChamberDto[]) {
);
return {
totalChambers: chambers.length,
governors,
activeGovernors,
totalAcm,
liveProposals,
};
Expand Down
46 changes: 11 additions & 35 deletions src/pages/MyGovernance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,13 @@ const governingStatusTermId = (label: GoverningStatus): string => {
return "governing_status_losing_status";
};

const formatDayHourMinuteSecond = (targetMs: number, nowMs: number): string => {
const formatDayHourMinute = (targetMs: number, nowMs: number): string => {
const deltaMs = Math.max(0, targetMs - nowMs);
const totalSeconds = Math.floor(deltaMs / 1000);
const days = Math.floor(totalSeconds / (24 * 60 * 60));
const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60));
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
const seconds = totalSeconds % 60;
return `${days}d:${String(hours).padStart(2, "0")}h:${String(
minutes,
).padStart(2, "0")}m:${String(seconds).padStart(2, "0")}s`;
const totalMinutes = Math.floor(deltaMs / 60_000);
const days = Math.floor(totalMinutes / (24 * 60));
const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
const minutes = totalMinutes % 60;
return `${days}d:${String(hours).padStart(2, "0")}h:${String(minutes).padStart(2, "0")}m`;
};

const MyGovernance: React.FC = () => {
Expand All @@ -159,22 +156,12 @@ const MyGovernance: React.FC = () => {
const [cmSummary, setCmSummary] = useState<CmSummaryDto | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
const [nowMs, setNowMs] = useState<number>(() => Date.now());
const [clockBaseMs, setClockBaseMs] = useState<number | null>(null);
const [clockFetchedAtMs, setClockFetchedAtMs] = useState<number | null>(null);

useEffect(() => {
const timer = window.setInterval(() => setNowMs(Date.now()), 1000);
return () => window.clearInterval(timer);
}, []);

useEffect(() => {
if (!clock?.now) return;
const parsed = new Date(clock.now).getTime();
if (!Number.isFinite(parsed)) return;
setClockBaseMs(parsed);
setClockFetchedAtMs(Date.now());
}, [clock?.now]);

useEffect(() => {
let active = true;
(async () => {
Expand Down Expand Up @@ -221,21 +208,10 @@ const MyGovernance: React.FC = () => {
? new Date(clock.nextEraAt).getTime()
: NaN;
if (Number.isFinite(targetMs)) {
const simNowMs =
Number.isFinite(clockBaseMs ?? NaN) &&
Number.isFinite(clockFetchedAtMs ?? NaN)
? (clockBaseMs as number) + (nowMs - (clockFetchedAtMs as number))
: nowMs;
return formatDayHourMinuteSecond(targetMs, simNowMs);
return formatDayHourMinute(targetMs, nowMs);
}
return eraActivity?.timeLeft ?? "—";
}, [
clock?.nextEraAt,
eraActivity?.timeLeft,
nowMs,
clockBaseMs,
clockFetchedAtMs,
]);
}, [clock?.nextEraAt, eraActivity?.timeLeft, nowMs]);

const myChambers = useMemo(() => {
if (!gov || !chambers) return [];
Expand Down Expand Up @@ -387,7 +363,7 @@ const MyGovernance: React.FC = () => {
</Surface>
<div className="flex flex-col items-center justify-center gap-3 px-2">
<Kicker align="center">Progress</Kicker>
<div className="relative h-2 w-full max-w-48 overflow-hidden rounded-full bg-muted/30">
<div className="relative h-2 w-48 overflow-hidden rounded-full bg-muted/30">
<div
className="h-full rounded-full bg-primary"
style={{ width: `${overallPercent}%` }}
Expand Down Expand Up @@ -565,7 +541,7 @@ const MyGovernance: React.FC = () => {
badge={
<Badge
size="md"
className="border-none bg-(--primary-dim) px-4 py-1 text-center text-sm font-bold tracking-wide text-primary uppercase"
className="border-none bg-(--primary-dim) px-4 py-1 text-center text-sm font-bold tracking-wide whitespace-nowrap text-primary uppercase"
>
M × {chamber.multiplier}
</Badge>
Expand All @@ -576,7 +552,7 @@ const MyGovernance: React.FC = () => {
asChild
size="md"
variant="primary"
className="w-full sm:w-56"
className="w-56"
>
<Link to={`/app/chambers/${chamber.id}`}>Enter</Link>
</Button>
Expand Down
25 changes: 6 additions & 19 deletions src/pages/chambers/Chambers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import { Button } from "@/components/primitives/button";
import { Link } from "react-router";
import { InlineHelp } from "@/components/InlineHelp";
import { NoDataYetBar } from "@/components/NoDataYetBar";
import { apiChambers, apiClock } from "@/lib/apiClient";
import { apiChambers } from "@/lib/apiClient";
import {
computeChamberMetrics,
getChamberNumericStats,
} from "@/lib/dtoParsers";
import type { ChamberDto } from "@/types/api";
import type { GetClockResponse } from "@/types/api";
import { Surface } from "@/components/Surface";

type Metric = {
Expand All @@ -35,7 +34,6 @@ const metricCards: Metric[] = [

const Chambers: React.FC = () => {
const [chambers, setChambers] = useState<ChamberDto[] | null>(null);
const [clock, setClock] = useState<GetClockResponse | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [filters, setFilters] = useState<{
Expand All @@ -49,15 +47,12 @@ const Chambers: React.FC = () => {
(async () => {
try {
const res = await apiChambers();
const clockRes = await apiClock().catch(() => null);
if (!active) return;
setChambers(res.items);
setClock(clockRes);
setLoadError(null);
} catch (error) {
if (!active) return;
setChambers([]);
setClock(null);
setLoadError((error as Error).message);
}
})();
Expand Down Expand Up @@ -95,12 +90,9 @@ const Chambers: React.FC = () => {

const computedMetrics = useMemo((): Metric[] => {
if (!chambers) return metricCards;
const { governors, totalAcm, liveProposals } =
const { activeGovernors, totalAcm, liveProposals } =
computeChamberMetrics(chambers);
const activeGovernors = Math.min(
governors,
Math.max(0, Math.floor(clock?.activeGovernors ?? governors)),
);
const governors = activeGovernors;
return [
{ label: "Total chambers", value: String(chambers.length) },
{
Expand All @@ -110,7 +102,7 @@ const Chambers: React.FC = () => {
{ label: "Total ACM", value: totalAcm.toLocaleString() },
{ label: "Live proposals", value: String(liveProposals) },
];
}, [chambers, clock]);
}, [chambers]);

return (
<div className="flex flex-col gap-6">
Expand Down Expand Up @@ -208,19 +200,14 @@ const Chambers: React.FC = () => {
badge={
<Badge
size="md"
className="border-none bg-(--primary-dim) px-4 py-1 text-center text-sm font-bold tracking-wide text-primary uppercase"
className="border-none bg-(--primary-dim) px-4 py-1 text-center text-sm font-bold tracking-wide whitespace-nowrap text-primary uppercase"
>
M × {chamber.multiplier}
</Badge>
}
footer={
<div className="flex w-full justify-center">
<Button
asChild
size="md"
variant="primary"
className="w-full sm:w-56"
>
<Button asChild size="md" variant="primary" className="w-56">
<Link to={`/app/chambers/${chamber.id}`}>Enter</Link>
</Button>
</div>
Expand Down
16 changes: 8 additions & 8 deletions src/pages/factions/Faction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ const Faction: React.FC = () => {
<h1 className="text-xl font-semibold text-text">{faction.name}</h1>
<p className="text-sm text-muted">{faction.description}</p>
</div>
<div className="flex flex-wrap items-center gap-2 sm:justify-end">
<div className="flex items-center gap-2">
<Badge variant="outline">Members: {faction.members}</Badge>
<Badge variant="outline">Votes: {faction.votes}</Badge>
{viewerRole ? (
Expand All @@ -193,7 +193,7 @@ const Faction: React.FC = () => {
</CardContent>
</Card>

<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2">
{canJoin ? (
<Button
size="sm"
Expand Down Expand Up @@ -281,7 +281,7 @@ const Faction: React.FC = () => {
onChange={(event) => setEditTagsText(event.target.value)}
placeholder="Tags, comma separated"
/>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2">
<Button
size="sm"
disabled={
Expand Down Expand Up @@ -363,7 +363,7 @@ const Faction: React.FC = () => {
Joined {new Date(membership.joinedAt).toLocaleString()}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2">
{canManageMembers ? (
<Select
value={membership.role}
Expand Down Expand Up @@ -411,7 +411,7 @@ const Faction: React.FC = () => {
key={channel.id}
className="rounded-md border border-border px-3 py-2 text-sm"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center justify-between gap-2">
<div>
<p className="font-semibold text-text">{channel.title}</p>
<p className="text-xs text-muted">
Expand Down Expand Up @@ -504,7 +504,7 @@ const Faction: React.FC = () => {
</div>
<p className="text-xs text-muted">{initiative.intent}</p>
{isFounderAdmin ? (
<div className="mt-2 flex flex-wrap items-center gap-2">
<div className="mt-2 flex items-center gap-2">
<Select
value={initiative.status}
onChange={(event) =>
Expand Down Expand Up @@ -598,7 +598,7 @@ const Faction: React.FC = () => {
key={thread.id}
className="space-y-2 rounded-md border border-border px-3 py-2"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center justify-between gap-2">
<div>
<p className="text-sm font-semibold text-text">
{thread.title}
Expand Down Expand Up @@ -635,7 +635,7 @@ const Faction: React.FC = () => {
</div>
) : null}
{canPost ? (
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<div className="flex items-center gap-2">
<Input
value={replyByThread[thread.id] ?? ""}
onChange={(event) =>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/factions/FactionCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const FactionCreate: React.FC = () => {
</Button>
</CardHeader>
<CardContent className="space-y-5">
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
<div className="grid gap-2 sm:grid-cols-3">
{[
{ n: 1, label: "Identity" },
{ n: 2, label: "Access & goals" },
Expand Down
1 change: 0 additions & 1 deletion src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,6 @@ export type GetMyGovernanceResponse = {
export type GetClockResponse = {
currentEra: number;
updatedAt: string;
now: string;
eraSeconds: number;
nextEraAt: string;
activeGovernors: number;
Expand Down