Compare commits
3 Commits
9981c634ea
...
cdfa1da90a
Author | SHA1 | Date | |
---|---|---|---|
cdfa1da90a | |||
185f713aeb | |||
ab46e2dd1e |
@ -1,8 +1 @@
|
||||
<mat-toolbar>
|
||||
<mat-toolbar-row>
|
||||
<mat-icon>chat</mat-icon>
|
||||
<span>Chat</span>
|
||||
</mat-toolbar-row>
|
||||
</mat-toolbar>
|
||||
|
||||
<router-outlet />
|
@ -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 {
|
||||
|
||||
}
|
||||
|
@ -13,8 +13,9 @@ export const appConfig: ApplicationConfig = {
|
||||
provideHttpClient(),
|
||||
provideAnimations(),
|
||||
provideToastr({
|
||||
timeOut: 3000,
|
||||
timeOut: 1000,
|
||||
positionClass: 'toast-top-right',
|
||||
closeButton: true
|
||||
}),
|
||||
]
|
||||
};
|
||||
|
@ -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' }
|
||||
];
|
||||
|
@ -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 { }
|
||||
|
@ -1,3 +1,5 @@
|
||||
<app-toolbar></app-toolbar>
|
||||
|
||||
<mat-card class="card">
|
||||
|
||||
<form [formGroup]="loginForm" (ngSubmit)="login()">
|
||||
|
@ -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");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
<app-toolbar></app-toolbar>
|
||||
|
||||
<mat-card class="card">
|
||||
|
||||
<form [formGroup]="registerForm" (ngSubmit)="register()">
|
||||
|
@ -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");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -6,18 +6,25 @@ 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';
|
||||
import { MatBadgeModule } from '@angular/material/badge';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ChatComponent,
|
||||
ChannelListComponent,
|
||||
FeedComponent,
|
||||
ChannelEntryComponent
|
||||
ChannelEntryComponent,
|
||||
MessageComponent,
|
||||
ProfilePictureComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ChatRoutingModule
|
||||
ChatRoutingModule,
|
||||
ToolbarComponent,
|
||||
MatBadgeModule
|
||||
]
|
||||
})
|
||||
export class ChatModule { }
|
||||
|
@ -1 +1,7 @@
|
||||
<div [title]="channel.description" [class.selected]="selected">{{channel.name}}</div>
|
||||
<div [title]="channel.description" [class.selected]="selected">
|
||||
|
||||
<span [matBadge]="this.hasAlert ? '1' : ''" matBadgeSize="small">
|
||||
{{channel.name}}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
@ -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;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Channel } from '../../../../models/channel';
|
||||
|
||||
@Component({
|
||||
@ -7,7 +7,18 @@ import { Channel } from '../../../../models/channel';
|
||||
templateUrl: './channel-entry.component.html',
|
||||
styleUrl: './channel-entry.component.scss'
|
||||
})
|
||||
export class ChannelEntryComponent {
|
||||
export class ChannelEntryComponent implements OnChanges {
|
||||
@Input("channel") public channel!: Channel;
|
||||
@Input("selected") public selected!: boolean;
|
||||
|
||||
public hasAlert: boolean = false;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (this.selected) {
|
||||
this.hasAlert = false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: subsribe to message alerts and display them
|
||||
// unsubscirbe when leaving
|
||||
}
|
||||
|
@ -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"
|
||||
/>
|
@ -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<Channel> = new EventEmitter<Channel>();
|
||||
|
||||
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 {
|
||||
|
@ -1,4 +1,6 @@
|
||||
<app-toolbar />
|
||||
|
||||
<div id="container">
|
||||
<app-channel-list (select)="Select($event)"></app-channel-list>
|
||||
<app-feed></app-feed>
|
||||
<app-feed [channel]="this.selectedChannel"></app-feed>
|
||||
</div>
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
<p>feed works!</p>
|
||||
<app-message
|
||||
*ngFor="let message of this.messages"
|
||||
[message]="message"
|
||||
/>
|
@ -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;
|
||||
}
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
9
src/app/chat/chat/feed/message/message.component.html
Normal file
9
src/app/chat/chat/feed/message/message.component.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="message-container">
|
||||
<app-profile-picture [username]="this.message.sender" />
|
||||
|
||||
<div class="message-inner-container">
|
||||
<div class="message-sender">{{this.message.sender}}</div>
|
||||
<div class="message-content">{{this.message.content}}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
35
src/app/chat/chat/feed/message/message.component.scss
Normal file
35
src/app/chat/chat/feed/message/message.component.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
12
src/app/chat/chat/feed/message/message.component.ts
Normal file
12
src/app/chat/chat/feed/message/message.component.ts
Normal file
@ -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;
|
||||
}
|
@ -0,0 +1 @@
|
||||
<img [src]="this.url" [alt]="this.username">
|
@ -0,0 +1,7 @@
|
||||
img {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 1);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
18
src/app/common/toolbar/toolbar.component.html
Normal file
18
src/app/common/toolbar/toolbar.component.html
Normal file
@ -0,0 +1,18 @@
|
||||
<mat-toolbar>
|
||||
|
||||
<mat-toolbar-row>
|
||||
|
||||
<mat-icon>chat</mat-icon>Chat
|
||||
|
||||
<div class="toolbar-right-side">
|
||||
<button
|
||||
*ngIf="this.authService.IsLoggedIn()"
|
||||
(click)="Logout()"
|
||||
mat-stroked-button>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</mat-toolbar-row>
|
||||
|
||||
</mat-toolbar>
|
7
src/app/common/toolbar/toolbar.component.scss
Normal file
7
src/app/common/toolbar/toolbar.component.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.toolbar-right-side {
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
35
src/app/common/toolbar/toolbar.component.ts
Normal file
35
src/app/common/toolbar/toolbar.component.ts
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
9
src/app/models/message.ts
Normal file
9
src/app/models/message.ts
Normal file
@ -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
|
||||
}
|
6
src/app/models/user.ts
Normal file
6
src/app/models/user.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class User {
|
||||
public username!: string;
|
||||
public status!: string;
|
||||
public picture!: string;
|
||||
public bio!: string;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
56
src/app/services/chat.service.ts
Normal file
56
src/app/services/chat.service.ts
Normal file
@ -0,0 +1,56 @@
|
||||
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() { }
|
||||
|
||||
// TODO: implement
|
||||
public ListChannels(): Observable<Channel[]> {
|
||||
return new Observable<Channel[]>(subscriber => {
|
||||
subscriber.next([
|
||||
{
|
||||
id: 0,
|
||||
name: 'default',
|
||||
description: 'this is the default channel'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'gaming',
|
||||
description: 'this is another channel'
|
||||
},
|
||||
]);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: implement
|
||||
// TODO: refactor this so it first returns the n last messages,
|
||||
// then listens for incoming messages and forwards them as they come
|
||||
public GetMessages(channelID: number): Observable<Message[]> {
|
||||
return new Observable<Message[]>(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();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
export class LoginResponse {
|
||||
public message?: string;
|
||||
public error?: string;
|
||||
public token?: string;
|
||||
import { APIResponse } from "./basic";
|
||||
|
||||
export class LoginResponse extends APIResponse {
|
||||
public session?: string;
|
||||
}
|
||||
|
||||
export class RegisterResponse {
|
||||
public message?: string;
|
||||
public error?: string;
|
||||
}
|
||||
export class RegisterResponse extends APIResponse { }
|
4
src/app/services/responses/basic.ts
Normal file
4
src/app/services/responses/basic.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class APIResponse {
|
||||
public message?: string;
|
||||
public error?: string;
|
||||
}
|
23
src/app/services/user.service.ts
Normal file
23
src/app/services/user.service.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { User } from '../models/user';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserService {
|
||||
constructor() { }
|
||||
|
||||
// TODO: implement
|
||||
public GetProfilePictureURL(username: string): Observable<string> {
|
||||
return new Observable<string>(subscriber => {
|
||||
subscriber.next("https://i.pinimg.com/736x/00/70/16/00701602b0eac0390b3107b9e2a665e0.jpg");
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: implement
|
||||
public GetUser(username: string): Observable<User> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user