refactor: extract UI components and redesign desktop client interface with improved visual hierarchy

Extract App.tsx logic into reusable components: AppHeader, ResourcePanel, StatusCard, StatTile, and ActionButton. Replace inline markup with component composition and props-based data flow.

Redesign visual system with enhanced gradients, refined color palette, and improved spacing. Update app-shell grid layout with 18px gaps and 1140px max width. Add radial gradient overlays and linear background
This commit is contained in:
2026-03-18 11:42:34 +01:00
parent 74d8fc28cc
commit 1ddcbf0b14
7 changed files with 681 additions and 265 deletions

View File

@@ -1,5 +1,8 @@
import { FormEvent, useEffect, useMemo, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { AppHeader } from "./components/AppHeader";
import { ResourcePanel } from "./components/ResourcePanel";
import { StatusCard } from "./components/StatusCard";
type EnrollmentState = {
assignedIp: string;
@@ -191,40 +194,23 @@ export function App() {
return (
<div className="client-shell">
<div className="app-frame">
<div className="top-strip">
<div className="brand-lockup">
<img src="/icon.png" alt="NexaVPN" />
<div className="brand-copy">
<p className="eyebrow">NexaVPN</p>
<h1>VPN Client</h1>
<p>{state ? "Private access client" : "Sign in to add this device"}</p>
</div>
</div>
<div className="top-actions">
{state ? (
<button className="shell-button-secondary" disabled={syncing} onClick={onSyncProfile} type="button">
{syncing ? "Syncing..." : "Sync"}
</button>
) : null}
{state ? (
<button className="shell-button-secondary" onClick={resetEnrollment} type="button">
Logout
</button>
) : null}
<button className="shell-button" disabled={!state} onClick={toggleConnection} type="button">
{!state ? "Provision first" : connected ? "Disconnect" : "Connect"}
</button>
</div>
</div>
<div className="app-shell">
<AppHeader
connected={connected}
enrolled={Boolean(state)}
onLogout={resetEnrollment}
onSync={onSyncProfile}
onToggleConnection={toggleConnection}
syncing={syncing}
/>
<div className="body-grid">
{!state ? (
<section className="login-panel">
<div className="surface-header">
<section className="login-card">
<div className="panel-head">
<div>
<p className="eyebrow">Sign in</p>
<h3>Provision device</h3>
<p className="section-eyebrow">Sign in</p>
<h2>Provision device</h2>
</div>
</div>
<form onSubmit={onSubmit}>
@@ -241,75 +227,31 @@ export function App() {
<input type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
</label>
{error ? <div className="error">{error}</div> : null}
<div className="login-card-actions">
<button className="shell-button" disabled={loading} type="submit">
<div className="login-actions">
<button className="action-button action-button-primary" disabled={loading} type="submit">
{loading ? "Provisioning..." : "Sign in"}
</button>
</div>
</form>
</section>
) : (
<section className="status-panel">
<div className="status-top">
<div>
<p className="eyebrow">Status</p>
<h3>{connected ? "Connected" : "Disconnected"}</h3>
<div className="status-state">
<span className={`status-dot ${connected ? "online" : ""}`} />
{connected ? "Tunnel active" : "Ready"}
</div>
</div>
</div>
<div className="surface">
<div className="surface-header">
<div>
<p className="eyebrow">Connection</p>
<h4>Overview</h4>
</div>
</div>
<div className="status-grid">
<div className="detail-card">
<span>Assigned VPN IP</span>
<strong>{state.assignedIp}</strong>
</div>
<div className="detail-card">
<span>Gateway endpoint</span>
<strong>{state.gatewayEndpoint}</strong>
</div>
<div className="detail-card">
<span>Access</span>
<strong>{profileLabel}</strong>
</div>
<div className="detail-card">
<span>Last sync</span>
<strong>{state.lastSyncTime}</strong>
</div>
</div>
</div>
<div className="main-column">
<StatusCard
accessLabel={profileLabel}
assignedIp={state.assignedIp}
connected={connected}
gatewayEndpoint={state.gatewayEndpoint}
lastSyncTime={state.lastSyncTime}
/>
{error ? <div className="error">{error}</div> : null}
</section>
</div>
)}
<aside className="status-panel">
<div className="surface-header">
<div>
<p className="eyebrow">Access</p>
<h4>Resources</h4>
</div>
</div>
<ul className="resource-stack">
{(state?.resources ?? ["No resources assigned yet"]).map((resource) => (
<li key={resource}>{resource}</li>
))}
</ul>
{state ? (
<button className="shell-button-secondary" onClick={resetEnrollment} type="button">
Neu einbinden
</button>
) : null}
</aside>
<ResourcePanel
onReset={resetEnrollment}
profileLabel={profileLabel}
resources={state?.resources ?? []}
/>
</div>
</div>
</div>