refactor: reorganize component structure and update imports

This commit is contained in:
Chneemann 2025-04-21 10:03:03 +02:00
parent 9c14319236
commit d77aee2b9f
35 changed files with 698 additions and 689 deletions

View file

@ -1,17 +1,17 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
import { ImprintComponent } from './shared/components/legal-information/imprint/imprint.component'; import { ImprintComponent } from './shared/components/legal-information/imprint/imprint.component';
import { PrivacyPolicyComponent } from './shared/components/legal-information/privacy-policy/privacy-policy.component'; import { PrivacyPolicyComponent } from './shared/components/legal-information/privacy-policy/privacy-policy.component';
import { BrowseComponent } from './components/home/browse/browse.component'; import { HomeComponent } from './components/home/home.component';
import { AuthComponent } from './components/auth/auth.component';
import { AuthGuard } from './auth.guard'; import { AuthGuard } from './auth.guard';
export const routes: Routes = [ export const routes: Routes = [
{ path: '', component: HomeComponent }, { path: '', component: AuthComponent },
{ path: 'login', component: HomeComponent }, { path: 'login', component: AuthComponent },
{ path: 'register', component: HomeComponent }, { path: 'register', component: AuthComponent },
{ path: 'forgot-password', component: HomeComponent }, { path: 'forgot-password', component: AuthComponent },
{ path: 'verify-email', component: HomeComponent }, { path: 'verify-email', component: AuthComponent },
{ path: 'imprint', component: ImprintComponent }, { path: 'imprint', component: ImprintComponent },
{ path: 'privacy-policy', component: PrivacyPolicyComponent }, { path: 'privacy-policy', component: PrivacyPolicyComponent },
{ path: 'browse', component: BrowseComponent, canActivate: [AuthGuard] }, { path: 'browse', component: HomeComponent, canActivate: [AuthGuard] },
]; ];

View file

@ -1,48 +1,22 @@
<section> <section class="bg-home">
<div class="headline"> <app-header />
<p>Movies, TV shows, and more</p>
<p>Watch whenever you want wherever you want</p> <!-- Non auth Routing -->
<p>Enter your email to create or restart your subscription.</p> @switch (currentRoute) { @case ('') {
</div> <app-landing-page />
<form } @case ('login') {
#taskForm="ngForm" <app-login />
(ngSubmit)="onSubmit(taskForm, mail)" } @case ('register') {
onsubmit="return false" <app-register />
> } @case ('forgot-password') {
<input <app-forgot-password />
type="mail" } @case ('verify-email') {
id="mail" <app-verify-email />
name="mail" } }
#mail="ngModel"
placeholder="Email Address" <app-footer />
class="custom-input"
autocomplete="email" @if (errorService.displayError) {
[(ngModel)]="authData.mail" <app-error-toast />
[class.error-border]=" }
(!mail.valid && mail.touched) ||
(mail.touched &&
!isUserEmailValid(authData.mail.toLowerCase()) &&
authData.mail.length > 0)
"
required
/>
<div class="error-msg">
@if (!mail.valid && mail.touched) {
<p>Please enter your email</p>
} @else { @if (mail.touched && authData.mail.length > 0 &&
!isUserEmailValid(authData.mail.toLowerCase())) {
<p>This is not a valid email format</p>
}}
</div>
<app-btn-large
[value]="'Sign Up'"
[imgPath]="'play'"
[imgPath]="'arrow_right'"
[imgReverse]="true"
[disabled]="
(!isUserEmailValid(authData.mail) && authData.mail.length > 0) ||
authData.send
"
></app-btn-large>
</form>
</section> </section>

View file

@ -1,88 +1,4 @@
@use "./../../../assets/style/colors.scss" as *;
@use "./../../../assets/style/form.scss" as *;
@use "./../../../assets/style/auth-layout.scss" as *;
section { section {
background-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), width: 100dvw;
url(./../../../assets/img/backgrounds/home.png); height: 100dvh;
}
.headline {
p:nth-child(1) {
font-size: 48px;
font-weight: 700;
padding: 12px;
}
p:nth-child(2) {
font-size: 24px;
font-weight: 500;
padding: 12px;
}
p:nth-child(3) {
font-size: 18px;
font-weight: 400;
padding: 12px;
}
}
form {
flex-direction: row;
position: relative;
width: 500px;
padding: 24px;
input {
width: 65%;
padding: 12px 18px;
margin: 8px 0;
color: $white;
border: 2px solid $white;
border-radius: 24px;
font-size: 18px;
&::placeholder {
color: rgba($white, 0.6);
}
}
.error-border {
border: 2px solid $red;
}
}
.error-msg {
position: absolute;
bottom: 0;
padding: 6px;
width: 57%;
}
/*------------- RESPONSIVE -------------*/
@media screen and (max-width: 550px) {
form {
flex-direction: column;
width: 90%;
input {
width: 100%;
}
}
.error-msg {
position: relative;
padding: 0;
height: 12px;
margin-bottom: 8px;
}
}
@media screen and (max-width: 500px) {
.headline {
p:nth-child(1) {
font-size: 36px;
}
p:nth-child(2) {
font-size: 20px;
}
p:nth-child(3) {
font-size: 16px;
}
}
} }

View file

@ -1,56 +1,44 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms'; import { HeaderComponent } from '../../shared/components/header/header.component';
import { BtnLargeComponent } from '../../shared/components/buttons/btn-large/btn-large.component'; import { FooterComponent } from '../../shared/components/footer/footer.component';
import { Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { RegisterComponent } from '../auth/register/register.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { LoginComponent } from '../auth/login/login.component';
import { ForgotPasswordComponent } from '../auth/forgot-password/forgot-password.component';
import { VerifyEmailComponent } from '../auth/verify-email/verify-email.component';
import { ErrorToastComponent } from '../../shared/components/error-toast/error-toast.component';
import { ErrorService } from '../../services/error.service'; import { ErrorService } from '../../services/error.service';
import { AuthService } from '../../services/auth.service'; import { LandingPageComponent } from './landing-page/landing-page.component';
@Component({ @Component({
selector: 'app-auth', selector: 'app-auth',
standalone: true, standalone: true,
imports: [CommonModule, BtnLargeComponent, FormsModule], imports: [
CommonModule,
HeaderComponent,
FooterComponent,
LandingPageComponent,
RegisterComponent,
LoginComponent,
ForgotPasswordComponent,
VerifyEmailComponent,
ErrorToastComponent,
],
templateUrl: './auth.component.html', templateUrl: './auth.component.html',
styleUrl: './auth.component.scss', styleUrl: './auth.component.scss',
}) })
export class AuthComponent { export class AuthComponent implements OnInit {
authData = { currentRoute: any;
mail: '',
send: false,
};
constructor( constructor(
private router: Router, private route: ActivatedRoute,
public errorService: ErrorService, public errorService: ErrorService
private authService: AuthService
) {} ) {}
isUserEmailValid(emailValue: string) { ngOnInit(): void {
const emailRegex = /^[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,}$/; this.route.url.subscribe((url) => {
return emailRegex.test(emailValue); this.currentRoute = url[0]?.path || '';
} });
async onSubmit(ngForm: NgForm, mailInput: any) {
if (ngForm.submitted && ngForm.form.valid) {
await this.checkDuplicatesEmail();
} else {
mailInput.control.markAsTouched();
}
}
async checkDuplicatesEmail() {
const body = {
email: this.authData.mail,
};
try {
this.authData.send = true;
await this.authService.checkAuthUserMail(body);
const queryParams = { mail: this.authData.mail };
this.router.navigate(['/register'], { queryParams });
this.errorService.clearError();
} catch (error) {
this.authData.send = false;
this.errorService.handleError(error);
}
} }
} }

View file

@ -0,0 +1,48 @@
<section>
<div class="headline">
<p>Movies, TV shows, and more</p>
<p>Watch whenever you want wherever you want</p>
<p>Enter your email to create or restart your subscription.</p>
</div>
<form
#taskForm="ngForm"
(ngSubmit)="onSubmit(taskForm, mail)"
onsubmit="return false"
>
<input
type="mail"
id="mail"
name="mail"
#mail="ngModel"
placeholder="Email Address"
class="custom-input"
autocomplete="email"
[(ngModel)]="authData.mail"
[class.error-border]="
(!mail.valid && mail.touched) ||
(mail.touched &&
!isUserEmailValid(authData.mail.toLowerCase()) &&
authData.mail.length > 0)
"
required
/>
<div class="error-msg">
@if (!mail.valid && mail.touched) {
<p>Please enter your email</p>
} @else { @if (mail.touched && authData.mail.length > 0 &&
!isUserEmailValid(authData.mail.toLowerCase())) {
<p>This is not a valid email format</p>
}}
</div>
<app-btn-large
[value]="'Sign Up'"
[imgPath]="'play'"
[imgPath]="'arrow_right'"
[imgReverse]="true"
[disabled]="
(!isUserEmailValid(authData.mail) && authData.mail.length > 0) ||
authData.send
"
></app-btn-large>
</form>
</section>

View file

@ -0,0 +1,88 @@
@use "./../../../../assets/style/colors.scss" as *;
@use "./../../../../assets/style/form.scss" as *;
@use "./../../../../assets/style/auth-layout.scss" as *;
section {
background-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)),
url(./../../../../assets/img/backgrounds/home.png);
}
.headline {
p:nth-child(1) {
font-size: 48px;
font-weight: 700;
padding: 12px;
}
p:nth-child(2) {
font-size: 24px;
font-weight: 500;
padding: 12px;
}
p:nth-child(3) {
font-size: 18px;
font-weight: 400;
padding: 12px;
}
}
form {
flex-direction: row;
position: relative;
width: 500px;
padding: 24px;
input {
width: 65%;
padding: 12px 18px;
margin: 8px 0;
color: $white;
border: 2px solid $white;
border-radius: 24px;
font-size: 18px;
&::placeholder {
color: rgba($white, 0.6);
}
}
.error-border {
border: 2px solid $red;
}
}
.error-msg {
position: absolute;
bottom: 0;
padding: 6px;
width: 57%;
}
/*------------- RESPONSIVE -------------*/
@media screen and (max-width: 550px) {
form {
flex-direction: column;
width: 90%;
input {
width: 100%;
}
}
.error-msg {
position: relative;
padding: 0;
height: 12px;
margin-bottom: 8px;
}
}
@media screen and (max-width: 500px) {
.headline {
p:nth-child(1) {
font-size: 36px;
}
p:nth-child(2) {
font-size: 20px;
}
p:nth-child(3) {
font-size: 16px;
}
}
}

View file

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowseComponent } from './browse.component'; import { LandingPageComponent } from './landing-page.component';
describe('BrowseComponent', () => { describe('LandingPageComponent', () => {
let component: BrowseComponent; let component: LandingPageComponent;
let fixture: ComponentFixture<BrowseComponent>; let fixture: ComponentFixture<LandingPageComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [BrowseComponent] imports: [LandingPageComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(BrowseComponent); fixture = TestBed.createComponent(LandingPageComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View file

@ -0,0 +1,56 @@
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { BtnLargeComponent } from '../../../shared/components/buttons/btn-large/btn-large.component';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common';
import { ErrorService } from '../../../services/error.service';
import { AuthService } from '../../../services/auth.service';
@Component({
selector: 'app-landing-page',
standalone: true,
imports: [CommonModule, BtnLargeComponent, FormsModule],
templateUrl: './landing-page.component.html',
styleUrl: './landing-page.component.scss',
})
export class LandingPageComponent {
authData = {
mail: '',
send: false,
};
constructor(
private router: Router,
public errorService: ErrorService,
private authService: AuthService
) {}
isUserEmailValid(emailValue: string) {
const emailRegex = /^[\w.%+-]+@[\w.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(emailValue);
}
async onSubmit(ngForm: NgForm, mailInput: any) {
if (ngForm.submitted && ngForm.form.valid) {
await this.checkDuplicatesEmail();
} else {
mailInput.control.markAsTouched();
}
}
async checkDuplicatesEmail() {
const body = {
email: this.authData.mail,
};
try {
this.authData.send = true;
await this.authService.checkAuthUserMail(body);
const queryParams = { mail: this.authData.mail };
this.router.navigate(['/register'], { queryParams });
this.errorService.clearError();
} catch (error) {
this.authData.send = false;
this.errorService.handleError(error);
}
}
}

View file

@ -1,118 +0,0 @@
<section>
<div *ngIf="isLoading"></div>
@if (!isLoading && movies.length > 0) {
<!-- If at least one film has been uploaded -->
@if (playMovie === "") {
<app-header
(moviesChange)="onMoviesChange($event)"
[showFullLogo]="false"
></app-header>
<!-- Hero Banner -->
<app-hero-banner
*ngIf="
currentMovie.length === 1 ||
(this.isWideScreen() && currentMovie.length > 1)
"
[isWideScreen]="isWideScreen()"
[currentMovie]="currentMovie"
[favoriteMovies]="favoriteMovies"
[watchedMovies]="watchedMovies"
(playMovie)="playVideo($event)"
(movieIsUploadedChange)="onMovieIsUploadedChange($event)"
(moviesChange)="onMoviesChange($event)"
(refreshChange)="onRefreshPage($event)"
(favoriteMovieChange)="onFavoriteMovieChange($event)"
></app-hero-banner>
<!-- Spacer -->
<div *ngIf="!this.isWideScreen()" class="spacer"></div>
<!-- Movie Categories -->
<app-categories
*ngIf="
this.isWideScreen() || (!this.isWideScreen() && currentMovie.length === 0)
"
[movies]="movies"
[currentMovie]="currentMovie[0]?.id"
[favoriteMovies]="favoriteMovies"
[watchedMovies]="watchedMovies"
(currentMovieId)="currentMovieId($event)"
></app-categories>
} @else {
<div class="video-overlay">
<div class="video-header">
<div class="back-button" (click)="closeVideo()">
<img src="./../../../../assets/img/back.svg" alt="Back" />
</div>
<div class="center">
<div class="resolution-controls">
<button
[ngClass]="{
'resolution-btn': true,
active: currentResolution === '360p',
'not-available': !movieIsUploaded['360']
}"
[disabled]="!movieIsUploaded['360'] || currentResolution === '360p'"
(click)="changeResolution('360p')"
>
360p
</button>
<button
[ngClass]="{
'resolution-btn': true,
active: currentResolution === '720p',
'not-available': !movieIsUploaded['720']
}"
[disabled]="!movieIsUploaded['720'] || currentResolution === '720p'"
(click)="changeResolution('720p')"
>
720p
</button>
<button
[ngClass]="{
'resolution-btn': true,
active: currentResolution === '1080p',
'not-available': !movieIsUploaded['1080']
}"
[disabled]="
!movieIsUploaded['1080'] || currentResolution === '1080p'
"
(click)="changeResolution('1080p')"
>
1080p
</button>
</div>
<p
*ngIf="
!movieIsUploaded['360'] ||
!movieIsUploaded['720'] ||
!movieIsUploaded['1080']
"
>
(If a button cannot be selected, the video will be converted, which
may take a few seconds.)
</p>
</div>
<div class="logo">
<img src="./../../../../assets/img/logo_ci.svg" alt="Logo" />
</div>
</div>
<app-video-player [playMovie]="playMovie"></app-video-player>
</div>
} } @if (!isLoading && movies.length === 0) {
<!-- If the database is empty -->
<app-header [showFullLogo]="false"></app-header>
<div class="error">
<h2>Unfortunately there are no films available in the database.</h2>
</div>
}
<div class="add-button">
<app-btn-small
[imgPath]="'add'"
(click)="toggleUploadMovieOverview(true)"
></app-btn-small>
</div>
</section>
@if (uploadMovieOverview) {
<app-upload-movie
(toggleUploadMovieOverview)="toggleUploadMovieOverview($event)"
></app-upload-movie>
}

View file

@ -1,183 +0,0 @@
@use "./../../../../assets/style/colors.scss" as *;
section {
height: 100dvh;
width: 100dvw;
background-color: $black;
overflow: hidden;
}
.video-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $black;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
overflow: hidden;
position: relative;
video {
width: 100%;
height: 100%;
}
&:hover .video-header {
opacity: 1;
}
}
.video-header {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 48px;
background: rgba($black, 0.5);
z-index: 1001;
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.center {
display: flex;
flex-direction: column;
p {
font-size: 14px;
font-weight: 400;
color: $light-blue;
text-shadow: 1px 1px 2px $black;
}
}
.spacer {
margin-top: 120px;
}
.add-button {
position: absolute;
bottom: 24px;
right: 24px;
}
.back-button {
cursor: pointer;
background: none;
border: none;
padding: 0;
img {
width: 24px;
height: 24px;
filter: brightness(0) saturate(100%) invert(100%) sepia(6%) saturate(312%)
hue-rotate(234deg) brightness(113%) contrast(100%);
transition: filter 300ms ease-in-out;
&:hover {
filter: brightness(0) saturate(100%) invert(13%) sepia(90%)
saturate(7329%) hue-rotate(344deg) brightness(105%) contrast(117%);
}
}
}
.logo img {
width: 32px;
height: auto;
}
.error {
display: flex;
justify-content: center;
position: fixed;
top: 120px;
left: 0;
right: 0;
padding: 24px;
text-align: center;
}
// Resolution Buttons
.resolution-controls {
display: flex;
justify-content: center;
gap: 12px;
margin: 18px 0;
}
.resolution-btn {
background-color: $light-blue;
color: $white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.resolution-btn.active {
background-color: $blue;
cursor: default;
}
.resolution-btn:disabled {
cursor: auto;
}
.resolution-btn:not(.active):hover {
background-color: $blue;
transform: scale(1.05);
}
.resolution-btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(38, 143, 255, 0.5);
}
.not-available {
opacity: 0.5;
background-color: $gray;
}
.not-available:not(.active):hover {
background-color: $gray;
transform: scale(1);
}
/*------------- RESPONSIVE -------------*/
@media screen and (max-width: 600px) {
.video-header {
padding: 12px 24px;
}
}
@media screen and (max-width: 550px) {
form {
flex-direction: column;
width: 90%;
input {
width: 100%;
}
}
.error-msg {
position: relative;
padding: 0;
height: 12px;
margin-bottom: 8px;
}
}
@media screen and (max-width: 500px) {
.resolution-btn {
border-radius: 2px;
padding: 3px 6px;
font-size: 14px;
}
}

View file

@ -1,142 +0,0 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { HeaderComponent } from '../../../shared/components/header/header.component';
import { HeroBannerComponent } from './hero-banner/hero-banner.component';
import { CategoriesComponent } from './categories/categories.component';
import { MovieService } from '../../../services/movie.service';
import { CommonModule } from '@angular/common';
import { VideoPlayerComponent } from './video-player/video-player.component';
import { BtnSmallComponent } from '../../../shared/components/buttons/btn-small/btn-small.component';
import { UploadMovieComponent } from './upload-movie/upload-movie.component';
import { UserService } from '../../../services/user.service';
@Component({
selector: 'app-browse',
standalone: true,
imports: [
CommonModule,
HeaderComponent,
HeroBannerComponent,
CategoriesComponent,
VideoPlayerComponent,
BtnSmallComponent,
UploadMovieComponent,
],
templateUrl: './browse.component.html',
styleUrl: './browse.component.scss',
})
export class BrowseComponent implements OnInit {
@ViewChild(VideoPlayerComponent) videoPlayer!: VideoPlayerComponent;
movies: any[] = [];
favoriteMovies: number[] = [];
watchedMovies: number[] = [];
currentMovie: any[] = [];
playMovie: string = '';
isLoading: boolean = true;
uploadMovieOverview: boolean = false;
currentResolution: '360p' | '720p' | '1080p' = '720p';
movieIsUploaded: { [resolution: string]: boolean } = {
'360': false,
'720': false,
'1080': false,
};
constructor(
private movieService: MovieService,
public userService: UserService
) {}
async ngOnInit() {
this.loadLikedAndWatchedMovies();
await this.loadAllMovies();
if (this.isWideScreen()) {
this.currentMovie.length === 0 ? this.loadRandomMovie() : null;
}
}
async loadLikedAndWatchedMovies() {
try {
const userData = await this.userService.getLikedAndWatchedMovies();
this.favoriteMovies = userData.liked_videos;
this.watchedMovies = userData.watched_videos;
} catch (error) {
console.error(error);
}
}
updateLikeMovies() {
const body = {
liked_videos: this.favoriteMovies,
};
this.userService.updateLikedMovies(body);
}
onRefreshPage(updatedMovies: any[]) {
this.currentMovie = [];
setTimeout(() => {
this.currentMovie = updatedMovies;
}, 1);
}
onMoviesChange(updatedMovies: any[]) {
if (this.isWideScreen()) {
this.loadRandomMovie();
} else {
this.currentMovie = updatedMovies;
}
}
isWideScreen() {
return window.innerWidth > 600;
}
onMovieIsUploadedChange(newStatus: { [resolution: string]: boolean }) {
this.movieIsUploaded = newStatus;
}
onFavoriteMovieChange(favoriteMovies: any) {
this.favoriteMovies = favoriteMovies;
this.updateLikeMovies();
}
changeResolution(resolution: '360p' | '720p' | '1080p') {
if (this.videoPlayer && this.movieIsUploaded[resolution.replace('p', '')]) {
this.videoPlayer.switchResolution(resolution);
this.currentResolution = resolution;
}
}
async loadAllMovies() {
this.isLoading = true;
try {
this.movies = await this.movieService.getAllMovies();
} finally {
this.isLoading = false;
}
}
closeVideo(): void {
this.playMovie = '';
}
playVideo(videoPath: string) {
this.currentResolution = '720p';
this.playMovie = videoPath;
}
loadRandomMovie(): void {
const randomIndex = Math.floor(Math.random() * this.movies.length);
this.currentMovie = [this.movies[randomIndex]];
}
currentMovieId(movieId: number) {
let index = this.movies.findIndex((movie) => movie.id === movieId);
if (index !== -1) {
this.currentMovie = [];
this.currentMovie.push(this.movies[index]);
}
}
toggleUploadMovieOverview(value: any) {
this.uploadMovieOverview = value;
}
}

View file

@ -1,4 +1,4 @@
@use "./../../../../../assets/style/colors.scss" as *; @use "./../../../../assets/style/colors.scss" as *;
section { section {
margin-top: 12px; margin-top: 12px;

View file

@ -7,7 +7,7 @@ import {
Input, Input,
Output, Output,
} from '@angular/core'; } from '@angular/core';
import { environment } from '../../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { MoviesListComponent } from './movie-list/movie-list.component'; import { MoviesListComponent } from './movie-list/movie-list.component';
@Component({ @Component({

View file

@ -1,4 +1,4 @@
@use "./../../../../../../assets/style/colors.scss" as *; @use "./../../../../../assets/style/colors.scss" as *;
.movies { .movies {
display: flex; display: flex;

View file

@ -1,5 +1,5 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter } from '@angular/core';
import { environment } from '../../../../../../environments/environment'; import { environment } from '../../../../../environments/environment';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({

View file

@ -1,4 +1,4 @@
@use "./../../../../../assets/style/colors.scss" as *; @use "./../../../../assets/style/colors.scss" as *;
section { section {
height: 400px; height: 400px;

View file

@ -9,11 +9,11 @@ import {
SimpleChanges, SimpleChanges,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { BtnLargeComponent } from '../../../../shared/components/buttons/btn-large/btn-large.component'; import { BtnLargeComponent } from '../../../shared/components/buttons/btn-large/btn-large.component';
import { MovieService } from '../../../../services/movie.service'; import { MovieService } from '../../../services/movie.service';
import { environment } from '../../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { BtnSmallComponent } from '../../../../shared/components/buttons/btn-small/btn-small.component'; import { BtnSmallComponent } from '../../../shared/components/buttons/btn-small/btn-small.component';
import { UserService } from '../../../../services/user.service'; import { UserService } from '../../../services/user.service';
@Component({ @Component({
selector: 'app-hero-banner', selector: 'app-hero-banner',

View file

@ -1,12 +1,118 @@
<section class="bg-home"> <section>
<app-header></app-header> <div *ngIf="isLoading"></div>
<app-auth *ngIf="currentRoute === ''"></app-auth> @if (!isLoading && movies.length > 0) {
<app-login *ngIf="currentRoute === 'login'"></app-login> <!-- If at least one film has been uploaded -->
<app-register *ngIf="currentRoute === 'register'"></app-register> @if (playMovie === "") {
<app-forgot-password <app-header
*ngIf="currentRoute === 'forgot-password'" (moviesChange)="onMoviesChange($event)"
></app-forgot-password> [showFullLogo]="false"
<app-verify-email *ngIf="currentRoute === 'verify-email'"></app-verify-email> ></app-header>
<app-footer></app-footer> <!-- Hero Banner -->
<app-error-toast *ngIf="errorService.displayError"></app-error-toast> <app-hero-banner
*ngIf="
currentMovie.length === 1 ||
(this.isWideScreen() && currentMovie.length > 1)
"
[isWideScreen]="isWideScreen()"
[currentMovie]="currentMovie"
[favoriteMovies]="favoriteMovies"
[watchedMovies]="watchedMovies"
(playMovie)="playVideo($event)"
(movieIsUploadedChange)="onMovieIsUploadedChange($event)"
(moviesChange)="onMoviesChange($event)"
(refreshChange)="onRefreshPage($event)"
(favoriteMovieChange)="onFavoriteMovieChange($event)"
></app-hero-banner>
<!-- Spacer -->
<div *ngIf="!this.isWideScreen()" class="spacer"></div>
<!-- Movie Categories -->
<app-categories
*ngIf="
this.isWideScreen() || (!this.isWideScreen() && currentMovie.length === 0)
"
[movies]="movies"
[currentMovie]="currentMovie[0]?.id"
[favoriteMovies]="favoriteMovies"
[watchedMovies]="watchedMovies"
(currentMovieId)="currentMovieId($event)"
></app-categories>
} @else {
<div class="video-overlay">
<div class="video-header">
<div class="back-button" (click)="closeVideo()">
<img src="./../../../../assets/img/back.svg" alt="Back" />
</div>
<div class="center">
<div class="resolution-controls">
<button
[ngClass]="{
'resolution-btn': true,
active: currentResolution === '360p',
'not-available': !movieIsUploaded['360']
}"
[disabled]="!movieIsUploaded['360'] || currentResolution === '360p'"
(click)="changeResolution('360p')"
>
360p
</button>
<button
[ngClass]="{
'resolution-btn': true,
active: currentResolution === '720p',
'not-available': !movieIsUploaded['720']
}"
[disabled]="!movieIsUploaded['720'] || currentResolution === '720p'"
(click)="changeResolution('720p')"
>
720p
</button>
<button
[ngClass]="{
'resolution-btn': true,
active: currentResolution === '1080p',
'not-available': !movieIsUploaded['1080']
}"
[disabled]="
!movieIsUploaded['1080'] || currentResolution === '1080p'
"
(click)="changeResolution('1080p')"
>
1080p
</button>
</div>
<p
*ngIf="
!movieIsUploaded['360'] ||
!movieIsUploaded['720'] ||
!movieIsUploaded['1080']
"
>
(If a button cannot be selected, the video will be converted, which
may take a few seconds.)
</p>
</div>
<div class="logo">
<img src="./../../../../assets/img/logo_ci.svg" alt="Logo" />
</div>
</div>
<app-video-player [playMovie]="playMovie"></app-video-player>
</div>
} } @if (!isLoading && movies.length === 0) {
<!-- If the database is empty -->
<app-header [showFullLogo]="false"></app-header>
<div class="error">
<h2>Unfortunately there are no films available in the database.</h2>
</div>
}
<div class="add-button">
<app-btn-small
[imgPath]="'add'"
(click)="toggleUploadMovieOverview(true)"
></app-btn-small>
</div>
</section> </section>
@if (uploadMovieOverview) {
<app-upload-movie
(toggleUploadMovieOverview)="toggleUploadMovieOverview($event)"
></app-upload-movie>
}

View file

@ -1,4 +1,183 @@
@use "./../../../assets/style/colors.scss" as *;
section { section {
width: 100dvw;
height: 100dvh; height: 100dvh;
width: 100dvw;
background-color: $black;
overflow: hidden;
}
.video-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $black;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
overflow: hidden;
position: relative;
video {
width: 100%;
height: 100%;
}
&:hover .video-header {
opacity: 1;
}
}
.video-header {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 48px;
background: rgba($black, 0.5);
z-index: 1001;
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.center {
display: flex;
flex-direction: column;
p {
font-size: 14px;
font-weight: 400;
color: $light-blue;
text-shadow: 1px 1px 2px $black;
}
}
.spacer {
margin-top: 120px;
}
.add-button {
position: absolute;
bottom: 24px;
right: 24px;
}
.back-button {
cursor: pointer;
background: none;
border: none;
padding: 0;
img {
width: 24px;
height: 24px;
filter: brightness(0) saturate(100%) invert(100%) sepia(6%) saturate(312%)
hue-rotate(234deg) brightness(113%) contrast(100%);
transition: filter 300ms ease-in-out;
&:hover {
filter: brightness(0) saturate(100%) invert(13%) sepia(90%)
saturate(7329%) hue-rotate(344deg) brightness(105%) contrast(117%);
}
}
}
.logo img {
width: 32px;
height: auto;
}
.error {
display: flex;
justify-content: center;
position: fixed;
top: 120px;
left: 0;
right: 0;
padding: 24px;
text-align: center;
}
// Resolution Buttons
.resolution-controls {
display: flex;
justify-content: center;
gap: 12px;
margin: 18px 0;
}
.resolution-btn {
background-color: $light-blue;
color: $white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.resolution-btn.active {
background-color: $blue;
cursor: default;
}
.resolution-btn:disabled {
cursor: auto;
}
.resolution-btn:not(.active):hover {
background-color: $blue;
transform: scale(1.05);
}
.resolution-btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(38, 143, 255, 0.5);
}
.not-available {
opacity: 0.5;
background-color: $gray;
}
.not-available:not(.active):hover {
background-color: $gray;
transform: scale(1);
}
/*------------- RESPONSIVE -------------*/
@media screen and (max-width: 600px) {
.video-header {
padding: 12px 24px;
}
}
@media screen and (max-width: 550px) {
form {
flex-direction: column;
width: 90%;
input {
width: 100%;
}
}
.error-msg {
position: relative;
padding: 0;
height: 12px;
margin-bottom: 8px;
}
}
@media screen and (max-width: 500px) {
.resolution-btn {
border-radius: 2px;
padding: 3px 6px;
font-size: 14px;
}
} }

View file

@ -8,10 +8,9 @@ describe('HomeComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [HomeComponent] imports: [HomeComponent],
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(HomeComponent); fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();

View file

@ -1,15 +1,13 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { HeaderComponent } from '../../shared/components/header/header.component'; import { HeaderComponent } from '../../shared/components/header/header.component';
import { FooterComponent } from '../../shared/components/footer/footer.component'; import { HeroBannerComponent } from './hero-banner/hero-banner.component';
import { AuthComponent } from '../auth/auth.component'; import { CategoriesComponent } from './categories/categories.component';
import { ActivatedRoute } from '@angular/router'; import { MovieService } from '../../services/movie.service';
import { RegisterComponent } from '../auth/register/register.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { LoginComponent } from '../auth/login/login.component'; import { VideoPlayerComponent } from './video-player/video-player.component';
import { ForgotPasswordComponent } from '../auth/forgot-password/forgot-password.component'; import { BtnSmallComponent } from '../../shared/components/buttons/btn-small/btn-small.component';
import { VerifyEmailComponent } from '../auth/verify-email/verify-email.component'; import { UploadMovieComponent } from './upload-movie/upload-movie.component';
import { ErrorToastComponent } from '../../shared/components/error-toast/error-toast.component'; import { UserService } from '../../services/user.service';
import { ErrorService } from '../../services/error.service';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -17,28 +15,128 @@ import { ErrorService } from '../../services/error.service';
imports: [ imports: [
CommonModule, CommonModule,
HeaderComponent, HeaderComponent,
FooterComponent, HeroBannerComponent,
AuthComponent, CategoriesComponent,
RegisterComponent, VideoPlayerComponent,
LoginComponent, BtnSmallComponent,
ForgotPasswordComponent, UploadMovieComponent,
VerifyEmailComponent,
ErrorToastComponent,
], ],
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.scss', styleUrl: './home.component.scss',
}) })
export class HomeComponent implements OnInit { export class HomeComponent implements OnInit {
currentRoute: any; @ViewChild(VideoPlayerComponent) videoPlayer!: VideoPlayerComponent;
movies: any[] = [];
favoriteMovies: number[] = [];
watchedMovies: number[] = [];
currentMovie: any[] = [];
playMovie: string = '';
isLoading: boolean = true;
uploadMovieOverview: boolean = false;
currentResolution: '360p' | '720p' | '1080p' = '720p';
movieIsUploaded: { [resolution: string]: boolean } = {
'360': false,
'720': false,
'1080': false,
};
constructor( constructor(
private route: ActivatedRoute, private movieService: MovieService,
public errorService: ErrorService public userService: UserService
) {} ) {}
ngOnInit(): void { async ngOnInit() {
this.route.url.subscribe((url) => { this.loadLikedAndWatchedMovies();
this.currentRoute = url[0]?.path || ''; await this.loadAllMovies();
}); if (this.isWideScreen()) {
this.currentMovie.length === 0 ? this.loadRandomMovie() : null;
}
}
async loadLikedAndWatchedMovies() {
try {
const userData = await this.userService.getLikedAndWatchedMovies();
this.favoriteMovies = userData.liked_videos;
this.watchedMovies = userData.watched_videos;
} catch (error) {
console.error(error);
}
}
updateLikeMovies() {
const body = {
liked_videos: this.favoriteMovies,
};
this.userService.updateLikedMovies(body);
}
onRefreshPage(updatedMovies: any[]) {
this.currentMovie = [];
setTimeout(() => {
this.currentMovie = updatedMovies;
}, 1);
}
onMoviesChange(updatedMovies: any[]) {
if (this.isWideScreen()) {
this.loadRandomMovie();
} else {
this.currentMovie = updatedMovies;
}
}
isWideScreen() {
return window.innerWidth > 600;
}
onMovieIsUploadedChange(newStatus: { [resolution: string]: boolean }) {
this.movieIsUploaded = newStatus;
}
onFavoriteMovieChange(favoriteMovies: any) {
this.favoriteMovies = favoriteMovies;
this.updateLikeMovies();
}
changeResolution(resolution: '360p' | '720p' | '1080p') {
if (this.videoPlayer && this.movieIsUploaded[resolution.replace('p', '')]) {
this.videoPlayer.switchResolution(resolution);
this.currentResolution = resolution;
}
}
async loadAllMovies() {
this.isLoading = true;
try {
this.movies = await this.movieService.getAllMovies();
} finally {
this.isLoading = false;
}
}
closeVideo(): void {
this.playMovie = '';
}
playVideo(videoPath: string) {
this.currentResolution = '720p';
this.playMovie = videoPath;
}
loadRandomMovie(): void {
const randomIndex = Math.floor(Math.random() * this.movies.length);
this.currentMovie = [this.movies[randomIndex]];
}
currentMovieId(movieId: number) {
let index = this.movies.findIndex((movie) => movie.id === movieId);
if (index !== -1) {
this.currentMovie = [];
this.currentMovie.push(this.movies[index]);
}
}
toggleUploadMovieOverview(value: any) {
this.uploadMovieOverview = value;
} }
} }

View file

@ -1,6 +1,6 @@
@use "./../../../../../assets/style/colors.scss" as *; @use "./../../../../assets/style/colors.scss" as *;
@use "./../../../../../assets/style/form.scss" as *; @use "./../../../../assets/style/form.scss" as *;
@use "./../../../../../assets/style/auth-layout.scss" as *; @use "./../../../../assets/style/auth-layout.scss" as *;
.center { .center {
display: flex; display: flex;

View file

@ -1,10 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Output } from '@angular/core'; import { Component, EventEmitter, Output } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms'; import { FormsModule, NgForm } from '@angular/forms';
import { ErrorService } from '../../../../services/error.service'; import { ErrorService } from '../../../services/error.service';
import { BtnLargeComponent } from '../../../../shared/components/buttons/btn-large/btn-large.component'; import { BtnLargeComponent } from '../../../shared/components/buttons/btn-large/btn-large.component';
import { MovieService } from '../../../../services/movie.service'; import { MovieService } from '../../../services/movie.service';
import { LoadingDialogComponent } from '../../../../shared/components/loading-dialog/loading-dialog.component'; import { LoadingDialogComponent } from '../../../shared/components/loading-dialog/loading-dialog.component';
@Component({ @Component({
selector: 'app-upload-movie', selector: 'app-upload-movie',