Rename product to Harbor and refine connection timer
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 12s
Build and Deploy Gateway / deploy (push) Successful in 1s

This commit is contained in:
2026-07-11 13:20:32 +03:00
parent 42c15df8c9
commit 84efbe7450
9 changed files with 305 additions and 45 deletions

View File

@@ -3,8 +3,8 @@ import { flushSync } from 'react-dom';
import { api } from '../api.js';
import {
connectionAction,
connectionDurationParts,
copyText,
formatConnectionDuration,
localProxyUrls,
subscriptionDomain,
subscriptionDaysLeft,
@@ -61,6 +61,30 @@ function InstructionBlock({ block, open, onToggle }) {
);
}
function DurationPart({ name, children }) {
return (
<span
className={`client-duration-part${name.endsWith('-value') ? ' is-value' : ' is-label'}`}
>
{children}
</span>
);
}
function HarborBrand({ isGateway }) {
const product = isGateway ? 'Gateway' : 'Connect';
return (
<div className={`harbor-brand is-${product.toLowerCase()}`} aria-label={`Harbor ${product}`}>
<svg viewBox="0 0 32 32" aria-hidden="true">
<circle cx="16" cy="6" r="3" />
<path d="M16 9v15M10 14h12M6 20c1.5 5 5 8 10 8s8.5-3 10-8M6 20l5 1M26 20l-5 1" />
</svg>
<span><strong>Harbor</strong> <em>{product}</em></span>
</div>
);
}
export function ClientOverviewPage({
state,
busy,
@@ -84,6 +108,7 @@ export function ClientOverviewPage({
const showPower = hasSubscription && Boolean(selectedTag);
const canStart = Boolean(selectedTag || state?.configExists);
const [now, setNow] = useState(Date.now());
const [durationMode, setDurationMode] = useState('digital');
const [editingSubscription, setEditingSubscription] = useState(!state?.hasSubscription);
const [pings, setPings] = useState({});
const [accessTab, setAccessTab] = useState('gateway');
@@ -103,6 +128,7 @@ export function ClientOverviewPage({
const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1';
const proxyUrls = localProxyUrls(state?.proxyPort, gatewayAddress);
const usage = subscriptionUsage(state?.userInfo);
const duration = connectionDurationParts(state?.singboxStartedAt, now);
const [displayedUsed, setDisplayedUsed] = useState(usage.used);
const hasUsage = Boolean(
state?.userInfo && ['upload', 'download', 'total', 'expire'].some((key) => key in state.userInfo),
@@ -288,8 +314,13 @@ export function ClientOverviewPage({
document.startViewTransition(update);
}
function toggleDurationMode() {
setDurationMode((mode) => mode === 'digital' ? 'words' : 'digital');
}
return (
<div className="client-shell">
<HarborBrand isGateway={isGateway} />
{hasSubscription && <button
ref={instructionsToggleRef}
className={`client-instructions-toggle${instructionsOpen ? ' is-open' : ''}`}
@@ -328,9 +359,43 @@ export function ClientOverviewPage({
</h2>
<div className="client-state-detail">
{connected ? (
<time key="duration" className="client-duration">
{formatConnectionDuration(state?.singboxStartedAt, now)}
</time>
<button
className={`client-duration-toggle${durationMode === 'words' ? ' is-words' : ''}`}
type="button"
key="duration"
aria-label={durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}
title={durationMode === 'digital' ? 'Показать время словами' : 'Показать цифровой таймер'}
onClick={toggleDurationMode}
>
<span className="client-duration-stack">
<time
className={`client-duration${durationMode === 'digital' ? ' is-active' : ''}`}
aria-hidden={durationMode !== 'digital'}
>
<DurationPart name="hours-value">{String(duration.totalHours).padStart(2, '0')}</DurationPart>
:<DurationPart name="minutes-value">{String(duration.minutes.value).padStart(2, '0')}</DurationPart>
:<DurationPart name="seconds-value">{String(duration.seconds.value).padStart(2, '0')}</DurationPart>
</time>
<time
className={`client-duration${durationMode === 'words' ? ' is-active' : ''}`}
aria-hidden={durationMode !== 'words'}
>
{[
['days', duration.days],
['hours', duration.hours],
['minutes', duration.minutes],
['seconds', duration.seconds],
]
.filter(([name, part]) => part.value || name === 'seconds')
.map(([name, part]) => (
<span className="client-duration-unit" data-unit={name} key={name}>
<DurationPart name={`${name}-value`}>{part.value}</DurationPart>{' '}
<DurationPart name={`${name}-label`}>{part.label}</DurationPart>
</span>
))}
</time>
</span>
</button>
) : (
<p key="hint">
{canStart ? 'Нажмите, чтобы включить' : 'Добавьте ссылку и выберите сервер'}