Files
PrivateChat/client/src/app/home-page.component.html

270 lines
11 KiB
HTML
Raw Normal View History

2026-03-09 19:35:08 +01:00
<main class="shell py-4 py-lg-5">
<div class="container-xl">
@if (!embeddedMode) {
<section class="hero-panel mb-4 mb-lg-5 p-4 p-lg-5">
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center justify-content-between gap-4">
<div class="hero-copy">
<div class="d-flex align-items-center justify-content-between gap-3">
<span class="eyebrow mb-0">WebRTC Private Chat</span>
<button
class="theme-toggle"
type="button"
(click)="cycleTheme()"
[attr.aria-label]="'Theme mode: ' + theme.mode() + '. Switch to ' + theme.nextMode()"
[title]="'Theme mode: ' + theme.mode() + '. Click for ' + theme.nextMode()"
>
<span class="theme-toggle-icon" aria-hidden="true">{{ theme.emoji() }}</span>
<span class="theme-toggle-label">{{ theme.mode() }}</span>
</button>
</div>
</div>
@if (session.currentUser(); as user) {
<div class="session-card p-3 p-lg-4">
<div class="text-uppercase small text-secondary mb-2">Signed in</div>
<div class="h4 mb-1">{{ user.displayName }}</div>
<div class="text-secondary mb-3">{{ user.username }}</div>
<div class="small status-pill mb-3">{{ session.status() }}</div>
2026-03-09 20:40:21 +01:00
<button class="btn btn-accent w-100 mb-2" type="button" (click)="openChatUi()">
2026-03-09 19:35:08 +01:00
Open chat UI
</button>
@if (session.isApprovalAdmin()) {
<a class="btn btn-outline-light w-100 mb-2" routerLink="/approvals">Approve accounts</a>
}
<button class="btn btn-outline-light w-100" type="button" (click)="logout()">Log out</button>
</div>
}
</div>
</section>
}
@if (!session.currentUser()) {
@if (embeddedMode) {
<section class="panel p-4 p-lg-5 text-center">
<h2 class="h3 mb-3">Open Settings to sign in</h2>
<p class="text-secondary mb-0">
This embedded client expects authentication and backend configuration from the native app settings.
</p>
</section>
} @else {
<section class="row g-4 align-items-stretch">
<div class="col-lg-6">
<div class="panel p-4 h-100">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="h3 mb-1">Connect to the signaling backend</h2>
<p class="text-secondary mb-0">Use the Fastify server for authentication and peer discovery.</p>
</div>
<span class="badge rounded-pill text-bg-dark">Angular + Bootstrap</span>
</div>
<div class="mb-3">
<label class="form-label" for="serverUrl">Backend URL</label>
<input
id="serverUrl"
name="serverUrl"
class="form-control form-control-lg"
[(ngModel)]="serverUrl"
placeholder="http://localhost:3000"
/>
</div>
<div class="btn-group mb-4 w-100" role="group" aria-label="Authentication mode">
<button
class="btn"
[class.btn-primary]="authMode === 'login'"
[class.btn-outline-primary]="authMode !== 'login'"
type="button"
(click)="authMode = 'login'"
>
Log in
</button>
<button
class="btn"
[class.btn-primary]="authMode === 'register'"
[class.btn-outline-primary]="authMode !== 'register'"
type="button"
(click)="authMode = 'register'"
>
Register
</button>
</div>
<form class="d-grid gap-3" (ngSubmit)="submitAuth()">
@if (authMode === 'register') {
<div>
<label class="form-label" for="displayName">Display name</label>
<input
id="displayName"
name="displayName"
class="form-control form-control-lg"
[(ngModel)]="displayName"
placeholder="Operator One"
/>
</div>
}
<div>
<label class="form-label" for="username">Username</label>
<input
id="username"
name="username"
class="form-control form-control-lg"
[(ngModel)]="username"
placeholder="alice"
[attr.autocomplete]="authMode === 'login' ? 'username webauthn' : 'username'"
/>
</div>
<div>
<label class="form-label" for="password">Password</label>
<input
id="password"
name="password"
type="password"
class="form-control form-control-lg"
[(ngModel)]="password"
placeholder="At least 8 characters"
autocomplete="current-password"
/>
</div>
<button class="btn btn-accent btn-lg mt-2" type="submit">
{{ authMode === 'login' ? 'Enter chat' : 'Create account' }}
</button>
@if (authMode === 'login' && session.webAuthnSupported()) {
<div class="text-center small text-secondary mt-1">or</div>
<button class="btn btn-outline-light btn-lg" type="button" (click)="loginWithAccessKey()">
🔑 Use access key
</button>
<div class="small text-secondary">
Leave the username blank to choose from discoverable passkeys, or enter it to target one account.
</div>
}
</form>
@if (session.error()) {
<div class="alert alert-danger mt-4 mb-0">{{ session.error() }}</div>
}
@if (session.notice()) {
<div class="alert alert-success mt-4 mb-0">{{ session.notice() }}</div>
}
</div>
</div>
<div class="col-lg-6">
<div class="panel panel-muted p-4 h-100">
<h2 class="h3 mb-3">Transport model</h2>
<div class="info-rail d-grid gap-3">
<article>
<div class="small text-uppercase text-secondary mb-2">1. Authenticate</div>
<p class="mb-0">Register or log in against the Fastify API to receive a JWT.</p>
</article>
<article>
<div class="small text-uppercase text-secondary mb-2">2. Discover peers</div>
<p class="mb-0">Open the WebSocket signaling session and receive the online peer list.</p>
</article>
<article>
<div class="small text-uppercase text-secondary mb-2">3. Exchange data directly</div>
<p class="mb-0">Create a WebRTC data channel and send text, JSON, or files peer-to-peer.</p>
</article>
</div>
</div>
</div>
</section>
}
} @else {
<section class="row g-4 align-items-stretch">
<div class="col-lg-5">
<div class="panel p-4 h-100">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<h2 class="h3 mb-1">Connection settings</h2>
<p class="text-secondary mb-0">Manage the backend endpoint used for auth and signaling.</p>
</div>
@if (session.isApprovalAdmin()) {
<a class="btn btn-sm btn-outline-light" routerLink="/approvals">Approvals</a>
}
</div>
@if (!embeddedMode) {
<label class="form-label" for="connectedServerUrl">Backend URL</label>
<div class="input-group mb-3">
<input
id="connectedServerUrl"
class="form-control"
[(ngModel)]="serverUrl"
(blur)="applyServerUrl()"
/>
<button class="btn btn-outline-secondary" type="button" (click)="applyServerUrl()">Apply</button>
</div>
} @else {
<div class="empty-state p-4 text-center text-secondary">
Backend settings are managed by the native app in embedded mode.
</div>
}
<div class="small status-pill mt-3">{{ session.status() }}</div>
@if (session.error()) {
<div class="alert alert-danger mt-4 mb-0">{{ session.error() }}</div>
}
@if (session.notice()) {
<div class="alert alert-success mt-4 mb-0">{{ session.notice() }}</div>
}
</div>
</div>
<div class="col-lg-7">
<div class="panel p-4 h-100">
<section class="access-key-panel">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<h3 class="h5 mb-1">Access keys</h3>
<p class="small text-secondary mb-0">Register one or more WebAuthn credentials for this account.</p>
</div>
<span class="badge rounded-pill text-bg-dark">{{ session.accessKeys().length }}</span>
</div>
@if (!embeddedMode && session.webAuthnSupported()) {
<div class="input-group mb-3">
<input
class="form-control"
[(ngModel)]="accessKeyLabel"
placeholder="Laptop passkey"
/>
<button class="btn btn-outline-light" type="button" (click)="registerAccessKey()">Add key</button>
</div>
} @else if (!embeddedMode) {
<div class="alert alert-warning mb-3">This browser does not expose WebAuthn registration APIs.</div>
} @else {
<div class="empty-state p-3 text-center text-secondary mb-3">
Access keys are managed through the native app in embedded mode.
</div>
}
<div class="d-grid gap-2">
@if (session.accessKeys().length === 0) {
<div class="empty-state p-3 text-center text-secondary">No access keys registered yet.</div>
}
@for (key of session.accessKeys(); track key.id) {
<article class="access-key-card p-3">
<div class="fw-semibold">{{ key.label }}</div>
<div class="small text-secondary">Device: {{ key.deviceType }}{{ key.backedUp ? ' / backed up' : '' }}</div>
<div class="small text-secondary">Transports: {{ key.transports.length > 0 ? key.transports.join(', ') : 'unspecified' }}</div>
<div class="small text-secondary">Added: {{ key.createdAt | date: 'medium' }}</div>
</article>
}
</div>
</section>
</div>
</div>
</section>
}
</div>
</main>