Skip to content

Commit 836abd0

Browse files
committed
feat(frontend): time-first UI for stream window and create preview
Move block-number language out of the user-facing surface. Stream cards now read "Ends in 4h 30m" / "Starts in 12m" / "Ended 1d ago" instead of raw "Block 1,234,567 → 1,234,997"; the create-form preview shows "Starts at ~3:42 PM / Ends at ~8:42 PM" instead of approximate block numbers. Block numbers remain available on hover via title attributes. - formatStreamWindow(start, end, current): wall-clock window phrasing - blockToClockTime(target, current): local-time estimate from a future block - Approximate-duration footnote on create-form preview makes the ~5s/block Nakamoto cadence transparent without forcing users to think in blocks. Internally streams remain block-counted (the contract has no clock), but the word "block" no longer appears in the primary product surface.
1 parent 023caea commit 836abd0

3 files changed

Lines changed: 67 additions & 8 deletions

File tree

frontend/src/app/dashboard/create/page.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
type DurationUnit,
1919
type TokenConfig,
2020
} from "@/lib/constants";
21-
import { formatTokenAmount, blocksToTimeString } from "@/lib/utils";
21+
import { formatTokenAmount, blocksToTimeString, blockToClockTime } from "@/lib/utils";
2222
import { toast } from "sonner";
2323
import { Zap, ArrowRight, Info, Loader2, CheckCircle2 } from "lucide-react";
2424

@@ -253,18 +253,32 @@ export default function CreateStreamPage() {
253253
<p className="text-zinc-200 font-mono">{formatTokenAmount(ratePerBlock)} {selectedToken.symbol}</p>
254254
</div>
255255
<div>
256-
<p className="text-zinc-500 text-xs">Start block</p>
257-
<p className="text-zinc-200 font-mono">~#{(blockHeight + 120).toLocaleString()}</p>
256+
<p className="text-zinc-500 text-xs">Starts at</p>
257+
<p
258+
className="text-zinc-200"
259+
title={`~Block #${(blockHeight + 120).toLocaleString()}`}
260+
>
261+
~{blockToClockTime(blockHeight + 120, blockHeight)}
262+
</p>
258263
</div>
259264
<div>
260-
<p className="text-zinc-500 text-xs">End block</p>
261-
<p className="text-zinc-200 font-mono">~#{(blockHeight + 120 + durationBlocks).toLocaleString()}</p>
265+
<p className="text-zinc-500 text-xs">Ends at</p>
266+
<p
267+
className="text-zinc-200"
268+
title={`~Block #${(blockHeight + 120 + durationBlocks).toLocaleString()}`}
269+
>
270+
~{blockToClockTime(blockHeight + 120 + durationBlocks, blockHeight)}
271+
</p>
262272
</div>
263273
<div>
264274
<p className="text-zinc-500 text-xs">Token</p>
265275
<p className="text-zinc-200">{selectedToken.name} ({selectedToken.symbol})</p>
266276
</div>
267277
</div>
278+
<p className="text-[11px] text-zinc-600 pt-1 border-t border-border/40">
279+
Duration is approximate. Stacks blocks target ~5s under Nakamoto,
280+
so the actual stream length may vary by a few minutes.
281+
</p>
268282
</div>
269283
)}
270284

frontend/src/components/stream/stream-card.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
truncateAddress,
1111
getStreamStatusLabel,
1212
getStreamProgress,
13+
formatStreamWindow,
1314
} from "@/lib/utils";
1415
import { STREAM_STATUS } from "@/lib/constants";
1516
import { useAppStore } from "@/stores/app-store";
@@ -86,9 +87,12 @@ export function StreamCard({
8687
)}
8788
</span>
8889
</p>
89-
<p className="text-xs text-zinc-600">
90-
Block {stream.startBlock.toLocaleString()} &rarr;{" "}
91-
{stream.endBlock.toLocaleString()}
90+
<p
91+
className="text-xs text-zinc-600"
92+
title={`Block ${stream.startBlock.toLocaleString()}${stream.endBlock.toLocaleString()}`}
93+
>
94+
{formatStreamWindow(stream.startBlock, stream.endBlock, blockHeight) ??
95+
`Block ${stream.startBlock.toLocaleString()}${stream.endBlock.toLocaleString()}`}
9296
</p>
9397
{stream.memo && (
9498
<p className="text-xs text-zinc-500 mt-0.5 italic truncate max-w-30 sm:max-w-50" title={stream.memo}>

frontend/src/lib/utils.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,47 @@ export function blocksToTimeString(blocks: number): string {
8888
return `${months}mo ${days % 30}d`;
8989
}
9090

91+
/**
92+
* Wall-clock description of a stream's window relative to the current block.
93+
* Hides block numbers behind a time-first phrasing.
94+
*
95+
* future: "Starts in 12m"
96+
* active: "Ends in 4h 30m"
97+
* past: "Ended 1d ago"
98+
* loading: null (caller renders a fallback)
99+
*/
100+
export function formatStreamWindow(
101+
startBlock: number,
102+
endBlock: number,
103+
currentBlock: number,
104+
): string | null {
105+
if (currentBlock <= 0) return null;
106+
if (currentBlock < startBlock) return `Starts in ${blocksToTimeString(startBlock - currentBlock)}`;
107+
if (currentBlock < endBlock) return `Ends in ${blocksToTimeString(endBlock - currentBlock)}`;
108+
return `Ended ${blocksToTimeString(currentBlock - endBlock)} ago`;
109+
}
110+
111+
/**
112+
* Approximate wall-clock time at which a future block will be mined,
113+
* computed as now + (targetBlock - currentBlock) * BLOCK_TIME_SECONDS.
114+
* Returns local-time "3:42 PM" if within the next 24h, else "May 21 · 3:42 PM".
115+
*/
116+
export function blockToClockTime(targetBlock: number, currentBlock: number): string {
117+
if (currentBlock <= 0) return "—";
118+
const deltaMs = (targetBlock - currentBlock) * BLOCK_TIME_SECONDS * 1000;
119+
const target = new Date(Date.now() + deltaMs);
120+
const sameDay = target.toDateString() === new Date().toDateString();
121+
if (sameDay) {
122+
return target.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
123+
}
124+
return target.toLocaleString([], {
125+
month: "short",
126+
day: "numeric",
127+
hour: "numeric",
128+
minute: "2-digit",
129+
});
130+
}
131+
91132
/** PRECISION constant matching smart contract (1e12) */
92133
export const PRECISION = 1_000_000_000_000n;
93134

0 commit comments

Comments
 (0)