sidebar, user component

This commit is contained in:
BENEDEK László 2025-06-08 17:30:22 +02:00
parent a393bd91ab
commit f5c20a8d21
20 changed files with 377 additions and 72 deletions

View File

@ -17,6 +17,7 @@ import { MessageBarComponent } from './chat/message-bar/message-bar.component';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { MatSidenavModule } from '@angular/material/sidenav';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -38,7 +39,8 @@ import { ReactiveFormsModule } from '@angular/forms';
MatIconModule, MatIconModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
ReactiveFormsModule ReactiveFormsModule,
MatSidenavModule
] ]
}) })
export class ChatModule { } export class ChatModule { }

View File

@ -1,5 +1,5 @@
button { button {
width: 95%; min-width: 80%;
display: block; display: block;
margin: 5px auto; margin: 10px auto;
} }

View File

@ -1,3 +0,0 @@
:host {
background-color: var(--mat-sys-surface-container);
}

View File

@ -10,12 +10,13 @@ import { ChannelEntryComponent } from './channel-entry/channel-entry.component';
}) })
export class ChannelListComponent { export class ChannelListComponent {
@Input('channels') public channels!: Channel[]; @Input('channels') public channels!: Channel[];
@Output("select") selectEmitter: EventEmitter<number> = new EventEmitter<number>(); @Output("select") public selectEmitter: EventEmitter<number> = new EventEmitter<number>();
@Output('close') public close: EventEmitter<void> = new EventEmitter<void>();
@ViewChildren("entry") entries!: QueryList<ChannelEntryComponent>; @ViewChildren("entry") entries!: QueryList<ChannelEntryComponent>;
private selectedChannel?: number; private selectedChannel?: number;
public get SelectedChannel() : number { public get SelectedChannel(): number {
return this.selectedChannel ?? 0; return this.selectedChannel ?? 0;
} }
@ -26,6 +27,6 @@ export class ChannelListComponent {
} }
public Notify(channel: number): void { public Notify(channel: number): void {
this.entries.find(entry=>entry.channel.id == channel)?.Notify() this.entries.find(entry => entry.channel.id == channel)?.Notify()
} }
} }

View File

@ -1,11 +1,28 @@
<div id="container"> <mat-drawer-container>
<app-toolbar />
<app-channel-list #channelList [channels]="this.GetChannelList()" (select)="Select($event)"></app-channel-list> <mat-drawer #drawer>
<div id="logo">
<h1><mat-icon>chat</mat-icon>Chat</h1>
<app-feed #feed <button mat-button (click)="drawer.close()">
[messages]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].messages : []"></app-feed> <mat-icon>close</mat-icon>Close
</button>
</div>
<app-message-bar <h3>Channels</h3>
[channel]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].channel.id : 1"></app-message-bar>
</div> <app-channel-list #channelList [channels]="this.GetChannelList()" (select)="Select($event)"
(close)="drawer.close()"></app-channel-list>
</mat-drawer>
<mat-drawer-content>
<app-toolbar (sidebar)="drawer.toggle()" />
<app-feed #feed
[messages]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].messages : []"></app-feed>
<app-message-bar
[channel]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].channel.id : 1"></app-message-bar>
</mat-drawer-content>
</mat-drawer-container>

View File

@ -1,35 +1,12 @@
#container { mat-drawer-container {
height: 100%;
}
mat-drawer-content {
height: 100%; height: 100%;
display: grid; display: grid;
grid-template-columns: 150px 1fr; grid-template-columns: 1fr;
grid-template-rows: 65px 1fr auto; grid-template-rows: auto 1fr auto;
gap: 0px; gap: 0px;
app-toolbar {
grid-column: span 2 / span 2;
}
app-channel-list {
grid-row: span 2 / span 2;
grid-row-start: 2;
}
app-feed {
grid-row-start: 2;
border-left: 2px solid var(--mat-sys-surface-variant);
border-top: 2px solid var(--mat-sys-surface-variant);
border-radius: 10px 0 0 0;
background-color: var(--mat-sys-background);
}
app-message-bar {
grid-column-start: 2;
grid-row-start: 3;
border-left: 2px solid var(--mat-sys-surface-variant);
}
} }

View File

@ -5,6 +5,7 @@ import { ChatService } from '../../services/chat.service';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { FeedComponent } from './feed/feed.component'; import { FeedComponent } from './feed/feed.component';
import { ChannelListComponent } from './channel-list/channel-list.component'; import { ChannelListComponent } from './channel-list/channel-list.component';
import { MatDrawer } from '@angular/material/sidenav';
@Component({ @Component({
selector: 'app-chat', selector: 'app-chat',
@ -16,6 +17,7 @@ export class ChatComponent implements OnInit {
public selectedChannel: number = 0; public selectedChannel: number = 0;
public channels: { channel: Channel, messages: Message[] }[] = []; public channels: { channel: Channel, messages: Message[] }[] = [];
@ViewChild("drawer") drawer!: MatDrawer;
@ViewChild("feed") feed!: FeedComponent; @ViewChild("feed") feed!: FeedComponent;
@ViewChild("channelList") channelList!: ChannelListComponent; @ViewChild("channelList") channelList!: ChannelListComponent;
@ -59,5 +61,6 @@ export class ChatComponent implements OnInit {
public Select(channel: number): void { public Select(channel: number): void {
this.selectedChannel = channel; this.selectedChannel = channel;
this.feed.ScrollEventHandler(); this.feed.ScrollEventHandler();
this.drawer.close();
} }
} }

View File

@ -2,7 +2,7 @@
<app-profile-picture [username]="this.message.sender_name" /> <app-profile-picture [username]="this.message.sender_name" />
<div class="message-inner-container"> <div class="message-inner-container">
<div class="message-sender">{{this.message.sender_name}}</div> <div class="message-sender"><a [routerLink]="`/user/${this.message.sender_name}`">{{this.message.sender_name}}</a></div>
<div class="message-content">{{this.message.content}}</div> <div class="message-content">{{this.message.content}}</div>
</div> </div>

View File

@ -33,3 +33,8 @@ app-profile-picture {
} }
} }
} }
a {
text-decoration: none;
color: inherit;
}

View File

@ -1,9 +1,11 @@
:host {
background-color: var(--mat-sys-surface-container);
}
form { form {
align-items: center; align-items: center;
padding: 10px; padding: 10px;
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;

View File

@ -2,17 +2,19 @@
<mat-toolbar-row> <mat-toolbar-row>
<mat-icon>chat</mat-icon>Chat <button id="sidebar-button" *ngIf="this.sidebar.observers.length != 0" mat-button
(click)="this.sidebar.emit()">
<mat-icon>menu</mat-icon>
</button>
<div class="toolbar-right-side"> <div id="logo">
<button <mat-icon>chat</mat-icon>Chat
*ngIf="this.authService.IsLoggedIn()"
(click)="Logout()"
mat-stroked-button>
Logout
</button>
</div> </div>
<button *ngIf="this.authService.IsLoggedIn()" (click)="Logout()" mat-stroked-button>
Logout
</button>
</mat-toolbar-row> </mat-toolbar-row>
</mat-toolbar> </mat-toolbar>

View File

@ -2,10 +2,20 @@ mat-toolbar {
background-color: var(--mat-sys-surface-container); background-color: var(--mat-sys-surface-container);
} }
.toolbar-right-side { #sidebar-button {
width: 100%; mat-icon {
width: fit-content;
button { margin: auto;
float: right; padding: 0;
} }
} }
#logo {
display: block;
margin: auto;
width: fit-content;
}
button {
float: right;
}

View File

@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -14,6 +14,8 @@ import { NgIf } from '@angular/common';
styleUrl: './toolbar.component.scss' styleUrl: './toolbar.component.scss'
}) })
export class ToolbarComponent { export class ToolbarComponent {
@Output('sidebar') sidebar: EventEmitter<void> = new EventEmitter<void>();
constructor( constructor(
public authService: AuthService, public authService: AuthService,
private toastrService: ToastrService, private toastrService: ToastrService,

View File

@ -0,0 +1,15 @@
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[back-button]'
})
export class BackButtonDirective {
constructor() { }
@HostListener('click')
onClick(): void {
window.history.back();
}
}

View File

@ -46,6 +46,7 @@ export class AuthService {
let url = `${environment.apiBase}/auth/logout`; let url = `${environment.apiBase}/auth/logout`;
localStorage.removeItem(this.LOCAL_USERNAME); localStorage.removeItem(this.LOCAL_USERNAME);
this.loggedIn = false;
return this.http.get(url, { withCredentials: true }).pipe( return this.http.get(url, { withCredentials: true }).pipe(
map(() => true), map(() => true),

View File

@ -3,7 +3,16 @@ import { CommonModule } from '@angular/common';
import { UserRoutingModule } from './user-routing.module'; import { UserRoutingModule } from './user-routing.module';
import { UserComponent } from './user/user.component'; import { UserComponent } from './user/user.component';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatCardModule } from '@angular/material/card';
import { ReactiveFormsModule } from '@angular/forms';
import { ToolbarComponent } from "../common/toolbar/toolbar.component";
import { BackButtonDirective } from '../directives/back-button.directive';
import { MatDividerModule } from '@angular/material/divider';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -11,7 +20,17 @@ import { UserComponent } from './user/user.component';
], ],
imports: [ imports: [
CommonModule, CommonModule,
UserRoutingModule UserRoutingModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatButtonModule,
MatSidenavModule,
MatCardModule,
ReactiveFormsModule,
ToolbarComponent,
BackButtonDirective,
MatDividerModule
] ]
}) })
export class UserModule { } export class UserModule { }

View File

@ -1 +1,102 @@
<p>{{username}}</p> <mat-drawer-container>
<mat-drawer #drawer>
<div id="logo">
<h1><mat-icon>chat</mat-icon>Chat</h1>
<button mat-button (click)="drawer.close()">
<mat-icon>close</mat-icon>Close
</button>
</div>
<h3>Options</h3>
<button class="option" routerLink="/chat" mat-button>Chat</button>
<button class="option" back-button mat-button>Back</button>
</mat-drawer>
<mat-drawer-content>
<app-toolbar (sidebar)="drawer.toggle()"></app-toolbar>
<!-- display -->
<mat-card id="display-card" *ngIf="this.username != this.authService.GetUsername()">
<img [src]="this.user?.picture" [alt]="this.username">
<h1>{{'@'}}{{this.user?.username}}</h1>
<h3>Status</h3>
<p>{{this.user?.status}}</p>
<h3>Bio</h3>
<p>{{this.user?.bio}}</p>
</mat-card>
<!-- edit -->
<mat-card id="edit-card" *ngIf="this.username == this.authService.GetUsername()">
<img [src]="this.user?.picture" [alt]="this.username">
<h1>{{'@'}}{{this.user?.username}}</h1>
<form (submit)="this.ChangePassword()" [formGroup]="passwordForm">
<h3>Change password</h3>
<mat-form-field>
<mat-label>New password</mat-label>
<input matInput required formControlName="newPassword">
</mat-form-field>
<mat-form-field>
<mat-label>Repeat new password</mat-label>
<input matInput required formControlName="repeatNewPassword">
</mat-form-field>
<mat-form-field>
<mat-label>Current password</mat-label>
<input matInput required formControlName="password">
</mat-form-field>
<button mat-button>
<mat-icon>key</mat-icon>Change password
</button>
</form>
<mat-divider />
<form (submit)="this.ChangeStatus()" [formGroup]="statusForm">
<h3>Change status</h3>
<mat-form-field>
<mat-label>Status</mat-label>
<textarea matInput required formControlName="status"></textarea>
</mat-form-field>
<button mat-button>
<mat-icon>edit</mat-icon>Change status
</button>
</form>
<mat-divider />
<form (submit)="this.ChangeBio()" [formGroup]="bioForm">
<h3>Change bio</h3>
<mat-form-field>
<mat-label>Bio</mat-label>
<textarea matInput required formControlName="bio"></textarea>
</mat-form-field>
<button mat-button>
<mat-icon>edit</mat-icon>Change bio
</button>
</form>
</mat-card>
</mat-drawer-content>
</mat-drawer-container>

View File

@ -0,0 +1,54 @@
mat-drawer-container {
height: 100%;
mat-drawer-content {
overflow-y: scroll;
}
}
.option {
min-width: 80%;
display: block;
margin: 10px auto;
}
mat-card {
margin: 30px auto;
max-width: 70%;
padding: 20px;
img {
width: 30%;
aspect-ratio: 1;
border-radius: 50%;
border: 2px solid var(--mat-sys-on-surface);
margin: 10px auto;
text-align: center;
}
h1 {
margin: auto;
}
p {
margin: 0 30px;
}
}
#edit-card {
form {
padding: 10px;
margin: 0;
* {
width: 100%;
}
button {
display: block;
margin: 10px auto;
width: fit-content;
}
}
}

View File

@ -1,5 +1,11 @@
import { Component } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { User } from '../../models/user';
import { UserService } from '../../services/user.service';
import { ToastrService } from 'ngx-toastr';
import { Subscription } from 'rxjs';
import { AuthService } from '../../services/auth.service';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
@Component({ @Component({
selector: 'app-user', selector: 'app-user',
@ -7,12 +13,76 @@ import { ActivatedRoute } from '@angular/router';
templateUrl: './user.component.html', templateUrl: './user.component.html',
styleUrl: './user.component.scss' styleUrl: './user.component.scss'
}) })
export class UserComponent { export class UserComponent implements OnInit, OnDestroy {
public username!: string; public username!: string;
public user?: User;
constructor(private route: ActivatedRoute) { private sub?: Subscription;
public passwordForm!: FormGroup;
public statusForm!: FormGroup;
public bioForm!: FormGroup;
constructor(
private route: ActivatedRoute,
private userService: UserService,
public authService: AuthService,
private toastrService: ToastrService,
private formBuilder: FormBuilder) {
this.route.paramMap.subscribe(params => { this.route.paramMap.subscribe(params => {
this.username = params.get('username')!; this.username = params.get('username')!;
}); });
} }
ngOnInit(): void {
this.passwordForm = this.formBuilder.group({
newPassword: new FormControl('', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(32)
]),
repeatNewPassword: new FormControl('', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(32)
]),
password: new FormControl('', [
Validators.required
])
});
this.statusForm = this.formBuilder.group({
status: new FormControl('', [
Validators.required,
Validators.minLength(0),
Validators.maxLength(200)
]),
});
this.bioForm = this.formBuilder.group({
bio: new FormControl('', [
Validators.required,
Validators.minLength(0),
Validators.maxLength(300)
]),
});
this.sub = this.userService.GetUser(this.username)
.subscribe({
next: user => {
this.user = user;
this.statusForm.setValue({ "status": user.status });
this.bioForm.setValue({ "bio": user.bio });
},
error: () => this.toastrService.error("Failed to fectch user info.", "Error"),
});
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
}
public ChangePassword(): void { }
public ChangeStatus(): void { }
public ChangeBio(): void { }
} }

View File

@ -22,3 +22,30 @@ body {
.force-dark-mode { .force-dark-mode {
color-scheme: dark !important; color-scheme: dark !important;
} }
h1,
h2,
h3,
h4 {
margin: 30px 30px 10px;
width: fit-content;
}
#logo {
margin-top: 30px;
h1 {
display: inline;
}
button {
float: right;
margin-right: 10px;
mat-icon {
width: fit-content;
margin: auto;
padding: 0;
}
}
}