material theming

This commit is contained in:
BENEDEK László 2025-06-05 03:18:56 +02:00
parent cdfa1da90a
commit bdc86eb26c
23 changed files with 283 additions and 86 deletions

View File

@ -32,7 +32,6 @@
} }
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"node_modules/ngx-toastr/toastr.css", "node_modules/ngx-toastr/toastr.css",
"src/styles.scss" "src/styles.scss"
], ],
@ -40,6 +39,12 @@
}, },
"configurations": { "configurations": {
"production": { "production": {
"fileReplacements": [
{
"replace": "src/environment/environment.ts",
"with": "src/environment/environment.prod.ts"
}
],
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
@ -93,7 +98,6 @@
} }
], ],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/cyan-orange.css",
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [] "scripts": []

View File

@ -38,4 +38,4 @@
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2" "typescript": "~5.7.2"
} }
} }

View File

@ -1 +0,0 @@
<router-outlet />

View File

@ -4,9 +4,6 @@ import { RouterOutlet } from '@angular/router';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet], imports: [RouterOutlet],
templateUrl: './app.component.html', template: "<router-outlet />",
styleUrl: './app.component.scss'
}) })
export class AppComponent { export class AppComponent { }
}

View File

@ -10,6 +10,8 @@ import { MessageComponent } from './chat/feed/message/message.component';
import { ProfilePictureComponent } from './chat/feed/message/profile-picture/profile-picture.component'; import { ProfilePictureComponent } from './chat/feed/message/profile-picture/profile-picture.component';
import { ToolbarComponent } from '../common/toolbar/toolbar.component'; import { ToolbarComponent } from '../common/toolbar/toolbar.component';
import { MatBadgeModule } from '@angular/material/badge'; import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -24,7 +26,9 @@ import { MatBadgeModule } from '@angular/material/badge';
CommonModule, CommonModule,
ChatRoutingModule, ChatRoutingModule,
ToolbarComponent, ToolbarComponent,
MatBadgeModule MatBadgeModule,
MatButtonModule,
MatTooltipModule
] ]
}) })
export class ChatModule { } export class ChatModule { }

View File

@ -1,7 +1,11 @@
<div [title]="channel.description" [class.selected]="selected"> <div>
<button
<span [matBadge]="this.hasAlert ? '1' : ''" matBadgeSize="small"> mat-stroked-button
{{channel.name}} [matTooltip]="channel.description"
</span> matTooltipPosition="right"
[matBadge]="this.hasAlert ? '1' : ''"
matBadgeSize="medium"
[disabled]="this.selected">
{{channel.name}}
</button>
</div> </div>

View File

@ -1,10 +1,5 @@
div { button {
padding: 5px 10px; width: 70%;
cursor: pointer; display: block;
} margin: 10px auto;
.selected {
color: var(--mat-sys-on-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;
} }

View File

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

View File

@ -1,5 +1,5 @@
#container { #container {
display: grid; display: grid;
grid-template-columns: minmax(auto, 200px) 1fr; grid-template-columns: minmax(auto, 150px) 1fr;
height: 100%; height: 100%;
} }

View File

@ -1,8 +1,7 @@
:host { :host {
color: var(--mat-sys-on-background); border-left: 2px solid var(--mat-sys-surface-variant);
background-color: var(--mat-sys-background); border-top: 2px solid var(--mat-sys-surface-variant);
border-left: 2px solid var(--mat-sys-secondary);
border-top: 2px solid var(--mat-sys-secondary);
border-radius: 10px 0; border-radius: 10px 0;
background-color: var(--mat-sys-background);
} }

View File

@ -3,6 +3,7 @@ import { Message } from '../../../models/message';
import { ChatService } from '../../../services/chat.service'; import { ChatService } from '../../../services/chat.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { Channel } from '../../../models/channel'; import { Channel } from '../../../models/channel';
import { Subscription } from 'rxjs';
@Component({ @Component({
selector: 'app-feed', selector: 'app-feed',
@ -14,14 +15,20 @@ export class FeedComponent implements OnChanges {
private readonly DEFAULT_CHANNEL_ID: number = 1; private readonly DEFAULT_CHANNEL_ID: number = 1;
@Input('channel') public channel?: Channel; @Input('channel') public channel?: Channel;
public messages!: Message[]; public messages: Message[] = [];
public subscription?: Subscription;
constructor(private chatService: ChatService, private userService: UserService) { } constructor(private chatService: ChatService, private userService: UserService) { }
ngOnChanges() { ngOnChanges() {
this.chatService.GetMessages(this.channel?.id ?? this.DEFAULT_CHANNEL_ID) if (this.subscription) {
.subscribe(messages => { this.subscription.unsubscribe();
this.messages = messages; this.messages = [];
}); }
this.subscription =
this.chatService.GetMessages(this.channel?.id ?? this.DEFAULT_CHANNEL_ID)
.subscribe(message => this.messages.push(message));
} }
} }

View File

@ -3,5 +3,5 @@ img {
width: 100%; width: 100%;
border-radius: 50%; border-radius: 50%;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 1); box-shadow: 0px 0px 5px var(--mat-sys-on-background);
} }

View File

@ -1,5 +1,6 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { UserService } from '../../../../../services/user.service'; import { UserService } from '../../../../../services/user.service';
import { ToastrService } from 'ngx-toastr';
@Component({ @Component({
selector: 'app-profile-picture', selector: 'app-profile-picture',
@ -11,10 +12,15 @@ export class ProfilePictureComponent implements OnInit {
@Input("username") public username!: string; @Input("username") public username!: string;
public url?: string; public url?: string;
constructor(private userService: UserService) { } constructor(private userService: UserService, private toastrService: ToastrService) { }
ngOnInit(): void { ngOnInit(): void {
this.userService.GetProfilePictureURL(this.username) this.userService.GetProfilePictureURL(this.username)
.subscribe(url => this.url = url); .subscribe({
next: url => this.url = url,
error: _ => {
this.toastrService.error("failed to fetch user info", "Error")
}
});
} }
} }

View File

@ -1,3 +1,7 @@
mat-toolbar {
background-color: var(--mat-sys-surface-container);
}
.toolbar-right-side { .toolbar-right-side {
width: 100%; width: 100%;

View File

@ -3,13 +3,12 @@ import { inject, Injectable } from '@angular/core';
import { catchError, map, Observable, of } from 'rxjs'; import { catchError, map, Observable, of } from 'rxjs';
import { LoginResponse, RegisterResponse } from './responses/auth'; import { LoginResponse, RegisterResponse } from './responses/auth';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router';
import { environment } from '../../environment/environment';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AuthService { export class AuthService {
private readonly API_BASE: string = "http://localhost:5000"
private readonly SESSION_COOKIE: string = "session"; private readonly SESSION_COOKIE: string = "session";
private readonly USERNAME_FIELD: string = "username"; private readonly USERNAME_FIELD: string = "username";
private readonly PASSWORD_FIELD: string = "password"; private readonly PASSWORD_FIELD: string = "password";
@ -18,7 +17,7 @@ export class AuthService {
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
public Login(username: string, password: string): Observable<LoginResponse> { public Login(username: string, password: string): Observable<LoginResponse> {
let url = `${this.API_BASE}/auth/login`; let url = `${environment.apiBase}/auth/login`;
let formData = new FormData(); let formData = new FormData();
formData.append(this.USERNAME_FIELD, username); formData.append(this.USERNAME_FIELD, username);
@ -28,7 +27,7 @@ export class AuthService {
} }
public Register(username: string, password: string, repeatPassword: string): Observable<RegisterResponse> { public Register(username: string, password: string, repeatPassword: string): Observable<RegisterResponse> {
let url = `${this.API_BASE}/auth/register`; let url = `${environment.apiBase}/auth/register`;
let formData = new FormData(); let formData = new FormData();
formData.append(this.USERNAME_FIELD, username); formData.append(this.USERNAME_FIELD, username);
@ -39,7 +38,7 @@ export class AuthService {
} }
public Logout(): Observable<boolean> { public Logout(): Observable<boolean> {
let url = `${this.API_BASE}/auth/logout`; let url = `${environment.apiBase}/auth/logout`;
return this.http.get(url, { withCredentials: true }).pipe( return this.http.get(url, { withCredentials: true }).pipe(
map(() => true), map(() => true),
@ -48,7 +47,7 @@ export class AuthService {
} }
public Bump(): Observable<boolean> { public Bump(): Observable<boolean> {
let url = `${this.API_BASE}/auth/bump` let url = `${environment.apiBase}/auth/bump`
return this.http.get(url, { withCredentials: true }).pipe( return this.http.get(url, { withCredentials: true }).pipe(
map(() => true), map(() => true),
@ -68,8 +67,8 @@ export class AuthService {
} }
export const IsLoggedInCanActivate: CanActivateFn = ( export const IsLoggedInCanActivate: CanActivateFn = (
route: ActivatedRouteSnapshot, _: ActivatedRouteSnapshot,
state: RouterStateSnapshot __: RouterStateSnapshot
) => { ) => {
if (inject(AuthService).IsLoggedIn()) { if (inject(AuthService).IsLoggedIn()) {
return true; return true;

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { from, Observable } from 'rxjs';
import { Channel } from '../models/channel'; import { Channel } from '../models/channel';
import { Message } from '../models/message'; import { Message } from '../models/message';
@ -21,7 +21,7 @@ export class ChatService {
}, },
{ {
id: 1, id: 1,
name: 'gaming', name: 'XIV. Leo',
description: 'this is another channel' description: 'this is another channel'
}, },
]); ]);
@ -32,25 +32,142 @@ export class ChatService {
// TODO: implement // TODO: implement
// TODO: refactor this so it first returns the n last messages, // TODO: refactor this so it first returns the n last messages,
// then listens for incoming messages and forwards them as they come // then listens for incoming messages and forwards them as they come
public GetMessages(channelID: number): Observable<Message[]> { public GetMessages(channelID: number): Observable<Message> {
return new Observable<Message[]>(subscriber => {
subscriber.next([ return from([
{ {
id: 1, id: 1,
sender: 'Test User 1', sender: 'admin',
channel: 1, channel: 1,
time: new Date(), time: new Date(),
content: 'this is my first message' content: 'this is my first message'
}, },
{ {
id: 2, id: 1,
sender: 'Test User 2', sender: 'admin',
channel: 2, channel: 1,
time: new Date(), time: new Date(),
content: 'this is my second message' content: 'this is my first message'
} },
]); {
subscriber.complete(); id: 1,
}); sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 2,
sender: 'admin',
channel: 2,
time: new Date(),
content: 'this is my second message'
}
]);
} }
} }

View File

@ -0,0 +1,6 @@
import { User } from "../../models/user";
import { APIResponse } from "./basic";
export class UserInfoResponse extends APIResponse {
public user?: User;
}

View File

@ -1,23 +1,52 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { catchError, map, Observable, throwError } from 'rxjs';
import { User } from '../models/user'; import { User } from '../models/user';
import { HttpClient } from '@angular/common/http';
import { UserInfoResponse } from './responses/user';
import { environment } from '../../environment/environment';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class UserService { export class UserService {
constructor() { } private users: Map<string, User> = new Map<string, User>();
// TODO: implement constructor(private http: HttpClient) { }
public GetProfilePictureURL(username: string): Observable<string> {
return new Observable<string>(subscriber => { public GetUser(username: string): Observable<User> {
subscriber.next("https://i.pinimg.com/736x/00/70/16/00701602b0eac0390b3107b9e2a665e0.jpg"); if (this.users.has(username)) {
subscriber.complete(); return new Observable<User>(subscriber => {
}); subscriber.next(this.users.get(username)!);
subscriber.complete();
});
} else {
let url = `${environment.apiBase}/user/info/${username}`
return this.http.get<UserInfoResponse>(url, { withCredentials: true }).pipe(
map(response => {
if (response.error) {
throw new Error(response.error);
}
if (response.user) {
this.users.set(username, response.user);
return response.user;
}
throw new Error("bad API response, missing user with no error");
}),
catchError(error => throwError(() => new Error(error.error.message)))
);
}
} }
// TODO: implement public GetProfilePictureURL(username: string): Observable<string> {
public GetUser(username: string): Observable<User> { if (this.users.has(username)) {
throw new Error('Not implemented'); return new Observable<string>(subscriber => {
subscriber.next(this.users.get(username)!.picture);
subscriber.complete();
});
} else {
return this.GetUser(username).pipe(map(user => user.picture));
}
} }
} }

View File

@ -0,0 +1,4 @@
export const environment = {
production: true,
apiBase: "https://chat.tek.govt.hu/api"
}

View File

@ -0,0 +1,4 @@
export const environment = {
production: false,
apiBase: "http://localhost:5000"
}

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Ui</title> <title>Chat</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">

View File

@ -1,4 +1,24 @@
/* You can add global styles to this file, and also import other style files */ @use '@angular/material' as mat;
html, body { height: 100%; } html,
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } body {
height: 100%;
margin: 0;
font-family: var(--mat-sys-body-medium-font);
background-color: var(--mat-sys-surface-container);
color-scheme: light dark;
@include mat.theme((
color: mat.$azure-palette,
typography: Roboto,
density: 0));
}
.force-light-mode {
color-scheme: light !important;
}
.force-dark-mode {
color-scheme: dark !important;
}