diff --git a/frontend/angular.json b/frontend/angular.json index 0534e656..66351670 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -7,7 +7,7 @@ "root": "", "sourceRoot": "src", "projectType": "application", - "prefix": "app", + "prefix": "ov", "schematics": { "@schematics/angular:component": { "style": "scss" @@ -134,6 +134,41 @@ } } } + }, + "shared-call-components": { + "projectType": "library", + "root": "projects/shared-call-components", + "sourceRoot": "projects/shared-call-components/src", + "prefix": "ov", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/shared-call-components/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/shared-call-components/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/shared-call-components/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "tsConfig": "projects/shared-call-components/tsconfig.spec.json", + "polyfills": ["zone.js", "zone.js/testing"] + } + } + } } }, "cli": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7ca462dd..70955166 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,8 +19,9 @@ "@angular/platform-browser-dynamic": "18.2.5", "@angular/router": "18.2.5", "core-js": "^3.38.1", - "openvidu-components-angular": "file:openvidu-components-angular-3.0.0-beta3-dev.tgz", + "openvidu-components-angular": "3.0.0-beta2", "rxjs": "7.8.1", + "tslib": "^2.3.0", "unique-names-generator": "^4.7.1", "zone.js": "0.14.10" }, @@ -11390,23 +11391,22 @@ } }, "node_modules/openvidu-components-angular": { - "version": "3.0.0-beta3-dev", - "resolved": "file:openvidu-components-angular-3.0.0-beta3-dev.tgz", - "integrity": "sha512-sT4ewgQAbeH9aziomXuVwPHQACUVKBebOOGX2vRtOVjkCzeknMAkMoBZ8g499eY6Gi1Rrldk7pGkeBIAgioh5Q==", + "version": "3.0.0-beta2", + "resolved": "https://registry.npmjs.org/openvidu-components-angular/-/openvidu-components-angular-3.0.0-beta2.tgz", + "integrity": "sha512-u3BD3RCGZWkOQIXXR+I1L0q9IvDKTVKvmbIZvqyTDjPYGqmZ9SWrao7qx7OEcYfn1beVbLgTpJc6wl+okiaMjA==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "^17.0.0 || ^18.0.0", "@angular/common": "^17.0.0 || ^18.0.0", "@angular/core": "^17.0.0 || ^18.0.0", "@angular/forms": "^17.0.0 || ^18.0.0", "@angular/material": "^17.0.0 || ^18.0.0", - "@livekit/track-processors": "^0.3.2", + "@livekit/track-processors": "0.3.2", "autolinker": "^4.0.0", "buffer": "^6.0.3", - "livekit-client": "^2.1.0" + "livekit-client": "2.1.0" } }, "node_modules/optionator": { diff --git a/frontend/package.json b/frontend/package.json index a02f7b58..4f22456b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "core-js": "^3.38.1", "openvidu-components-angular": "3.0.0-beta2", "rxjs": "7.8.1", + "tslib": "^2.3.0", "unique-names-generator": "^4.7.1", "zone.js": "0.14.10" }, @@ -53,6 +54,7 @@ "scripts": { "dev:start": "ng serve --configuration development --port=5080 --host=0.0.0.0", "dev:build": "./node_modules/@angular/cli/bin/ng.js build --output-path ../backend/public/", + "install:lib": "npm run lib:build && npm run lib:pack&& npm install ../shared-call-components/dist/shared-call-components/shared-call-components-**.tgz", "build": "func() { ./node_modules/@angular/cli/bin/ng.js build --configuration production --base-href=\"${1:-/}\"; }; func", "build-and-copy": "npm run build && mkdir -p ../backend/dist/public && cp -r dist/openvidu-call/* ../backend/dist/public", "e2e:all": "tsc --project ./e2e && npx mocha --recursive --timeout 30000 ./e2e/dist/**/*.test.js", @@ -60,6 +62,10 @@ "e2e:room": "tsc --project ./e2e && npx mocha --recursive --timeout 30000 ./e2e/dist/room.test.js", "e2e:recordings": "tsc --project ./e2e && npx mocha --recursive --timeout 30000 ./e2e/dist/recording.test.js", "e2e:auth": "tsc --project ./e2e && npx mocha --recursive --timeout 30000 ./e2e/dist/auth.test.js", + "lib:serve": "ng build shared-call-components --watch", + "lib:build": "ng build shared-call-components", + "lib:pack": "cd dist/shared-call-components && npm pack", + "lib:sync-call-pro": "rm -rf ../../openvidu-call-pro/frontend/node_modules/shared-call-components && cp dist/shared-call-components/shared-call-components-**.tgz ../../openvidu-call-pro/frontend && cd ../../openvidu-call-pro/frontend && npm install shared-call-components-**.tgz", "test": "ng test openvidu-call --watch=false --code-coverage", "lint": "eslint src --fix", "format": "prettier --ignore-path ../gitignore . --write" diff --git a/frontend/projects/shared-call-components/README.md b/frontend/projects/shared-call-components/README.md new file mode 100644 index 00000000..8ec85fab --- /dev/null +++ b/frontend/projects/shared-call-components/README.md @@ -0,0 +1,24 @@ +# SharedCallComponents + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.0. + +## Code scaffolding + +Run `ng generate component component-name --project shared-call-components` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project shared-call-components`. +> Note: Don't forget to add `--project shared-call-components` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build shared-call-components` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build shared-call-components`, go to the dist folder `cd dist/shared-call-components` and run `npm publish`. + +## Running unit tests + +Run `ng test shared-call-components` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/frontend/projects/shared-call-components/ng-package.json b/frontend/projects/shared-call-components/ng-package.json new file mode 100644 index 00000000..8df365d5 --- /dev/null +++ b/frontend/projects/shared-call-components/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/shared-call-components", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/frontend/projects/shared-call-components/package.json b/frontend/projects/shared-call-components/package.json new file mode 100644 index 00000000..c2ec2058 --- /dev/null +++ b/frontend/projects/shared-call-components/package.json @@ -0,0 +1,12 @@ +{ + "name": "shared-call-components", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^18.2.0", + "@angular/core": "^18.2.0" + }, + "dependencies": { + "tslib": "^2.3.0" + }, + "sideEffects": false +} diff --git a/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.html b/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.html new file mode 100644 index 00000000..953c6c4c --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.html @@ -0,0 +1,14 @@ + +
+
+ + @if (isHovering && isEnabled) { + add_a_photo + } +
+
+
{{ title || '' }}
+
{{ description || 'Select your app logo' }}
+
+
+
diff --git a/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.scss b/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.scss new file mode 100644 index 00000000..11439eb2 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.scss @@ -0,0 +1,61 @@ +.logo-card { + max-width: 100%; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.3s ease-in-out; + position: relative; // Para posicionar el icono de selección + + &:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + } +} + +.card-content { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; +} + +.logo-container { + position: relative; // Para posicionar el icono de selección + border-radius: 4px; +} + +.app-logo { + width: 64px; // Ajusta el tamaño del logo + height: 64px; // Ajusta el tamaño del logo + border-radius: 4px; // Opcional: para esquinas redondeadas + object-fit: cover; // Mantiene la proporción de la imagen +} + +.select-icon { + position: absolute; // Para que el icono se superponga al logo + top: 50%; // Centrado verticalmente + left: 50%; // Centrado horizontalmente + transform: translate(-50%, -50%); // Ajuste del centro + color: #673ab7; // Color del icono + font-size: 36px; // Tamaño del icono + opacity: 0; // Inicialmente oculto + transition: opacity 0.2s ease; // Transición suave +} + +.logo-card:hover .select-icon { + opacity: 1; // Mostrar el icono al hacer hover +} + +.text-container { + display: flex; + flex-direction: column; +} + +.card-title { + font-size: 1.2rem; + font-weight: 600; + color: #333; +} + +.card-subtitle { + font-size: 0.9rem; + color: #666; +} \ No newline at end of file diff --git a/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.spec.ts b/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.spec.ts new file mode 100644 index 00000000..36094222 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LogoCardComponent } from './logo-card.component'; + +describe('LogoCardComponent', () => { + let component: LogoCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LogoCardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LogoCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.ts b/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.ts new file mode 100644 index 00000000..af63800a --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/card/logo-card/logo-card.component.ts @@ -0,0 +1,32 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; + +@Component({ + selector: 'ov-logo-card', + standalone: true, + imports: [CommonModule, MatCardModule, MatIconModule], + templateUrl: './logo-card.component.html', + styleUrl: './logo-card.component.scss' +}) +export class LogoCardComponent { + @Input() title: string = ''; + @Input() description: string = ''; + @Input() logoSrc: string = ''; // URL de la imagen del logo + @Input() cardBackgroundColor: string = '#ffffff'; + @Input() isEnabled: boolean = true; + + isHovering: boolean = false; // Estado para controlar el hover + + // Métodos para manejar el mouseenter y mouseleave + onMouseEnter() { + if (this.isEnabled) { + this.isHovering = true; + } + } + + onMouseLeave() { + this.isHovering = false; + } +} diff --git a/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.html b/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.html new file mode 100644 index 00000000..57ce7136 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.html @@ -0,0 +1,16 @@ + +
+
+ {{ icon }} +
+
+
{{ title || '' }}
+
{{ description || 'OpenVidu' }}
+
+
+ + +
diff --git a/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.scss b/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.scss new file mode 100644 index 00000000..a703404c --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.scss @@ -0,0 +1,67 @@ +.toggle-card { + max-width: 100%; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.3s ease-in-out; +} + +.toggle-card:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.card-content { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; +} + +.icon-container { + background-color: #673ab7; /* Color de fondo del icono */ + padding: 16px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.card-icon { + color: white; + font-size: 24px; +} + +.text-container { + display: flex; + flex-direction: column; +} + +.card-title { + font-size: 1.2rem; + font-weight: 600; + color: #333; +} + +.card-subtitle { + font-size: 0.9rem; + color: #666; +} + +.card-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 8px; + padding: 10px; + background-color: #f8f9fa; + border-top: 1px solid #e9ecef; +} + +.view-more { + font-size: 0.9rem; + color: #007bff; + text-decoration: none; +} + +.view-more:hover { + text-decoration: underline; +} diff --git a/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.spec.ts b/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.spec.ts new file mode 100644 index 00000000..d86c2824 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToggleCardComponent } from './toggle-card.component'; + +describe('ToggleCardComponent', () => { + let component: ToggleCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ToggleCardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ToggleCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.ts b/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.ts new file mode 100644 index 00000000..dd0c1e98 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/card/toggle-card/toggle-card.component.ts @@ -0,0 +1,59 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; + +@Component({ + selector: 'ov-toggle-card', + standalone: true, + imports: [MatCardModule, MatIconModule, MatSlideToggleModule], + templateUrl: './toggle-card.component.html', + styleUrl: './toggle-card.component.scss' +}) +export class ToggleCardComponent { + /** + * The title of the dynamic card component. + * This input property allows setting a custom title for the card. + */ + @Input() title: string = ''; + /** + * A brief description of the dynamic card component. + * This input property allows setting a description for the card. + */ + @Input() description: string = ''; + /** + * The name of the icon to be displayed. Defaults to "settings". + * + * @type {string} + * @default 'settings' + */ + @Input() icon: string = 'settings'; // Nombre del ícono (por defecto "settings") + /** + * The background color of the icon. + * + * @default '#000000' + */ + @Input() iconBackgroundColor: string = '#000000'; + /** + * The background color of the card component. + * Accepts any valid CSS color string. + */ + @Input() cardBackgroundColor: string = '#ffffff'; + + /** + * A boolean input property that determines the toggle state. Only applicable when `footerType` is set to `'toggle'`. + * Defaults to `false`. + */ + @Input() toggleValue: boolean = false; + + @Output() onToggleValueChanged = new EventEmitter(); + @Output() onTextLinkClicked = new EventEmitter(); + + onToggleChange(event: any) { + this.onToggleValueChanged.emit(event.checked); + } + + onLinkClick() { + this.onTextLinkClicked.emit(); + } +} diff --git a/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.html b/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.html new file mode 100644 index 00000000..f04345d2 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.html @@ -0,0 +1,60 @@ + + + + + + OpenVidu Console + + + + + + + + + + + + + + + + + + {{ link.icon }} + @if (!isSideMenuCollapsed) { + {{ link.label }} + } + + + + + + + + + + + +
+ + +
+
+
diff --git a/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.scss b/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.scss new file mode 100644 index 00000000..484b5eb8 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.scss @@ -0,0 +1,89 @@ +#dashboard-container, +mat-sidenav-container { + height: 100%; +} + +.page-content { + height: 100%; + padding: 15px; +} +.toolbar-title { + font-size: 1.5rem; + font-weight: bold; + margin-left: 16px; +} + +.toolbar-spacer { + flex: 1 1 auto; +} + +mat-toolbar { + position: fixed; + top: 0; + z-index: 2; +} + +mat-sidenav-container { + height: 100%; +} + +.expanded { + width: 250px; +} +.collapsed { + width: 70px; +} +a { + padding: 0; +} + +// Move the content down so that it won't be hidden by the toolbar +mat-sidenav { + background-color: white; + padding-top: 3.5rem; + @media screen and (min-width: 600px) { + padding-top: 4rem; + } + + .entry { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem; + justify-content: start; + } + + .centeredEntry { + justify-content: center !important; + + } +} + +// Move the content down so that it won't be hidden by the toolbar +mat-sidenav-content { + padding-top: 3.5rem; + @media screen and (min-width: 600px) { + padding-top: 4rem; + } +} + +.toolbar-spacer { + flex: 1 1 auto; +} + +.main-container { + padding: 0px 2rem; + height: 100%; +} + +.section-title { + padding-bottom: 0px; +} + +.menu-button { + height: 60px !important; + text-align: justify; +} +.menu-hr { + margin: 0px !important; +} diff --git a/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.spec.ts b/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.spec.ts new file mode 100644 index 00000000..58ba78b0 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConsoleNavComponent } from './console-nav.component'; + +describe('ConsoleNavComponent', () => { + let component: ConsoleNavComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConsoleNavComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConsoleNavComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.ts b/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.ts new file mode 100644 index 00000000..1d46b95a --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/console-nav/console-nav.component.ts @@ -0,0 +1,36 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatListModule } from '@angular/material/list'; +import { MatSidenav, MatSidenavModule } from '@angular/material/sidenav'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { RouterModule } from '@angular/router'; +import { ConsoleNavLink } from '@lib/models/sidenav.model'; + +@Component({ + selector: 'ov-console-nav', + standalone: true, + imports: [CommonModule, MatToolbarModule, MatListModule, MatButtonModule, MatIconModule, MatSidenavModule, RouterModule], + templateUrl: './console-nav.component.html', + styleUrl: './console-nav.component.scss' +}) +export class ConsoleNavComponent { + @ViewChild(MatSidenav) sidenav!: MatSidenav; + isMobile = false; + isTablet = false; + isSideMenuCollapsed = false; + @Input() navLinks: ConsoleNavLink[] = []; + + @Output() onLogoutClicked: EventEmitter = new EventEmitter(); + + async toggleSideMenu() { + if (this.isMobile) { + this.isSideMenuCollapsed = false; + await this.sidenav.toggle(); + } else { + this.isSideMenuCollapsed = !this.isSideMenuCollapsed; + await this.sidenav.open(); + } + } +} diff --git a/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.html b/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.html new file mode 100644 index 00000000..b36bd75d --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.scss b/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.scss new file mode 100644 index 00000000..3042433c --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.scss @@ -0,0 +1,5 @@ +.card-container { + display: grid; + grid-gap: 16px; + width: 100%; +} diff --git a/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.spec.ts b/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.spec.ts new file mode 100644 index 00000000..7b3c8947 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DynamicGridComponent } from './dynamic-grid.component'; + +describe('DynamicGridComponent', () => { + let component: DynamicGridComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DynamicGridComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DynamicGridComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.ts b/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.ts new file mode 100644 index 00000000..77d782bc --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/dynamic-grid/dynamic-grid.component.ts @@ -0,0 +1,39 @@ +import { CommonModule } from '@angular/common'; +import { Component, HostListener, Input } from '@angular/core'; +import { MatGridListModule } from '@angular/material/grid-list'; + +@Component({ + selector: 'ov-dynamic-grid', + standalone: true, + imports: [CommonModule, MatGridListModule], + templateUrl: './dynamic-grid.component.html', + styleUrl: './dynamic-grid.component.scss' +}) +export class DynamicGridComponent { + @Input() maxColumns: number = 3; // Maximum number of columns + columns: number = 1; // Current number of columns + private itemsCount: number = 0; + + ngOnInit() { + this.updateColumns(); + } + + @HostListener('window:resize', ['$event']) + onResize() { + this.updateColumns(); + } + + private updateColumns() { + // Count the number of items + const content = document.querySelector('.card-container'); + if (content) { + this.itemsCount = content.childElementCount; + const containerWidth = content.clientWidth; + const columnWidth = 350; // Minimum width of each card + + // Calculate the number of columns based on the container width + const calculatedColumns = Math.floor(containerWidth / columnWidth); + this.columns = Math.min(calculatedColumns, this.maxColumns, this.itemsCount); // Set the number of columns + } + } +} diff --git a/frontend/projects/shared-call-components/src/lib/components/index.ts b/frontend/projects/shared-call-components/src/lib/components/index.ts new file mode 100644 index 00000000..f882792d --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/components/index.ts @@ -0,0 +1,4 @@ +export * from './console-nav/console-nav.component'; +export * from './card/toggle-card/toggle-card.component'; +export * from './dynamic-grid/dynamic-grid.component'; +export * from './card/logo-card/logo-card.component'; diff --git a/frontend/projects/shared-call-components/src/lib/models/index.ts b/frontend/projects/shared-call-components/src/lib/models/index.ts new file mode 100644 index 00000000..ee32ea21 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/models/index.ts @@ -0,0 +1 @@ +export * from '@lib/models/sidenav.model'; \ No newline at end of file diff --git a/frontend/projects/shared-call-components/src/lib/models/sidenav.model.ts b/frontend/projects/shared-call-components/src/lib/models/sidenav.model.ts new file mode 100644 index 00000000..531dc708 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/models/sidenav.model.ts @@ -0,0 +1,6 @@ +export interface ConsoleNavLink { + label: string; // Nombre del enlace + icon?: string; // Icono opcional + route?: string; // Ruta para la navegación (opcional) + clickHandler?: () => void; // Función para manejar clics (opcional) + } diff --git a/frontend/projects/shared-call-components/src/lib/services/context.service.spec.ts b/frontend/projects/shared-call-components/src/lib/services/context.service.spec.ts new file mode 100644 index 00000000..0057967d --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/services/context.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ContextService } from './context.service'; + +describe('ContextService', () => { + let service: ContextService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ContextService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/projects/shared-call-components/src/lib/services/context.service.ts b/frontend/projects/shared-call-components/src/lib/services/context.service.ts new file mode 100644 index 00000000..21055799 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/services/context.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Service to manage the context of the application, including embedded mode and token management. + */ +export class ContextService { + /** + * Indicates whether the application is in embedded mode. + */ + private embeddedMode: boolean; + + /** + * Stores the token for the current session. + */ + private token: string | null; + + /** + * Initializes a new instance of the ContextService class. + */ + constructor() {} + + /** + * Sets the embedded mode of the application. + * @param isEmbedded - A boolean indicating whether the application is in embedded mode. + */ + setEmbeddedMode(isEmbedded: boolean): void { + this.embeddedMode = isEmbedded; + } + + /** + * Checks if the application is in embedded mode. + * @returns A boolean indicating whether the application is in embedded mode. + */ + isEmbeddedMode(): boolean { + return this.embeddedMode; + } + + /** + * Sets the token for the current session. + * @param token - A string representing the token. + */ + setToken(token: string): void { + this.token = token; + console.log(token); + //TODO Parse token + } + + /** + * Retrieves the token for the current session. + * @returns A string representing the token, or null if no token is set. + */ + getToken(): string | null { + return this.token; + } +} diff --git a/frontend/projects/shared-call-components/src/lib/services/http.service.spec.ts b/frontend/projects/shared-call-components/src/lib/services/http.service.spec.ts new file mode 100644 index 00000000..7b345d48 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/services/http.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { HttpService } from './http.service'; + +describe('HttpService', () => { + let service: HttpService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(HttpService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/projects/shared-call-components/src/lib/services/http.service.ts b/frontend/projects/shared-call-components/src/lib/services/http.service.ts new file mode 100644 index 00000000..fd908137 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/services/http.service.ts @@ -0,0 +1,176 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { RecordingInfo } from 'openvidu-components-angular'; +import { lastValueFrom } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class HttpService { + // private baseHref: string; + private pathPrefix = 'call/api'; + + constructor( + private http: HttpClient, + ) { + // this.baseHref = '/' + (!!window.location.pathname.split('/')[1] ? window.location.pathname.split('/')[1] + '/' : ''); + } + + private generateUserHeaders(): HttpHeaders { + const headers = new HttpHeaders({ + 'Content-Type': 'application/json' + }); + //! TODO: Fix this + const userCredentials = undefined; //this.storageService.getParticipantCredentials(); + + if (userCredentials?.username && userCredentials?.password) { + return headers.append( + 'Authorization', + `Basic ${btoa(`${userCredentials.username}:${userCredentials.password}`)}` + ); + } + + return headers; + } + + private generateAdminHeaders(): HttpHeaders { + const headers = new HttpHeaders({ + 'Content-Type': 'application/json' + }); + // TODO: Fix this + const adminCredentials = undefined; // this.storageService.getAdminCredentials(); + + if (!adminCredentials) { + console.error('Admin credentials not found'); + return headers; + } + + const { username, password } = adminCredentials; + + if (username && password) { + return headers.set('Authorization', `Basic ${btoa(`${username}:${password}`)}`); + } + + return headers; + } + + async getConfig() { + return this.getRequest(`${this.pathPrefix}/config`); + } + + getToken(roomName: string, participantName: string): Promise<{ token: string }> { + const headers = this.generateUserHeaders(); + + return this.postRequest(`${this.pathPrefix}/rooms`, { roomName, participantName }, headers); + } + + adminLogin(body: { username: string; password: string }): Promise<{ message: string }> { + return this.postRequest(`${this.pathPrefix}/admin/login`, body); + } + + adminLogout(): Promise<{ message: string }> { + return this.postRequest(`${this.pathPrefix}/admin/logout`, {}); + } + + userLogin(body: { username: string; password: string }): Promise<{ message: string }> { + return this.postRequest(`${this.pathPrefix}/login`, body); + } + + userLogout(): Promise<{ message: string }> { + return this.postRequest(`${this.pathPrefix}/logout`, {}); + } + + getRecordings(continuationToken?: string): Promise<{ recordings: RecordingInfo[]; continuationToken: string }> { + let path = `${this.pathPrefix}/admin/recordings`; + + if (continuationToken) { + path += `?continuationToken=${continuationToken}`; + } + + const headers = this.generateAdminHeaders(); + + return this.getRequest(path, headers); + } + + startRecording(roomName: string): Promise { + const headers = this.generateUserHeaders(); + + return this.postRequest(`${this.pathPrefix}/recordings`, { roomName }, headers); + } + + stopRecording(recordingId: string): Promise { + const headers = this.generateUserHeaders(); + return this.putRequest(`${this.pathPrefix}/recordings/${recordingId}`, {}, headers); + } + + deleteRecording(recordingId: string): Promise { + const headers = this.generateUserHeaders(); + return this.deleteRequest(`${this.pathPrefix}/recordings/${recordingId}`, headers); + } + + deleteRecordingByAdmin(recordingId: string): Promise { + const headers = this.generateAdminHeaders(); + return this.deleteRequest(`${this.pathPrefix}/admin/recordings/${recordingId}`, headers); + } + + startBroadcasting(roomName: string, broadcastUrl: string): Promise { + const body = { roomName, broadcastUrl }; + const headers = this.generateUserHeaders(); + return this.postRequest(`${this.pathPrefix}/broadcasts/`, body, headers); + } + + stopBroadcasting(broadcastId: string): Promise { + const headers = this.generateUserHeaders(); + return this.putRequest(`${this.pathPrefix}/broadcasts/${broadcastId}`, {}, headers); + } + + private postRequest(path: string, body: any, headers?: HttpHeaders): Promise { + try { + return lastValueFrom(this.http.post(path, body, { headers })); + } catch (error) { + if (error.status === 404) { + throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; + } + + throw error; + } + } + + private getRequest(path: string, headers?: HttpHeaders): any { + try { + return lastValueFrom(this.http.get(path, { headers })); + } catch (error) { + if (error.status === 404) { + throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; + } + + throw error; + } + } + + private deleteRequest(path: string, headers?: HttpHeaders) { + try { + return lastValueFrom(this.http.delete(path, { headers })); + } catch (error) { + console.log(error); + + if (error.status === 404) { + throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; + } + + throw error; + } + } + + private putRequest(path: string, body: any = {}, headers?: HttpHeaders) { + try { + return lastValueFrom(this.http.put(path, body, { headers })); + } catch (error) { + if (error.status === 404) { + throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; + } + + throw error; + } + } +} diff --git a/frontend/projects/shared-call-components/src/lib/services/index.ts b/frontend/projects/shared-call-components/src/lib/services/index.ts new file mode 100644 index 00000000..6a4f0430 --- /dev/null +++ b/frontend/projects/shared-call-components/src/lib/services/index.ts @@ -0,0 +1,2 @@ +export * from './context.service'; +export * from './http.service'; diff --git a/frontend/projects/shared-call-components/src/public-api.ts b/frontend/projects/shared-call-components/src/public-api.ts new file mode 100644 index 00000000..2380ced5 --- /dev/null +++ b/frontend/projects/shared-call-components/src/public-api.ts @@ -0,0 +1,7 @@ +/* + * Public API Surface of shared-call-components + */ + +export * from '@lib/components/index'; +export * from '@lib/services/index'; +export * from '@lib/models/index'; \ No newline at end of file diff --git a/frontend/projects/shared-call-components/tsconfig.lib.json b/frontend/projects/shared-call-components/tsconfig.lib.json new file mode 100644 index 00000000..39075f94 --- /dev/null +++ b/frontend/projects/shared-call-components/tsconfig.lib.json @@ -0,0 +1,14 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + + "exclude": ["**/*.spec.ts"] +} diff --git a/frontend/projects/shared-call-components/tsconfig.lib.prod.json b/frontend/projects/shared-call-components/tsconfig.lib.prod.json new file mode 100644 index 00000000..9215caac --- /dev/null +++ b/frontend/projects/shared-call-components/tsconfig.lib.prod.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/frontend/projects/shared-call-components/tsconfig.spec.json b/frontend/projects/shared-call-components/tsconfig.spec.json new file mode 100644 index 00000000..254686d5 --- /dev/null +++ b/frontend/projects/shared-call-components/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 772e9804..1c67898f 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,13 +1,24 @@ import { Routes } from '@angular/router'; -import { HomeComponent } from '@pages/home/home.component'; -import { AdminDashboardComponent } from '@pages/admin-dashboard/admin-dashboard.component'; -import { VideoRoomComponent } from '@pages/video-room/video-room.component'; -import { roomGuard } from '@guards/room.guard'; - - +import { HomeComponent } from '@app/pages/home/home.component'; +import { VideoRoomComponent } from '@app/pages/video-room/video-room.component'; +import { ConsoleComponent } from '@app/pages/console/console.component'; +import { roomGuard } from '@app/guards/room.guard'; +import { UnauthorizedComponent } from '@app/pages/unauthorized/unauthorized.component'; +import { embeddedGuard } from '@app/guards/embedded.guard'; +import { AppearanceComponent } from '@app/pages/console/appearance/appearance.component'; +import { RoomConfigComponent } from '@app/pages/console/room-config/room-config.component'; export const routes: Routes = [ - { path: '', redirectTo: 'home', pathMatch: 'full' }, + { path: '', redirectTo: 'console', pathMatch: 'full' }, { path: 'home', component: HomeComponent }, - { path: 'admin', component: AdminDashboardComponent }, + { + path: 'console', + component: ConsoleComponent, + children: [ + { path: 'appearance', component: AppearanceComponent }, + { path: 'room-config', component: RoomConfigComponent } + ] + }, + { path: 'embedded', component: VideoRoomComponent, canActivate: [embeddedGuard] }, + { path: 'embedded/unauthorized', component: UnauthorizedComponent }, { path: ':roomName', component: VideoRoomComponent, canActivate: [roomGuard] } ]; diff --git a/frontend/src/app/guards/embedded.guard.ts b/frontend/src/app/guards/embedded.guard.ts new file mode 100644 index 00000000..c10f1121 --- /dev/null +++ b/frontend/src/app/guards/embedded.guard.ts @@ -0,0 +1,30 @@ +import { inject } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; +import { ContextService } from '@lib/services/context.service'; + +export const embeddedGuard: CanActivateFn = async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const contextService = inject(ContextService); + const router = inject(Router); + + const isEmbedded = state.url.includes('embedded'); + + contextService.setEmbeddedMode(isEmbedded); + + if (isEmbedded) { + const token = route.queryParams['token']; + + if (!token) { + // Redirect to the unauthorized page if the token is not provided + const queryParams = { reason: 'no-token' }; + router.navigate(['embedded/unauthorized'], { queryParams }); + + return false; + } + + // Redirect to the room page if the token is provided + contextService.setToken(token); + } + + // Allow access to the requested page + return true; +}; diff --git a/frontend/src/app/guards/room.guard.ts b/frontend/src/app/guards/room.guard.ts index fa39c8b9..7335b27e 100644 --- a/frontend/src/app/guards/room.guard.ts +++ b/frontend/src/app/guards/room.guard.ts @@ -1,33 +1,33 @@ import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, CanActivateFn } from '@angular/router'; -import { ConfigService } from '@services/config.service'; -import { StorageAppService } from '@services/storage.service'; -import { HttpService } from '@services/http.service'; +// import { ConfigService } from '@services/config.service'; +// import { StorageAppService } from '@services/storage.service'; +// import { HttpService } from '@services/http.service'; export const roomGuard: CanActivateFn = async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { - const configService = inject(ConfigService); - const storageService = inject(StorageAppService); - const restService = inject(HttpService); - const router = inject(Router); + // const configService = inject(ConfigService); + // const storageService = inject(StorageAppService); + // const restService = inject(HttpService); + // const router = inject(Router); - try { - await configService.initialize(); + // try { + // await configService.initialize(); - if (configService.isPrivateAccess()) { - const userCredentials = storageService.getParticipantCredentials(); + // if (configService.isPrivateAccess()) { + // const userCredentials = storageService.getParticipantCredentials(); - if (!userCredentials) { - router.navigate(['/']); - return false; - } + // if (!userCredentials) { + // router.navigate(['/']); + // return false; + // } - await restService.userLogin(userCredentials); - return true; - } - } catch (error) { - router.navigate(['/'], { queryParams: { roomName: state.url } }); - return false; - } + // await restService.userLogin(userCredentials); + // return true; + // } + // } catch (error) { + // router.navigate(['/'], { queryParams: { roomName: state.url } }); + // return false; + // } return true; }; diff --git a/frontend/src/app/pages/console/access-permissions/access-permissions.component.html b/frontend/src/app/pages/console/access-permissions/access-permissions.component.html new file mode 100644 index 00000000..6cd11d96 --- /dev/null +++ b/frontend/src/app/pages/console/access-permissions/access-permissions.component.html @@ -0,0 +1 @@ +

access-permissions works!

diff --git a/frontend/src/app/pages/console/access-permissions/access-permissions.component.scss b/frontend/src/app/pages/console/access-permissions/access-permissions.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/pages/console/access-permissions/access-permissions.component.spec.ts b/frontend/src/app/pages/console/access-permissions/access-permissions.component.spec.ts new file mode 100644 index 00000000..d2948556 --- /dev/null +++ b/frontend/src/app/pages/console/access-permissions/access-permissions.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AccessPermissionsComponent } from './access-permissions.component'; + +describe('AccessPermissionsComponent', () => { + let component: AccessPermissionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AccessPermissionsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AccessPermissionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/console/access-permissions/access-permissions.component.ts b/frontend/src/app/pages/console/access-permissions/access-permissions.component.ts new file mode 100644 index 00000000..7062cd10 --- /dev/null +++ b/frontend/src/app/pages/console/access-permissions/access-permissions.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ov-access-permissions', + standalone: true, + imports: [], + templateUrl: './access-permissions.component.html', + styleUrl: './access-permissions.component.scss' +}) +export class AccessPermissionsComponent { + +} diff --git a/frontend/src/app/pages/console/appearance/appearance.component.html b/frontend/src/app/pages/console/appearance/appearance.component.html new file mode 100644 index 00000000..4b78df5b --- /dev/null +++ b/frontend/src/app/pages/console/appearance/appearance.component.html @@ -0,0 +1,10 @@ + + + + diff --git a/frontend/src/app/pages/console/appearance/appearance.component.scss b/frontend/src/app/pages/console/appearance/appearance.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/pages/console/appearance/appearance.component.spec.ts b/frontend/src/app/pages/console/appearance/appearance.component.spec.ts new file mode 100644 index 00000000..f9ff97f9 --- /dev/null +++ b/frontend/src/app/pages/console/appearance/appearance.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppearanceComponent } from './appearance.component'; + +describe('AppearanceComponent', () => { + let component: AppearanceComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppearanceComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AppearanceComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/console/appearance/appearance.component.ts b/frontend/src/app/pages/console/appearance/appearance.component.ts new file mode 100644 index 00000000..259a3bb5 --- /dev/null +++ b/frontend/src/app/pages/console/appearance/appearance.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { DynamicGridComponent, LogoCardComponent } from 'shared-call-components'; + +@Component({ + selector: 'ov-appearance', + standalone: true, + imports: [DynamicGridComponent, LogoCardComponent], + templateUrl: './appearance.component.html', + styleUrl: './appearance.component.scss' +}) +export class AppearanceComponent {} diff --git a/frontend/src/app/pages/console/console.component.html b/frontend/src/app/pages/console/console.component.html new file mode 100644 index 00000000..3d8f16a1 --- /dev/null +++ b/frontend/src/app/pages/console/console.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/pages/console/console.component.scss b/frontend/src/app/pages/console/console.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/pages/console/console.component.spec.ts b/frontend/src/app/pages/console/console.component.spec.ts new file mode 100644 index 00000000..b778867f --- /dev/null +++ b/frontend/src/app/pages/console/console.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConsoleComponent } from './console.component'; + +describe('ConsoleComponent', () => { + let component: ConsoleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConsoleComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConsoleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/console/console.component.ts b/frontend/src/app/pages/console/console.component.ts new file mode 100644 index 00000000..3d27ae96 --- /dev/null +++ b/frontend/src/app/pages/console/console.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ConsoleNavComponent, ConsoleNavLink } from 'shared-call-components'; + +@Component({ + selector: 'app-console', + standalone: true, + imports: [ConsoleNavComponent], + templateUrl: './console.component.html', + styleUrl: './console.component.scss' +}) +export class ConsoleComponent { + navLinks: ConsoleNavLink[] = [ + { label: 'Overview', route: '/', icon: 'dashboard' }, + { label: 'Appearance', route: 'appearance', icon: 'palette' }, + { label: 'Access & Permissions', route: 'access', icon: 'lock' }, + { label: 'Room Config', route: 'room-config', icon: 'video_settings' }, + { label: 'Security', route: 'security', icon: 'security' }, + { label: 'Integrations', route: 'integrations', icon: 'integration_instructions' }, + { label: 'Support', route: 'support', icon: 'support' }, + { label: 'About', route: 'about', icon: 'info' }, + ]; + + logout() { + console.log('logout'); + } +} diff --git a/frontend/src/app/pages/console/room-config/room-config.component.html b/frontend/src/app/pages/console/room-config/room-config.component.html new file mode 100644 index 00000000..fc33b1df --- /dev/null +++ b/frontend/src/app/pages/console/room-config/room-config.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/frontend/src/app/pages/console/room-config/room-config.component.scss b/frontend/src/app/pages/console/room-config/room-config.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/pages/console/room-config/room-config.component.spec.ts b/frontend/src/app/pages/console/room-config/room-config.component.spec.ts new file mode 100644 index 00000000..a7606047 --- /dev/null +++ b/frontend/src/app/pages/console/room-config/room-config.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RoomConfigComponent } from './room-config.component'; + +describe('RoomConfigComponent', () => { + let component: RoomConfigComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RoomConfigComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RoomConfigComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/console/room-config/room-config.component.ts b/frontend/src/app/pages/console/room-config/room-config.component.ts new file mode 100644 index 00000000..76cda651 --- /dev/null +++ b/frontend/src/app/pages/console/room-config/room-config.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { DynamicGridComponent, ToggleCardComponent } from 'shared-call-components'; + +@Component({ + selector: 'ov-room-config', + standalone: true, + imports: [DynamicGridComponent, ToggleCardComponent], + templateUrl: './room-config.component.html', + styleUrl: './room-config.component.scss' +}) +export class RoomConfigComponent { + recordingEnabled = false; + broadcastingEnabled = false; + chatEnabled = false; + onRecordingToggle(checked: boolean) { + this.recordingEnabled = checked; + console.log('Recording toggled', this.recordingEnabled); + } + + onBroadcastingToggle(checked: boolean) { + this.broadcastingEnabled = checked; + console.log('Broadcasting toggled', this.broadcastingEnabled); + } + + onChatToggle(checked: boolean) { + this.chatEnabled = checked; + console.log('Chat toggled', this.chatEnabled); + } +} diff --git a/frontend/src/app/pages/home/home.component.ts b/frontend/src/app/pages/home/home.component.ts index c08e6719..23b2c1c9 100644 --- a/frontend/src/app/pages/home/home.component.ts +++ b/frontend/src/app/pages/home/home.component.ts @@ -16,9 +16,9 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { ConfigService } from '@services/config.service'; -import { HttpService } from '@services/http.service'; -import { StorageAppService } from '@services/storage.service'; +import { ConfigService } from '@app/services/config.service'; +import { StorageAppService } from '@app/services/storage.service'; +import { HttpService } from 'shared-call-components'; import packageInfo from '../../../../package.json'; @@ -53,7 +53,7 @@ export class HomeComponent implements OnInit, OnDestroy { private route: ActivatedRoute ) { this.loginForm = this.fb.group({ - username: [this.storageService.getParticipantName() ?? '', [Validators.required, Validators.minLength(4)]], + username: [/*this.storageService.getParticipantName() ??*/ '', [Validators.required, Validators.minLength(4)]], password: ['', [Validators.required, Validators.minLength(4)]] }); @@ -77,7 +77,7 @@ export class HomeComponent implements OnInit, OnDestroy { await this.httpService.userLogin(userCredentials); this.storageService.setParticipantCredentials(userCredentials); - this.username = this.storageService.getParticipantName(); + // this.username = this.storageService.getParticipantName(); this.isUserLogged = true; this.loginError = false; } diff --git a/frontend/src/app/pages/unauthorized/unauthorized.component.html b/frontend/src/app/pages/unauthorized/unauthorized.component.html new file mode 100644 index 00000000..76820466 --- /dev/null +++ b/frontend/src/app/pages/unauthorized/unauthorized.component.html @@ -0,0 +1,2 @@ +

The page you are trying to access is restricted.

+

Reason: {{ message }}

diff --git a/frontend/src/app/pages/unauthorized/unauthorized.component.scss b/frontend/src/app/pages/unauthorized/unauthorized.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/pages/unauthorized/unauthorized.component.spec.ts b/frontend/src/app/pages/unauthorized/unauthorized.component.spec.ts new file mode 100644 index 00000000..bab5ad8d --- /dev/null +++ b/frontend/src/app/pages/unauthorized/unauthorized.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UnauthorizedComponent } from './unauthorized.component'; + +describe('UnauthorizedComponent', () => { + let component: UnauthorizedComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UnauthorizedComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(UnauthorizedComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/unauthorized/unauthorized.component.ts b/frontend/src/app/pages/unauthorized/unauthorized.component.ts new file mode 100644 index 00000000..0fd5e08c --- /dev/null +++ b/frontend/src/app/pages/unauthorized/unauthorized.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +@Component({ + selector: 'app-unauthorized', + standalone: true, + imports: [], + templateUrl: './unauthorized.component.html', + styleUrl: './unauthorized.component.scss' +}) +export class UnauthorizedComponent implements OnInit { + message = 'Unauthorized access'; + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + this.route.queryParams.subscribe((params) => { + if (params['reason'] === 'no-token') { + this.message = 'No token provided'; + } + }); + } +} diff --git a/frontend/src/app/pages/video-room/video-room.component.ts b/frontend/src/app/pages/video-room/video-room.component.ts index 29b9c38f..92583227 100644 --- a/frontend/src/app/pages/video-room/video-room.component.ts +++ b/frontend/src/app/pages/video-room/video-room.component.ts @@ -11,7 +11,7 @@ import { ApiDirectiveModule } from 'openvidu-components-angular'; -import { HttpService } from '@services/http.service'; +import { HttpService } from 'shared-call-components'; @Component({ selector: 'app-video-room', diff --git a/frontend/src/app/services/config.service.ts b/frontend/src/app/services/config.service.ts index 4b2f182b..e8d35b17 100644 --- a/frontend/src/app/services/config.service.ts +++ b/frontend/src/app/services/config.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { HttpService } from '@app/services/http.service'; -import { ServerConfigurationResponse } from '@models/server.model'; +import { ServerConfigurationResponse } from '@app/models/server.model'; +import { HttpService } from 'shared-call-components'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/services/http.service.ts b/frontend/src/app/services/http.service.ts index e3018ae5..00cf8358 100644 --- a/frontend/src/app/services/http.service.ts +++ b/frontend/src/app/services/http.service.ts @@ -1,176 +1,176 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { RecordingInfo } from 'openvidu-components-angular'; -import { lastValueFrom } from 'rxjs'; -import { StorageAppService } from '@services/storage.service'; - -@Injectable({ - providedIn: 'root' -}) -export class HttpService { - // private baseHref: string; - private pathPrefix = 'call/api'; - - constructor( - private http: HttpClient, - private storageService: StorageAppService - ) { - // this.baseHref = '/' + (!!window.location.pathname.split('/')[1] ? window.location.pathname.split('/')[1] + '/' : ''); - } - - private generateUserHeaders(): HttpHeaders { - const headers = new HttpHeaders({ - 'Content-Type': 'application/json' - }); - const userCredentials = this.storageService.getParticipantCredentials(); - - if (userCredentials?.username && userCredentials?.password) { - return headers.append( - 'Authorization', - `Basic ${btoa(`${userCredentials.username}:${userCredentials.password}`)}` - ); - } - - return headers; - } - - private generateAdminHeaders(): HttpHeaders { - const headers = new HttpHeaders({ - 'Content-Type': 'application/json' - }); - const adminCredentials = this.storageService.getAdminCredentials(); - - if (!adminCredentials) { - console.error('Admin credentials not found'); - return headers; - } - - const { username, password } = adminCredentials; - - if (username && password) { - return headers.set('Authorization', `Basic ${btoa(`${username}:${password}`)}`); - } - - return headers; - } - - async getConfig() { - return this.getRequest(`${this.pathPrefix}/config`); - } - - getToken(roomName: string, participantName: string): Promise<{ token: string }> { - const headers = this.generateUserHeaders(); - - return this.postRequest(`${this.pathPrefix}/rooms`, { roomName, participantName }, headers); - } - - adminLogin(body: { username: string; password: string }): Promise<{ message: string }> { - return this.postRequest(`${this.pathPrefix}/admin/login`, body); - } - - adminLogout(): Promise<{ message: string }> { - return this.postRequest(`${this.pathPrefix}/admin/logout`, {}); - } - - userLogin(body: { username: string; password: string }): Promise<{ message: string }> { - return this.postRequest(`${this.pathPrefix}/login`, body); - } - - userLogout(): Promise<{ message: string }> { - return this.postRequest(`${this.pathPrefix}/logout`, {}); - } - - getRecordings(continuationToken?: string): Promise<{ recordings: RecordingInfo[]; continuationToken: string }> { - let path = `${this.pathPrefix}/admin/recordings`; - - if (continuationToken) { - path += `?continuationToken=${continuationToken}`; - } - - const headers = this.generateAdminHeaders(); - - return this.getRequest(path, headers); - } - - startRecording(roomName: string): Promise { - const headers = this.generateUserHeaders(); - - return this.postRequest(`${this.pathPrefix}/recordings`, { roomName }, headers); - } - - stopRecording(recordingId: string): Promise { - const headers = this.generateUserHeaders(); - return this.putRequest(`${this.pathPrefix}/recordings/${recordingId}`, {}, headers); - } - - deleteRecording(recordingId: string): Promise { - const headers = this.generateUserHeaders(); - return this.deleteRequest(`${this.pathPrefix}/recordings/${recordingId}`, headers); - } - - deleteRecordingByAdmin(recordingId: string): Promise { - const headers = this.generateAdminHeaders(); - return this.deleteRequest(`${this.pathPrefix}/admin/recordings/${recordingId}`, headers); - } - - startBroadcasting(roomName: string, broadcastUrl: string): Promise { - const body = { roomName, broadcastUrl }; - const headers = this.generateUserHeaders(); - return this.postRequest(`${this.pathPrefix}/broadcasts/`, body, headers); - } - - stopBroadcasting(broadcastId: string): Promise { - const headers = this.generateUserHeaders(); - return this.putRequest(`${this.pathPrefix}/broadcasts/${broadcastId}`, {}, headers); - } - - private postRequest(path: string, body: any, headers?: HttpHeaders): Promise { - try { - return lastValueFrom(this.http.post(path, body, { headers })); - } catch (error) { - if (error.status === 404) { - throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; - } - - throw error; - } - } - - private getRequest(path: string, headers?: HttpHeaders): any { - try { - return lastValueFrom(this.http.get(path, { headers })); - } catch (error) { - if (error.status === 404) { - throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; - } - - throw error; - } - } - - private deleteRequest(path: string, headers?: HttpHeaders) { - try { - return lastValueFrom(this.http.delete(path, { headers })); - } catch (error) { - console.log(error); - - if (error.status === 404) { - throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; - } - - throw error; - } - } - - private putRequest(path: string, body: any = {}, headers?: HttpHeaders) { - try { - return lastValueFrom(this.http.put(path, body, { headers })); - } catch (error) { - if (error.status === 404) { - throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; - } - - throw error; - } - } -} +// import { HttpClient, HttpHeaders } from '@angular/common/http'; +// import { Injectable } from '@angular/core'; +// import { RecordingInfo } from 'openvidu-components-angular'; +// import { lastValueFrom } from 'rxjs'; +// import { StorageAppService } from '@services/storage.service'; + +// @Injectable({ +// providedIn: 'root' +// }) +// export class HttpService { +// // private baseHref: string; +// private pathPrefix = 'call/api'; + +// constructor( +// private http: HttpClient, +// private storageService: StorageAppService +// ) { +// // this.baseHref = '/' + (!!window.location.pathname.split('/')[1] ? window.location.pathname.split('/')[1] + '/' : ''); +// } + +// private generateUserHeaders(): HttpHeaders { +// const headers = new HttpHeaders({ +// 'Content-Type': 'application/json' +// }); +// const userCredentials = this.storageService.getParticipantCredentials(); + +// if (userCredentials?.username && userCredentials?.password) { +// return headers.append( +// 'Authorization', +// `Basic ${btoa(`${userCredentials.username}:${userCredentials.password}`)}` +// ); +// } + +// return headers; +// } + +// private generateAdminHeaders(): HttpHeaders { +// const headers = new HttpHeaders({ +// 'Content-Type': 'application/json' +// }); +// const adminCredentials = this.storageService.getAdminCredentials(); + +// if (!adminCredentials) { +// console.error('Admin credentials not found'); +// return headers; +// } + +// const { username, password } = adminCredentials; + +// if (username && password) { +// return headers.set('Authorization', `Basic ${btoa(`${username}:${password}`)}`); +// } + +// return headers; +// } + +// async getConfig() { +// return this.getRequest(`${this.pathPrefix}/config`); +// } + +// getToken(roomName: string, participantName: string): Promise<{ token: string }> { +// const headers = this.generateUserHeaders(); + +// return this.postRequest(`${this.pathPrefix}/rooms`, { roomName, participantName }, headers); +// } + +// adminLogin(body: { username: string; password: string }): Promise<{ message: string }> { +// return this.postRequest(`${this.pathPrefix}/admin/login`, body); +// } + +// adminLogout(): Promise<{ message: string }> { +// return this.postRequest(`${this.pathPrefix}/admin/logout`, {}); +// } + +// userLogin(body: { username: string; password: string }): Promise<{ message: string }> { +// return this.postRequest(`${this.pathPrefix}/login`, body); +// } + +// userLogout(): Promise<{ message: string }> { +// return this.postRequest(`${this.pathPrefix}/logout`, {}); +// } + +// getRecordings(continuationToken?: string): Promise<{ recordings: RecordingInfo[]; continuationToken: string }> { +// let path = `${this.pathPrefix}/admin/recordings`; + +// if (continuationToken) { +// path += `?continuationToken=${continuationToken}`; +// } + +// const headers = this.generateAdminHeaders(); + +// return this.getRequest(path, headers); +// } + +// startRecording(roomName: string): Promise { +// const headers = this.generateUserHeaders(); + +// return this.postRequest(`${this.pathPrefix}/recordings`, { roomName }, headers); +// } + +// stopRecording(recordingId: string): Promise { +// const headers = this.generateUserHeaders(); +// return this.putRequest(`${this.pathPrefix}/recordings/${recordingId}`, {}, headers); +// } + +// deleteRecording(recordingId: string): Promise { +// const headers = this.generateUserHeaders(); +// return this.deleteRequest(`${this.pathPrefix}/recordings/${recordingId}`, headers); +// } + +// deleteRecordingByAdmin(recordingId: string): Promise { +// const headers = this.generateAdminHeaders(); +// return this.deleteRequest(`${this.pathPrefix}/admin/recordings/${recordingId}`, headers); +// } + +// startBroadcasting(roomName: string, broadcastUrl: string): Promise { +// const body = { roomName, broadcastUrl }; +// const headers = this.generateUserHeaders(); +// return this.postRequest(`${this.pathPrefix}/broadcasts/`, body, headers); +// } + +// stopBroadcasting(broadcastId: string): Promise { +// const headers = this.generateUserHeaders(); +// return this.putRequest(`${this.pathPrefix}/broadcasts/${broadcastId}`, {}, headers); +// } + +// private postRequest(path: string, body: any, headers?: HttpHeaders): Promise { +// try { +// return lastValueFrom(this.http.post(path, body, { headers })); +// } catch (error) { +// if (error.status === 404) { +// throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; +// } + +// throw error; +// } +// } + +// private getRequest(path: string, headers?: HttpHeaders): any { +// try { +// return lastValueFrom(this.http.get(path, { headers })); +// } catch (error) { +// if (error.status === 404) { +// throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; +// } + +// throw error; +// } +// } + +// private deleteRequest(path: string, headers?: HttpHeaders) { +// try { +// return lastValueFrom(this.http.delete(path, { headers })); +// } catch (error) { +// console.log(error); + +// if (error.status === 404) { +// throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; +// } + +// throw error; +// } +// } + +// private putRequest(path: string, body: any = {}, headers?: HttpHeaders) { +// try { +// return lastValueFrom(this.http.put(path, body, { headers })); +// } catch (error) { +// if (error.status === 404) { +// throw { status: error.status, message: 'Cannot connect with backend. ' + error.url + ' not found' }; +// } + +// throw error; +// } +// } +// } diff --git a/frontend/src/app/services/storage.service.ts b/frontend/src/app/services/storage.service.ts index af6f6869..c37a4f41 100644 --- a/frontend/src/app/services/storage.service.ts +++ b/frontend/src/app/services/storage.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { LoggerService, StorageService } from 'openvidu-components-angular'; -import { STORAGE_PREFIX, StorageAppKeys } from '../models/storage.model'; +// import { LoggerService, StorageService } from 'openvidu-components-angular'; +// import { STORAGE_PREFIX, StorageAppKeys } from '../models/storage.model'; /** * @internal @@ -8,50 +8,50 @@ import { STORAGE_PREFIX, StorageAppKeys } from '../models/storage.model'; @Injectable({ providedIn: 'root' }) -export class StorageAppService extends StorageService { - constructor(loggerSrv: LoggerService) { - super(loggerSrv); - this.PREFIX_KEY = STORAGE_PREFIX; - } +export class StorageAppService /*extends StorageService*/ { + // constructor(loggerSrv: LoggerService) { + // super(loggerSrv); + // this.PREFIX_KEY = STORAGE_PREFIX; + // } setAdminCredentials(credentials: { username: string; password: string }) { - const encodedCredentials = btoa(`${credentials.username}:${credentials.password}`); - this.set(StorageAppKeys.ADMIN_CREDENTIALS, encodedCredentials); + // const encodedCredentials = btoa(`${credentials.username}:${credentials.password}`); + // this.set(StorageAppKeys.ADMIN_CREDENTIALS, encodedCredentials); } getAdminCredentials(): { username: string; password: string } | undefined { - const encodedCredentials = this.get(StorageAppKeys.ADMIN_CREDENTIALS); + // const encodedCredentials = this.get(StorageAppKeys.ADMIN_CREDENTIALS); - if (encodedCredentials) { - const [username, password] = atob(encodedCredentials).split(':'); - return { username, password }; - } + // if (encodedCredentials) { + // const [username, password] = atob(encodedCredentials).split(':'); + // return { username, password }; + // } return undefined; } clearAdminCredentials() { - this.remove(StorageAppKeys.ADMIN_CREDENTIALS); + // this.remove(StorageAppKeys.ADMIN_CREDENTIALS); } setParticipantCredentials(credentials: { username: string; password: string }) { - const encodedCredentials = btoa(`${credentials.username}:${credentials.password}`); - this.setParticipantName(credentials.username); - this.set(StorageAppKeys.PARTICIPANT_CREDENTIALS, encodedCredentials); + // const encodedCredentials = btoa(`${credentials.username}:${credentials.password}`); + // this.setParticipantName(credentials.username); + // this.set(StorageAppKeys.PARTICIPANT_CREDENTIALS, encodedCredentials); } getParticipantCredentials(): { username: string; password: string } | null { - const encodedCredentials = this.get(StorageAppKeys.PARTICIPANT_CREDENTIALS); + // const encodedCredentials = this.get(StorageAppKeys.PARTICIPANT_CREDENTIALS); - if (encodedCredentials) { - const [username, password] = atob(encodedCredentials).split(':'); - return { username, password }; - } + // if (encodedCredentials) { + // const [username, password] = atob(encodedCredentials).split(':'); + // return { username, password }; + // } return null; } clearParticipantCredentials() { - this.remove(StorageAppKeys.PARTICIPANT_CREDENTIALS); + // this.remove(StorageAppKeys.PARTICIPANT_CREDENTIALS); } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 6c918769..700ed518 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -17,11 +17,13 @@ "paths": { "core-js/es7/reflect": ["./node_modules/core-js/proposals/reflect-metadata"], "@app/*": ["src/app/*"], - "@services/*": ["src/app/services/*"], - "@models/*": ["src/app/models/*"], - "@guards/*": ["src/app/guards/*"], - "@pages/*": ["src/app/pages/*"], - "@environment/*": ["src/environments/*"] + "@lib/*": ["projects/shared-call-components/src/lib/*"], + // "@services/*": ["src/app/services/*"], + // "@models/*": ["src/app/models/*"], + // "@guards/*": ["src/app/guards/*"], + // "@pages/*": ["src/app/pages/*"], + "@environment/*": ["src/environments/*"], + "shared-call-components": ["dist/shared-call-components"] }, "useDefineForClassFields": false }