diff --git a/src/app/app.component.html b/src/app/app.component.html index 4b8b9cf..67e7bd4 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,8 +1 @@ - - - chat - Chat - - - - \ No newline at end of file + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7d72c02..a9c69e6 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,12 +1,12 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'app-root', - imports: [RouterOutlet, MatToolbarModule, MatIconModule], + imports: [RouterOutlet], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) -export class AppComponent { } +export class AppComponent { + +} diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 2d9809f..0607ded 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -13,8 +13,9 @@ export const appConfig: ApplicationConfig = { provideHttpClient(), provideAnimations(), provideToastr({ - timeOut: 3000, + timeOut: 1000, positionClass: 'toast-top-right', + closeButton: true }), ] }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 5333a8b..3cbf43d 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -11,6 +11,6 @@ export const routes: Routes = [ loadChildren: () => import('./chat/chat.module').then(m => m.ChatModule), canActivate: [IsLoggedInCanActivate] }, - { path: '', redirectTo: 'auth', pathMatch: 'full' }, + { path: '', redirectTo: 'chat', pathMatch: 'full' }, { path: '**/*', redirectTo: '', pathMatch: 'full' } ]; diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts index 28f986a..60f23b2 100644 --- a/src/app/auth/auth.module.ts +++ b/src/app/auth/auth.module.ts @@ -10,6 +10,7 @@ import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; +import { ToolbarComponent } from "../common/toolbar/toolbar.component"; @NgModule({ declarations: [ @@ -24,7 +25,8 @@ import { MatButtonModule } from '@angular/material/button'; MatFormFieldModule, MatInputModule, MatButtonModule, - ReactiveFormsModule - ] + ReactiveFormsModule, + ToolbarComponent +] }) export class AuthModule { } diff --git a/src/app/auth/login/login.component.html b/src/app/auth/login/login.component.html index 8215ac4..67ed08e 100644 --- a/src/app/auth/login/login.component.html +++ b/src/app/auth/login/login.component.html @@ -1,3 +1,5 @@ + +
diff --git a/src/app/auth/login/login.component.ts b/src/app/auth/login/login.component.ts index 1696ef5..631fb46 100644 --- a/src/app/auth/login/login.component.ts +++ b/src/app/auth/login/login.component.ts @@ -35,23 +35,14 @@ export class LoginComponent implements OnInit { ).subscribe({ next: result => { if (result.error) { - this.toastrService.error(result.error, "Error", { - timeOut: 3000, - closeButton: true - }); + this.toastrService.error(result.error, "Error"); } else { - this.toastrService.info(result.message, "Success", { - timeOut: 3000, - closeButton: true - }); + this.toastrService.info(result.message, "Success"); this.router.navigateByUrl('chat'); } }, error: _ => { - this.toastrService.error("API error", "Error", { - timeOut: 3000, - closeButton: true - }); + this.toastrService.error("API error", "Error"); } }) } diff --git a/src/app/auth/register/register.component.html b/src/app/auth/register/register.component.html index 691191d..10279a1 100644 --- a/src/app/auth/register/register.component.html +++ b/src/app/auth/register/register.component.html @@ -1,3 +1,5 @@ + + diff --git a/src/app/auth/register/register.component.ts b/src/app/auth/register/register.component.ts index 24b2534..66c3a46 100644 --- a/src/app/auth/register/register.component.ts +++ b/src/app/auth/register/register.component.ts @@ -49,23 +49,14 @@ export class RegisterComponent { ).subscribe({ next: result => { if (result.error) { - this.toastrService.error(result.error, "Error", { - timeOut: 3000, - closeButton: true - }); + this.toastrService.error(result.error, "Error"); } else { - this.toastrService.info(result.message, "Success", { - timeOut: 3000, - closeButton: true - }); + this.toastrService.info(result.message, "Success"); this.router.navigateByUrl('/auth/login'); } }, error: _ => { - this.toastrService.error("API error", "Error", { - timeOut: 3000, - closeButton: true - }); + this.toastrService.error("API error", "Error"); } }) } diff --git a/src/app/chat/chat.module.ts b/src/app/chat/chat.module.ts index 05daa1f..7dee050 100644 --- a/src/app/chat/chat.module.ts +++ b/src/app/chat/chat.module.ts @@ -6,6 +6,9 @@ import { ChatComponent } from './chat/chat.component'; import { ChannelListComponent } from './chat/channel-list/channel-list.component'; import { FeedComponent } from './chat/feed/feed.component'; import { ChannelEntryComponent } from './chat/channel-list/channel-entry/channel-entry.component'; +import { MessageComponent } from './chat/feed/message/message.component'; +import { ProfilePictureComponent } from './chat/feed/message/profile-picture/profile-picture.component'; +import { ToolbarComponent } from '../common/toolbar/toolbar.component'; @NgModule({ @@ -13,11 +16,14 @@ import { ChannelEntryComponent } from './chat/channel-list/channel-entry/channel ChatComponent, ChannelListComponent, FeedComponent, - ChannelEntryComponent + ChannelEntryComponent, + MessageComponent, + ProfilePictureComponent ], imports: [ CommonModule, - ChatRoutingModule + ChatRoutingModule, + ToolbarComponent ] }) export class ChatModule { } diff --git a/src/app/chat/chat/channel-list/channel-entry/channel-entry.component.scss b/src/app/chat/chat/channel-list/channel-entry/channel-entry.component.scss index 51cdd88..3bb7cd2 100644 --- a/src/app/chat/chat/channel-list/channel-entry/channel-entry.component.scss +++ b/src/app/chat/chat/channel-list/channel-entry/channel-entry.component.scss @@ -5,6 +5,6 @@ div { .selected { color: var(--mat-sys-on-secondary); - background-color: var(--mat-sys-secondary); + background: linear-gradient(to right, var(--mat-sys-secondary), var(--mat-sys-secondary-container) 90%, rgba(0,0,0,0)); transition: all .3s ease-in-out; } \ No newline at end of file diff --git a/src/app/chat/chat/channel-list/channel-list.component.html b/src/app/chat/chat/channel-list/channel-list.component.html index 22670af..f2c8970 100644 --- a/src/app/chat/chat/channel-list/channel-list.component.html +++ b/src/app/chat/chat/channel-list/channel-list.component.html @@ -2,5 +2,5 @@ *ngFor="let channel of this.channels; let i = index" [channel]="channels[i]" (click)="Select(i)" - [selected]="channels[i].id == selectedChannel.id" + [selected]="channels[i].id == selectedChannel?.id" /> \ No newline at end of file diff --git a/src/app/chat/chat/channel-list/channel-list.component.ts b/src/app/chat/chat/channel-list/channel-list.component.ts index 8e63372..70bd3ad 100644 --- a/src/app/chat/chat/channel-list/channel-list.component.ts +++ b/src/app/chat/chat/channel-list/channel-list.component.ts @@ -1,5 +1,6 @@ -import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, OnChanges, OnInit, Output } from '@angular/core'; import { Channel } from '../../../models/channel'; +import { ChatService } from '../../../services/chat.service'; @Component({ selector: 'app-channel-list', @@ -10,27 +11,17 @@ import { Channel } from '../../../models/channel'; export class ChannelListComponent implements OnInit { @Output("select") selectEmitter: EventEmitter = new EventEmitter(); - public channels: Channel[] = [ - { - id: 0, - name: 'default', - description: 'this is the default channel' - }, - { - id: 1, - name: 'gaming', - description: 'this is another channel' - }, - ]; + public channels!: Channel[]; + public selectedChannel?: Channel; - public selectedChannel!: Channel; - - constructor() { } + constructor(private chatService: ChatService) { } ngOnInit() { - // TODO: query list of channels - - this.selectedChannel = this.channels[0]; + this.chatService.ListChannels() + .subscribe(channels => { + this.channels = channels; + this.selectedChannel = this.channels[0]; + }); } public Select(index: number): void { diff --git a/src/app/chat/chat/chat.component.html b/src/app/chat/chat/chat.component.html index 0ab9ea5..faa7a10 100644 --- a/src/app/chat/chat/chat.component.html +++ b/src/app/chat/chat/chat.component.html @@ -1,4 +1,6 @@ + +
- +
\ No newline at end of file diff --git a/src/app/chat/chat/chat.component.ts b/src/app/chat/chat/chat.component.ts index 899bd19..f1488fb 100644 --- a/src/app/chat/chat/chat.component.ts +++ b/src/app/chat/chat/chat.component.ts @@ -8,7 +8,9 @@ import { Channel } from '../../models/channel'; styleUrl: './chat.component.scss' }) export class ChatComponent { + public selectedChannel?: Channel; + public Select(channel: Channel): void { - // TODO: update feed + this.selectedChannel = channel; } } diff --git a/src/app/chat/chat/feed/feed.component.html b/src/app/chat/chat/feed/feed.component.html index e469260..575a4d5 100644 --- a/src/app/chat/chat/feed/feed.component.html +++ b/src/app/chat/chat/feed/feed.component.html @@ -1 +1,4 @@ -

feed works!

+ \ No newline at end of file diff --git a/src/app/chat/chat/feed/feed.component.scss b/src/app/chat/chat/feed/feed.component.scss index e69de29..75740b9 100644 --- a/src/app/chat/chat/feed/feed.component.scss +++ b/src/app/chat/chat/feed/feed.component.scss @@ -0,0 +1,8 @@ +:host { + color: var(--mat-sys-on-background); + background-color: var(--mat-sys-background); + + border-left: 2px solid var(--mat-sys-secondary); + border-top: 2px solid var(--mat-sys-secondary); + border-radius: 10px 0; +} \ No newline at end of file diff --git a/src/app/chat/chat/feed/feed.component.ts b/src/app/chat/chat/feed/feed.component.ts index 62c16b1..fcfcef8 100644 --- a/src/app/chat/chat/feed/feed.component.ts +++ b/src/app/chat/chat/feed/feed.component.ts @@ -1,4 +1,8 @@ -import { Component } from '@angular/core'; +import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Message } from '../../../models/message'; +import { ChatService } from '../../../services/chat.service'; +import { UserService } from '../../../services/user.service'; +import { Channel } from '../../../models/channel'; @Component({ selector: 'app-feed', @@ -6,6 +10,18 @@ import { Component } from '@angular/core'; templateUrl: './feed.component.html', styleUrl: './feed.component.scss' }) -export class FeedComponent { +export class FeedComponent implements OnChanges { + private readonly DEFAULT_CHANNEL_ID: number = 1; + @Input('channel') public channel?: Channel; + public messages!: Message[]; + + constructor(private chatService: ChatService, private userService: UserService) { } + + ngOnChanges() { + this.chatService.GetMessages(this.channel?.id ?? this.DEFAULT_CHANNEL_ID) + .subscribe(messages => { + this.messages = messages; + }); + } } diff --git a/src/app/chat/chat/feed/message/message.component.html b/src/app/chat/chat/feed/message/message.component.html new file mode 100644 index 0000000..8e34014 --- /dev/null +++ b/src/app/chat/chat/feed/message/message.component.html @@ -0,0 +1,9 @@ +
+ + +
+
{{this.message.sender}}
+
{{this.message.content}}
+
+ +
\ No newline at end of file diff --git a/src/app/chat/chat/feed/message/message.component.scss b/src/app/chat/chat/feed/message/message.component.scss new file mode 100644 index 0000000..fae8a42 --- /dev/null +++ b/src/app/chat/chat/feed/message/message.component.scss @@ -0,0 +1,35 @@ +app-profile-picture { + display: inline-block; + width: 50px; + height: 50px; + + margin: 10px; +} + +.message-container { + display: grid; + grid-template-columns: 70px 1fr; + grid-template-rows: 1fr; + grid-column-gap: 0px; + grid-row-gap: 0px; + + .message-inner-container { + margin-top: 10px; + + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1.5rem 1fr; + grid-column-gap: 0px; + grid-row-gap: 0px; + + height: fit-content; + + .message-sender { + vertical-align: top; + } + + .message-content { + height: fit-content; + } + } +} \ No newline at end of file diff --git a/src/app/chat/chat/feed/message/message.component.ts b/src/app/chat/chat/feed/message/message.component.ts new file mode 100644 index 0000000..64c85be --- /dev/null +++ b/src/app/chat/chat/feed/message/message.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core'; +import { Message } from '../../../../models/message'; + +@Component({ + selector: 'app-message', + standalone: false, + templateUrl: './message.component.html', + styleUrl: './message.component.scss' +}) +export class MessageComponent { + @Input('message') public message!: Message; +} diff --git a/src/app/chat/chat/feed/message/profile-picture/profile-picture.component.html b/src/app/chat/chat/feed/message/profile-picture/profile-picture.component.html new file mode 100644 index 0000000..08dfcb4 --- /dev/null +++ b/src/app/chat/chat/feed/message/profile-picture/profile-picture.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/chat/chat/feed/message/profile-picture/profile-picture.component.scss b/src/app/chat/chat/feed/message/profile-picture/profile-picture.component.scss new file mode 100644 index 0000000..2dda265 --- /dev/null +++ b/src/app/chat/chat/feed/message/profile-picture/profile-picture.component.scss @@ -0,0 +1,7 @@ +img { + display: inline-block; + width: 100%; + + border-radius: 50%; + box-shadow: 0px 0px 5px rgba(0, 0, 0, 1); +} \ No newline at end of file diff --git a/src/app/chat/chat/feed/message/profile-picture/profile-picture.component.ts b/src/app/chat/chat/feed/message/profile-picture/profile-picture.component.ts new file mode 100644 index 0000000..bc35fd2 --- /dev/null +++ b/src/app/chat/chat/feed/message/profile-picture/profile-picture.component.ts @@ -0,0 +1,20 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { UserService } from '../../../../../services/user.service'; + +@Component({ + selector: 'app-profile-picture', + standalone: false, + templateUrl: './profile-picture.component.html', + styleUrl: './profile-picture.component.scss' +}) +export class ProfilePictureComponent implements OnInit { + @Input("username") public username!: string; + public url?: string; + + constructor(private userService: UserService) { } + + ngOnInit(): void { + this.userService.GetProfilePictureURL(this.username) + .subscribe(url => this.url = url); + } +} diff --git a/src/app/common/toolbar/toolbar.component.html b/src/app/common/toolbar/toolbar.component.html new file mode 100644 index 0000000..8c788a4 --- /dev/null +++ b/src/app/common/toolbar/toolbar.component.html @@ -0,0 +1,18 @@ + + + + + chatChat + +
+ +
+ +
+ +
\ No newline at end of file diff --git a/src/app/common/toolbar/toolbar.component.scss b/src/app/common/toolbar/toolbar.component.scss new file mode 100644 index 0000000..b4e2bcb --- /dev/null +++ b/src/app/common/toolbar/toolbar.component.scss @@ -0,0 +1,7 @@ +.toolbar-right-side { + width: 100%; + + button { + float: right; + } +} \ No newline at end of file diff --git a/src/app/common/toolbar/toolbar.component.ts b/src/app/common/toolbar/toolbar.component.ts new file mode 100644 index 0000000..c6cb5c0 --- /dev/null +++ b/src/app/common/toolbar/toolbar.component.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { AuthService } from '../../services/auth.service'; +import { ToastrService } from 'ngx-toastr'; +import { Router } from '@angular/router'; +import { NgIf } from '@angular/common'; + +@Component({ + selector: 'app-toolbar', + imports: [MatToolbarModule, MatIconModule, MatButtonModule, NgIf], + templateUrl: './toolbar.component.html', + styleUrl: './toolbar.component.scss' +}) +export class ToolbarComponent { + constructor( + public authService: AuthService, + private toastrService: ToastrService, + private router: Router) { } + + public Logout(): void { + this.authService.Logout() + .subscribe({ + next: result => { + if (result) { + this.router.navigateByUrl('auth/login'); + this.toastrService.info("Successfully logged out", "Logout"); + } else { + this.toastrService.error("Logout failed", "Error"); + } + } + }); + } +} diff --git a/src/app/models/message.ts b/src/app/models/message.ts new file mode 100644 index 0000000..6644397 --- /dev/null +++ b/src/app/models/message.ts @@ -0,0 +1,9 @@ +import { Timestamp } from "rxjs" + +export class Message { + public id!: number + public sender!: string + public channel!: number + public time!: Date + public content!: string +} \ No newline at end of file diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index 37e094a..efc42aa 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { catchError, map, Observable, of } from 'rxjs'; import { LoginResponse, RegisterResponse } from './responses/auth'; -import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; @Injectable({ providedIn: 'root' @@ -71,5 +71,10 @@ export const IsLoggedInCanActivate: CanActivateFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ) => { - return inject(AuthService).IsLoggedIn(); + if (inject(AuthService).IsLoggedIn()) { + return true; + } else { + inject(Router).navigateByUrl("auth/login"); + return false; + } } \ No newline at end of file diff --git a/src/app/services/chat.service.ts b/src/app/services/chat.service.ts new file mode 100644 index 0000000..0cfab1d --- /dev/null +++ b/src/app/services/chat.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Channel } from '../models/channel'; +import { Message } from '../models/message'; + +@Injectable({ + providedIn: 'root' +}) +export class ChatService { + + constructor() { } + + public ListChannels(): Observable { + return new Observable(subscriber => { + subscriber.next([ + { + id: 0, + name: 'default', + description: 'this is the default channel' + }, + { + id: 1, + name: 'gaming', + description: 'this is another channel' + }, + ]); + subscriber.complete(); + }); + } + + public GetMessages(channelID: number): Observable { + return new Observable(subscriber => { + subscriber.next([ + { + id: 1, + sender: 'Test User 1', + channel: 1, + time: new Date(), + content: 'this is my first message' + }, + { + id: 2, + sender: 'Test User 2', + channel: 2, + time: new Date(), + content: 'this is my second message' + } + ]); + subscriber.complete(); + }); + } +} diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts new file mode 100644 index 0000000..777479d --- /dev/null +++ b/src/app/services/user.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + constructor() { } + + public GetProfilePictureURL(username: string): Observable { + return new Observable(subscriber => { + subscriber.next("https://i.pinimg.com/736x/00/70/16/00701602b0eac0390b3107b9e2a665e0.jpg"); + subscriber.complete(); + }); + } +}