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

View File

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

View File

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

View File

@ -4,9 +4,6 @@ import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
template: "<router-outlet />",
})
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 { ToolbarComponent } from '../common/toolbar/toolbar.component';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
@NgModule({
declarations: [
@ -24,7 +26,9 @@ import { MatBadgeModule } from '@angular/material/badge';
CommonModule,
ChatRoutingModule,
ToolbarComponent,
MatBadgeModule
MatBadgeModule,
MatButtonModule,
MatTooltipModule
]
})
export class ChatModule { }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
: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-left: 2px solid var(--mat-sys-surface-variant);
border-top: 2px solid var(--mat-sys-surface-variant);
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 { UserService } from '../../../services/user.service';
import { Channel } from '../../../models/channel';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-feed',
@ -14,14 +15,20 @@ export class FeedComponent implements OnChanges {
private readonly DEFAULT_CHANNEL_ID: number = 1;
@Input('channel') public channel?: Channel;
public messages!: Message[];
public messages: Message[] = [];
public subscription?: Subscription;
constructor(private chatService: ChatService, private userService: UserService) { }
ngOnChanges() {
this.chatService.GetMessages(this.channel?.id ?? this.DEFAULT_CHANNEL_ID)
.subscribe(messages => {
this.messages = messages;
});
if (this.subscription) {
this.subscription.unsubscribe();
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%;
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 { UserService } from '../../../../../services/user.service';
import { ToastrService } from 'ngx-toastr';
@Component({
selector: 'app-profile-picture',
@ -11,10 +12,15 @@ export class ProfilePictureComponent implements OnInit {
@Input("username") public username!: string;
public url?: string;
constructor(private userService: UserService) { }
constructor(private userService: UserService, private toastrService: ToastrService) { }
ngOnInit(): void {
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 {
width: 100%;

View File

@ -3,13 +3,12 @@ import { inject, Injectable } from '@angular/core';
import { catchError, map, Observable, of } from 'rxjs';
import { LoginResponse, RegisterResponse } from './responses/auth';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router';
import { environment } from '../../environment/environment';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly API_BASE: string = "http://localhost:5000"
private readonly SESSION_COOKIE: string = "session";
private readonly USERNAME_FIELD: string = "username";
private readonly PASSWORD_FIELD: string = "password";
@ -18,7 +17,7 @@ export class AuthService {
constructor(private http: HttpClient) { }
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();
formData.append(this.USERNAME_FIELD, username);
@ -28,7 +27,7 @@ export class AuthService {
}
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();
formData.append(this.USERNAME_FIELD, username);
@ -39,7 +38,7 @@ export class AuthService {
}
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(
map(() => true),
@ -48,7 +47,7 @@ export class AuthService {
}
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(
map(() => true),
@ -68,8 +67,8 @@ export class AuthService {
}
export const IsLoggedInCanActivate: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
_: ActivatedRouteSnapshot,
__: RouterStateSnapshot
) => {
if (inject(AuthService).IsLoggedIn()) {
return true;

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { from, Observable } from 'rxjs';
import { Channel } from '../models/channel';
import { Message } from '../models/message';
@ -21,7 +21,7 @@ export class ChatService {
},
{
id: 1,
name: 'gaming',
name: 'XIV. Leo',
description: 'this is another channel'
},
]);
@ -32,25 +32,142 @@ export class ChatService {
// 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();
});
public GetMessages(channelID: number): Observable<Message> {
return from([
{
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: 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 { Observable } from 'rxjs';
import { catchError, map, Observable, throwError } from 'rxjs';
import { User } from '../models/user';
import { HttpClient } from '@angular/common/http';
import { UserInfoResponse } from './responses/user';
import { environment } from '../../environment/environment';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor() { }
private users: Map<string, User> = new Map<string, User>();
// 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();
});
constructor(private http: HttpClient) { }
public GetUser(username: string): Observable<User> {
if (this.users.has(username)) {
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 GetUser(username: string): Observable<User> {
throw new Error('Not implemented');
public GetProfilePictureURL(username: string): Observable<string> {
if (this.users.has(username)) {
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">
<head>
<meta charset="utf-8">
<title>Ui</title>
<title>Chat</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
html,
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;
}