frontend: enhance share recording dialog with improved layout, error handling, and new access options

This commit is contained in:
Carlos Santos 2025-07-15 13:35:53 +02:00
parent ff741e08fe
commit 156e63e04a
5 changed files with 394 additions and 73 deletions

View File

@ -1,47 +1,108 @@
<h2 mat-dialog-title>Share Recording</h2>
<mat-dialog-content class="dialog-content">
@if (!recordingUrl) {
@if (erroMessage) {
<div class="error-text">
<mat-icon color="warn">error</mat-icon>
{{ erroMessage }}
<div class="share-recording-dialog">
<h2 mat-dialog-title class="dialog-title">
<mat-icon class="title-icon ov-recording-icon">share</mat-icon>
Share Recording
</h2>
<mat-dialog-content class="dialog-content ov-theme-transition">
@if (!recordingUrl) {
<div class="content-section fade-in">
@if (erroMessage) {
<div class="error-message">
<mat-icon class="error-icon">error</mat-icon>
<span class="error-text">{{ erroMessage }}</span>
</div>
}
<div class="access-type-section">
<h3 class="section-label">Access Type</h3>
<mat-radio-group [(ngModel)]="accessType" class="access-options">
<mat-radio-button value="public" class="access-option">
<div class="option-content">
<mat-icon class="option-icon">public</mat-icon>
<div class="option-details">
<span class="option-title">Public Access</span>
<span class="option-description">Anyone with the link can view</span>
</div>
</div>
</mat-radio-button>
<mat-radio-button value="private" class="access-option">
<div class="option-content">
<mat-icon class="option-icon">lock</mat-icon>
<div class="option-details">
<span class="option-title">Private Access</span>
<span class="option-description">Only authenticated users can view</span>
</div>
</div>
</mat-radio-button>
</mat-radio-group>
</div>
<div class="generate-section">
@if (loading) {
<div class="loading-state">
<mat-spinner diameter="20"></mat-spinner>
<span class="loading-text">Generating secure link...</span>
</div>
} @else {
<button
mat-flat-button
color="primary"
(click)="getRecordingUrl()"
[disabled]="loading"
class="generate-button"
>
<mat-icon>link</mat-icon>
Generate Shareable Link
</button>
}
</div>
</div>
}
<mat-radio-group [(ngModel)]="accessType">
<mat-radio-button value="public">Public (anyone with the link)</mat-radio-button>
<mat-radio-button value="private">Private (authenticated users)</mat-radio-button>
</mat-radio-group>
@if (recordingUrl) {
<div class="url-section fade-in">
<h3 class="section-label">Shareable Link</h3>
<div class="url-container">
<mat-form-field appearance="outline" class="url-field">
<mat-label>Recording URL</mat-label>
<input matInput [value]="recordingUrl" readonly class="url-input" />
<mat-icon matPrefix class="url-prefix-icon">link</mat-icon>
<button
mat-icon-button
matSuffix
(click)="copyToClipboard()"
[matTooltip]="copied ? 'Copied!' : 'Copy to clipboard'"
matTooltipPosition="above"
class="copy-button"
[class.copied]="copied"
>
<mat-icon>{{ copied ? 'check' : 'content_copy' }}</mat-icon>
</button>
</mat-form-field>
@if (copied) {
<div class="copy-success-message fade-in">
<mat-icon>check_circle</mat-icon>
Link copied to clipboard!
</div>
}
</div>
<div class="generate-btn-container">
@if (loading) {
<mat-spinner diameter="24"></mat-spinner>
} @else {
<button mat-flat-button color="primary" (click)="getRecordingUrl()" [disabled]="loading">
Generate Link
</button>
}
</div>
}
<div class="url-actions">
<button mat-button class="back-button" (click)="goBack()">
<mat-icon>arrow_back</mat-icon>
<span>Go back</span>
</button>
@if (recordingUrl) {
<div class="recording-url-container">
<mat-form-field appearance="outline" class="url-field">
<mat-label>Shareable URL</mat-label>
<input matInput [value]="recordingUrl" readonly />
<button
mat-icon-button
matSuffix
(click)="copyToClipboard()"
[matTooltip]="copied ? 'Copied!' : 'Copy to clipboard'"
matTooltipPosition="above"
>
<mat-icon>{{ copied ? 'check' : 'content_copy' }}</mat-icon>
</button>
</mat-form-field>
</div>
}
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close>Close</button>
</mat-dialog-actions>
</div>
</div>
}
</mat-dialog-content>
<mat-dialog-actions class="dialog-actions">
<button mat-button mat-dialog-close class="close-button">
<mat-icon>close</mat-icon>
Close
</button>
</mat-dialog-actions>
</div>

View File

@ -1,43 +1,292 @@
.dialog-content {
display: flex;
flex-direction: column;
gap: 20px;
min-width: 100%;
overflow: hidden;
@import '../../../../../../../src/assets/styles/design-tokens';
mat-radio-group {
.share-recording-dialog {
@include ov-theme-transition;
.dialog-title {
@include ov-flex-center;
justify-content: flex-start;
gap: var(--ov-meet-spacing-sm);
margin: 0;
padding: var(--ov-meet-spacing-lg) var(--ov-meet-spacing-xl);
font-size: var(--ov-meet-font-size-xl);
font-weight: var(--ov-meet-font-weight-semibold);
color: var(--ov-meet-text-primary);
.title-icon {
@include ov-icon(md);
}
}
.dialog-content {
@include ov-theme-transition;
padding: var(--ov-meet-spacing-xl);
display: flex;
flex-direction: column;
gap: 10px;
gap: var(--ov-meet-spacing-lg);
mat-radio-button {
color: var(--ov-text-surface-color);
@include ov-tablet-down {
min-width: 400px;
padding: var(--ov-meet-spacing-lg);
}
@include ov-mobile-down {
min-width: 320px;
padding: var(--ov-meet-spacing-md);
}
.content-section {
display: flex;
flex-direction: column;
gap: var(--ov-meet-spacing-lg);
}
.section-label {
margin: 0 0 var(--ov-meet-spacing-sm) 0;
font-size: var(--ov-meet-font-size-md);
font-weight: var(--ov-meet-font-weight-semibold);
color: var(--ov-meet-text-primary);
}
.error-message {
@include ov-flex-center;
justify-content: flex-start;
gap: var(--ov-meet-spacing-sm);
padding: var(--ov-meet-spacing-md);
background: rgba(var(--ov-meet-color-error), 0.1);
border: 1px solid var(--ov-meet-color-error);
border-radius: var(--ov-meet-radius-md);
color: var(--ov-meet-color-error);
.error-icon {
@include ov-icon(sm);
}
.error-text {
font-size: var(--ov-meet-font-size-sm);
font-weight: var(--ov-meet-font-weight-medium);
}
}
.access-type-section {
.access-options {
display: flex;
flex-direction: column;
gap: var(--ov-meet-spacing-sm);
.access-option {
@include ov-theme-transition;
border: 1px solid var(--ov-meet-border-color);
border-radius: var(--ov-meet-radius-sm);
padding: var(--ov-meet-spacing-md);
background: var(--ov-meet-surface-color);
&:hover {
background: var(--ov-meet-surface-hover);
border-color: var(--ov-meet-color-primary);
}
&.mat-mdc-radio-button-checked {
border-color: var(--ov-meet-color-primary);
background: rgba(var(--ov-meet-color-primary), 0.05);
}
.option-content {
@include ov-flex-center;
justify-content: flex-start;
gap: var(--ov-meet-spacing-md);
margin-left: var(--ov-meet-spacing-lg);
.option-icon {
@include ov-icon(md);
color: var(--ov-meet-text-secondary);
}
.option-details {
display: flex;
flex-direction: column;
gap: var(--ov-meet-spacing-xs);
.option-title {
font-size: var(--ov-meet-font-size-md);
font-weight: var(--ov-meet-font-weight-medium);
color: var(--ov-meet-text-primary);
}
.option-description {
font-size: var(--ov-meet-font-size-sm);
color: var(--ov-meet-text-secondary);
}
}
}
}
}
}
.generate-section {
@include ov-flex-center;
justify-content: center;
.loading-state {
@include ov-flex-center;
gap: var(--ov-meet-spacing-sm);
color: var(--ov-meet-text-secondary);
.loading-text {
font-size: var(--ov-meet-font-size-sm);
font-weight: var(--ov-meet-font-weight-medium);
}
}
.generate-button {
@include ov-button-base;
@include ov-flex-center;
gap: var(--ov-meet-spacing-sm);
mat-icon {
@include ov-icon(sm);
}
}
}
.url-section {
display: flex;
flex-direction: column;
gap: var(--ov-meet-spacing-md);
.url-container {
position: relative;
.url-field {
width: 100%;
.mat-mdc-form-field-subscript-wrapper {
display: none;
}
.url-input {
font-family: 'Courier New', monospace;
font-size: var(--ov-meet-font-size-sm);
color: var(--ov-meet-text-primary);
padding-right: var(--ov-meet-spacing-xl);
}
.url-prefix-icon {
@include ov-icon(sm);
color: var(--ov-meet-text-secondary);
margin-right: var(--ov-meet-spacing-xs);
}
.copy-button {
@include ov-theme-transition;
color: var(--ov-meet-text-secondary);
&:hover {
color: var(--ov-meet-color-primary);
background: rgba(var(--ov-meet-color-primary), 0.1);
}
&.copied {
color: var(--ov-meet-color-success);
background: rgba(var(--ov-meet-color-success), 0.1);
}
mat-icon {
@include ov-icon(sm);
}
}
}
.copy-success-message {
@include ov-flex-center;
justify-content: center;
gap: var(--ov-meet-spacing-xs);
margin-top: var(--ov-meet-spacing-sm);
padding: var(--ov-meet-spacing-sm);
background: rgba(var(--ov-meet-color-success), 0.1);
border: 1px solid var(--ov-meet-color-success);
border-radius: var(--ov-meet-radius-sm);
color: var(--ov-meet-color-success);
font-size: var(--ov-meet-font-size-sm);
font-weight: var(--ov-meet-font-weight-medium);
mat-icon {
@include ov-icon(sm);
}
}
}
.url-actions {
@include ov-flex-center;
justify-content: center;
margin-top: var(--ov-meet-spacing-md);
.back-button {
@include ov-flex-center;
gap: var(--ov-meet-spacing-xs);
color: var(--ov-meet-text-secondary);
&:hover {
color: var(--ov-meet-text-primary);
background-color: var(--ov-meet-surface-hover);
}
mat-icon {
@include ov-icon(sm);
}
}
}
}
}
.dialog-actions {
@include ov-theme-transition;
padding: var(--ov-meet-spacing-md) var(--ov-meet-spacing-xl) var(--ov-meet-spacing-lg);
border-top: 1px solid var(--ov-meet-border-color-light);
justify-content: flex-end;
.close-button {
@include ov-flex-center;
gap: var(--ov-meet-spacing-xs);
color: var(--ov-meet-text-primary);
background: var(--ov-meet-surface-hover);
mat-icon {
@include ov-icon(sm);
}
}
}
}
.error-text {
display: flex;
align-items: center;
gap: 6px;
color: var(--ov-error-color);
font-weight: 500;
}
// Responsive adjustments
@include ov-mobile-down {
.share-recording-dialog {
.dialog-content {
min-width: 280px;
padding: var(--ov-meet-spacing-md);
.generate-btn-container {
align-self: flex-start;
}
.access-type-section .access-options .access-option {
padding: var(--ov-meet-spacing-sm);
.recording-url-container {
width: 100%;
margin-top: 8px;
}
.option-content {
gap: var(--ov-meet-spacing-sm);
.url-field {
width: 100%;
.option-details {
.option-title {
font-size: var(--ov-meet-font-size-sm);
}
input {
font-size: 14px;
color: var(--ov-text-surface-color);
.option-description {
font-size: var(--ov-meet-font-size-xs);
}
}
}
}
.url-section .url-actions .back-button {
font-size: var(--ov-meet-font-size-sm);
padding: var(--ov-meet-spacing-xs) var(--ov-meet-spacing-sm);
}
}
}
}

View File

@ -82,4 +82,10 @@ export class ShareRecordingDialogComponent {
this.copied = false;
}, 2000);
}
goBack() {
this.recordingUrl = undefined;
this.copied = false;
this.erroMessage = undefined;
}
}

View File

@ -263,11 +263,12 @@ export class RecordingManagerService {
*/
openShareRecordingDialog(recordingId: string, recordingUrl?: string) {
this.dialog.open(ShareRecordingDialogComponent, {
width: '400px',
width: '450px',
data: {
recordingId,
recordingUrl
}
},
panelClass: 'ov-meet-dialog'
});
}
}

View File

@ -14,6 +14,10 @@
.ov-flex-center {
@include ov-flex-center;
}
.ov-meet-dialog {
border-radius: var(--ov-meet-spacing-md);
background: var(--ov-meet-surface-color);
}
// Spacing classes
.ov-mt-xs {