From d08082278124a0d5eba9be24d915baf088d82372 Mon Sep 17 00:00:00 2001 From: topchetoeu <36534413+TopchetoEU@users.noreply.github.com> Date: Fri, 30 Jun 2023 23:01:08 +0300 Subject: [PATCH] feat: add error popups and likes --- backend/routers/ImageRouter.ts | 12 ++-- backend/server/decorators/page.ts | 2 +- frontend/src/app/app.component.html | 6 ++ frontend/src/app/app.component.ts | 26 ++++++++- frontend/src/app/app.module.ts | 4 +- frontend/src/app/image/image.component.html | 11 +++- frontend/src/app/image/image.component.scss | 40 +++++++++++++ frontend/src/app/image/image.component.ts | 15 ++++- frontend/src/app/images/images.component.html | 5 +- frontend/src/app/images/images.component.scss | 32 ----------- frontend/src/app/images/images.component.ts | 1 - .../src/app/message/message.component.html | 5 ++ .../src/app/message/message.component.scss | 30 ++++++++++ frontend/src/app/message/message.component.ts | 57 +++++++++++++++++++ .../app/page-login/page-login.component.ts | 9 ++- .../app/page-signup/page-signup.component.ts | 15 +++-- .../app/page-upload/page-upload.component.ts | 17 ++++-- .../src/app/page-user/page-user.component.ts | 10 +++- frontend/src/app/services/images.service.ts | 9 +++ frontend/src/app/services/messages.service.ts | 57 +++++++++++++++++++ frontend/src/assets/like-active.svg | 1 + frontend/src/assets/like-inactive.svg | 1 + 22 files changed, 302 insertions(+), 63 deletions(-) create mode 100644 frontend/src/app/message/message.component.html create mode 100644 frontend/src/app/message/message.component.scss create mode 100644 frontend/src/app/message/message.component.ts create mode 100644 frontend/src/app/services/messages.service.ts create mode 100644 frontend/src/assets/like-active.svg create mode 100644 frontend/src/assets/like-inactive.svg diff --git a/backend/routers/ImageRouter.ts b/backend/routers/ImageRouter.ts index 3434062..93e94b4 100644 --- a/backend/routers/ImageRouter.ts +++ b/backend/routers/ImageRouter.ts @@ -5,12 +5,12 @@ import { JWTPayload } from "../utils/JWT.ts"; import AppRouter from "./AppRouter.ts"; import HttpError from "../server/HttpError.ts"; import Image, { Visibility } from "../models/Image.ts"; -import uuid, { Page } from "../server/decorators/page.ts"; import { Headers } from "../server/RestRequest.ts"; import { convert } from "../server/decorators/schema.ts"; import { now } from "../utils/utils.ts"; import AppDatabase from "../AppDatabase.ts"; import RestResponse from "../server/RestResponse.ts"; +import { Page } from "../server/decorators/page.ts"; export default class ImageRouter extends AppRouter { public static deserialize(image: Image, me?: string) { @@ -138,7 +138,7 @@ export default class ImageRouter extends AppRouter { } } @rest('POST', '/change') - async change(@uuid() id: UUID, @body() raw: unknown, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) { + async change(@schema('uuid') id: UUID, @body() raw: unknown, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) { const body = await convert(raw, { name: 'string?', visibility: 'number?' }); const user = await this.db.users.findOne({ username: jwt.name }); if (!user) throw new HttpError("You don't exist."); @@ -152,7 +152,7 @@ export default class ImageRouter extends AppRouter { } @rest('POST', '/like') - async like(@uuid() id: UUID, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) { + async like(@schema('uuid') id: UUID, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) { const user = await this.db.users.findOne({ username: jwt.name }); if (!user) throw new HttpError("You don't exist."); @@ -167,10 +167,10 @@ export default class ImageRouter extends AppRouter { ); - return ImageRouter.deserialize((await this.db.images.findOne({ _id: id }))!); + return ImageRouter.deserialize((await this.db.images.findOne({ _id: id }))!, jwt?.name); } - @rest('POST', '/dislike') - async dislike(@uuid() id: UUID, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) { + @rest('POST', '/unlike') + async unlike(@schema('uuid') id: UUID, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) { const user = await this.db.users.findOne({ username: jwt.name }); if (!user) throw new HttpError("You don't exist."); diff --git a/backend/server/decorators/page.ts b/backend/server/decorators/page.ts index 1418a1b..7aac8d2 100644 --- a/backend/server/decorators/page.ts +++ b/backend/server/decorators/page.ts @@ -24,7 +24,7 @@ export class Page { } } -export default function uuid() { +export default function page() { return makeParameterModifier(req => { let n: number | undefined = Number.parseInt(req.params.n); let i: number | undefined = Number.parseInt(req.params.i); diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index f10f7f5..9d182a3 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -14,4 +14,10 @@
+
+
+
+ + +
\ No newline at end of file diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 9af3a9c..5b57b55 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,6 +1,7 @@ -import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, NgZone, ViewChild } from '@angular/core'; import { UsersService } from './services/users.service'; import { ScrollService } from './services/scroll.service'; +import { Message, MessagesService } from './services/messages.service'; @Component({ selector: 'app-root', @@ -10,12 +11,31 @@ import { ScrollService } from './services/scroll.service'; export class AppComponent implements AfterViewInit { @ViewChild('container') container!: ElementRef; + public messages: Message[] = []; + public removedMessages: Message[] = []; + public ngAfterViewInit(): void { this.scroll.scrollHost = this.container.nativeElement; } + public despawnMsg(id: number) { + this.zone.run(() => { + this.removedMessages = this.removedMessages.filter(v => v.id !== id); + }); + } + constructor( public users: UsersService, - public scroll: ScrollService - ) { } + public scroll: ScrollService, + public zone: NgZone, + public _messages: MessagesService, + ) { + _messages.added.subscribe(v => { + this.messages.push(v); + }); + _messages.removed.subscribe(v => { + this.messages.splice(this.messages.indexOf(v), 1); + this.removedMessages.push(v); + }); + } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6f9d124..a38f4d7 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -12,6 +12,7 @@ import { PageUploadComponent } from './page-upload/page-upload.component'; import { PageUserComponent } from './page-user/page-user.component'; import { ImagesComponent } from './images/images.component'; import { ImageComponent } from './image/image.component'; +import { MessageComponent } from './message/message.component'; @NgModule({ declarations: [ @@ -22,7 +23,8 @@ import { ImageComponent } from './image/image.component'; PageUploadComponent, PageUserComponent, ImagesComponent, - ImageComponent + ImageComponent, + MessageComponent ], imports: [ BrowserModule, diff --git a/frontend/src/app/image/image.component.html b/frontend/src/app/image/image.component.html index f0f6943..b58676a 100644 --- a/frontend/src/app/image/image.component.html +++ b/frontend/src/app/image/image.component.html @@ -1 +1,10 @@ -

image works!

+ +

{{image.name}}

+
+ + By {{image.author}} +
\ No newline at end of file diff --git a/frontend/src/app/image/image.component.scss b/frontend/src/app/image/image.component.scss index e69de29..9d8dada 100644 --- a/frontend/src/app/image/image.component.scss +++ b/frontend/src/app/image/image.component.scss @@ -0,0 +1,40 @@ +:host { + user-select: none; + + .image { + pointer-events: none; + height: 15rem; + display: block; + } + + width: min-content; + border-radius: 1rem; + box-shadow: .25rem .25rem 1rem -.5rem #000; + padding: 1rem; + box-sizing: border-box; + display: inline-flex; + flex-direction: column; + gap: 1rem; + margin: 1rem; + + .name, .author { + text-align: left; + padding: 0; + margin: 0; + } + + .stats { + display: flex; + justify-content: space-between; + + .likes { + // flex: 1 0 auto; + .like { + height: 1.5rem; + } + display: inline-flex; + align-items: center; + gap: .5rem; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/image/image.component.ts b/frontend/src/app/image/image.component.ts index 65a6812..c5de98c 100644 --- a/frontend/src/app/image/image.component.ts +++ b/frontend/src/app/image/image.component.ts @@ -1,4 +1,6 @@ -import { Component } from '@angular/core'; +import { Component, Input } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { Image, ImagesService } from '../services/images.service'; @Component({ selector: 'app-image', @@ -6,5 +8,16 @@ import { Component } from '@angular/core'; styleUrls: ['./image.component.scss'] }) export class ImageComponent { + @Input() + public image?: Image | null; + public environment = environment; + public async like() { + this.image = await this.images.like(this.image!.id); + } + public async unlike() { + this.image = await this.images.unlike(this.image!.id); + } + + public constructor(public images: ImagesService) {} } diff --git a/frontend/src/app/images/images.component.html b/frontend/src/app/images/images.component.html index 01f9fff..997e505 100644 --- a/frontend/src/app/images/images.component.html +++ b/frontend/src/app/images/images.component.html @@ -1,9 +1,10 @@
-
+ +

No more photos :(

\ No newline at end of file diff --git a/frontend/src/app/images/images.component.scss b/frontend/src/app/images/images.component.scss index ca5bbb3..b4d277b 100644 --- a/frontend/src/app/images/images.component.scss +++ b/frontend/src/app/images/images.component.scss @@ -8,38 +8,6 @@ box-sizing: border-box; padding: 0 1rem; text-align: center; - - .image { - user-select: none; - - img { - pointer-events: none; - height: 15rem; - display: block; - } - - width: min-content; - border-radius: 1rem; - box-shadow: .25rem .25rem 1rem -.5rem #000; - padding: 1rem; - box-sizing: border-box; - display: inline-flex; - flex-direction: column; - align-items: center; - gap: 1rem; - margin: 1rem; - - p, h3, h5 { - padding: 0; - margin: 0; - } - h3, h5 { - text-align: center; - } - p { - text-align: justify; - } - } } @media screen and (width < 50rem) { .images { diff --git a/frontend/src/app/images/images.component.ts b/frontend/src/app/images/images.component.ts index 8c44906..196666e 100644 --- a/frontend/src/app/images/images.component.ts +++ b/frontend/src/app/images/images.component.ts @@ -41,7 +41,6 @@ export class ImagesComponent { @Input() public images: Image[] | null = null; public ended = false; - public environment = environment; public i = 0; public constructor() { diff --git a/frontend/src/app/message/message.component.html b/frontend/src/app/message/message.component.html new file mode 100644 index 0000000..c8c62a6 --- /dev/null +++ b/frontend/src/app/message/message.component.html @@ -0,0 +1,5 @@ +
+
+ {{message.content}} +
+
diff --git a/frontend/src/app/message/message.component.scss b/frontend/src/app/message/message.component.scss new file mode 100644 index 0000000..9cb0e32 --- /dev/null +++ b/frontend/src/app/message/message.component.scss @@ -0,0 +1,30 @@ + +.container { + padding: .5rem 1rem; + margin: .5rem; + border-bottom-right-radius: 0; + border-top-right-radius: 0; + border-right-width: 3px; + border-right-style: solid; + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; + width: 20rem; +} + +.error { + border-right-color: red; +} + +.warn { + border-right-color: orange; +} + +.success { + border-right-color: green; +} + +.info { + border-right-color: lightskyblue; +} diff --git a/frontend/src/app/message/message.component.ts b/frontend/src/app/message/message.component.ts new file mode 100644 index 0000000..db84b5f --- /dev/null +++ b/frontend/src/app/message/message.component.ts @@ -0,0 +1,57 @@ +import { Component, ElementRef, EventEmitter, Input, Output } from '@angular/core'; +import { Message } from '../services/messages.service'; + +@Component({ + selector: 'app-message', + templateUrl: './message.component.html', + styleUrls: ['./message.component.scss'] +}) +export class MessageComponent { + private _despawning: boolean = false; + private _currAnimation?: Animation; + private _died: boolean = false; + + public get isDespawning() { + return this._despawning; + } + + public get hasDied() { + return this._died; + } + + @Input('message') + public message!: Message; + @Input('isDespawning') + public set isDespawning(val) { + this._despawning = val; + if (val) { + this._currAnimation = this.ref.nativeElement.animate([ + { + opacity: 1 + }, + { + opacity: 0 + } + ], { + easing: 'ease-out', + duration: 500, + }); + this._currAnimation!.onfinish = () => { + this._died = true; + this.ref.nativeElement.style.display = 'none'; + this.despawned.next(); + }; + } + else { + this._currAnimation?.cancel(); + } + } + + + @Output('despawned') + public despawned = new EventEmitter(); + + public constructor( + private ref: ElementRef + ) { } +} diff --git a/frontend/src/app/page-login/page-login.component.ts b/frontend/src/app/page-login/page-login.component.ts index f5cd9f2..8c68462 100644 --- a/frontend/src/app/page-login/page-login.component.ts +++ b/frontend/src/app/page-login/page-login.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { UsersService } from '../services/users.service'; import { Router } from '@angular/router'; +import { MessagesService } from '../services/messages.service'; @Component({ selector: 'app-page-login', @@ -15,11 +16,15 @@ export class PageLoginComponent { }); public async login() { - await this.users.login(this.form.value.username ?? '', this.form.value.password ?? ''); - this.router.navigateByUrl('/'); + try { + await this.users.login(this.form.value.username ?? '', this.form.value.password ?? ''); + this.router.navigateByUrl('/'); + } + catch (e: any) { this.msgs.error(e); } } public constructor( + private msgs: MessagesService, private users: UsersService, private router: Router ) { } diff --git a/frontend/src/app/page-signup/page-signup.component.ts b/frontend/src/app/page-signup/page-signup.component.ts index a76719a..d13ed9f 100644 --- a/frontend/src/app/page-signup/page-signup.component.ts +++ b/frontend/src/app/page-signup/page-signup.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { UsersService } from '../services/users.service'; import { Router } from '@angular/router'; +import { MessagesService } from '../services/messages.service'; @Component({ selector: 'app-page-signup', @@ -15,14 +16,18 @@ export class PageSignupComponent { }); public async signup() { - await this.users.signup( - this.form.value.username ?? '', - this.form.value.password ?? '' - ); - this.router.navigateByUrl('/'); + try { + await this.users.signup( + this.form.value.username ?? '', + this.form.value.password ?? '' + ); + this.router.navigateByUrl('/'); + } + catch (e: any) { this.msgs.error(e); } } public constructor( + private msgs: MessagesService, private users: UsersService, private router: Router ) { } diff --git a/frontend/src/app/page-upload/page-upload.component.ts b/frontend/src/app/page-upload/page-upload.component.ts index 813ca75..8f16fdd 100644 --- a/frontend/src/app/page-upload/page-upload.component.ts +++ b/frontend/src/app/page-upload/page-upload.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ImagesService, Visibility } from '../services/images.service'; +import { MessagesService } from '../services/messages.service'; @Component({ selector: 'app-page-upload', @@ -20,15 +21,19 @@ export class PageUploadComponent { } public async submit() { - await this.images.upload({ - name: this.form.value.name ?? undefined, - visibility: this.form.value.visibility ?? undefined, - }, this.form.value.file!); - this.router.navigateByUrl('/'); + try { + await this.images.upload({ + name: this.form.value.name ?? undefined, + visibility: this.form.value.visibility ?? undefined, + }, this.form.value.file!); + this.router.navigateByUrl('/'); + } + catch (e: any) { this.msgs.error(e); } } public constructor( private images: ImagesService, - private router: Router + private router: Router, + private msgs: MessagesService, ) {} } diff --git a/frontend/src/app/page-user/page-user.component.ts b/frontend/src/app/page-user/page-user.component.ts index feb4156..1107981 100644 --- a/frontend/src/app/page-user/page-user.component.ts +++ b/frontend/src/app/page-user/page-user.component.ts @@ -5,6 +5,8 @@ import { Image, ImagesService } from '../services/images.service'; import { ImagesComponent } from '../images/images.component'; import { ScrollService } from '../services/scroll.service'; import { Observable } from 'rxjs'; +import { MessagesService } from '../services/messages.service'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-page-user', @@ -26,10 +28,14 @@ export class PageUserComponent { users: UsersService, private _images: ImagesService, private _scroll: ScrollService, + private msgs: MessagesService, ) { route.paramMap.subscribe(async v => { - this.user = await users.get(v.get('name')!); - this.images = ImagesComponent.fromFeed(this._scroll.scrollHost!, 10, this.feed.bind(this)); + try { + this.user = await users.get(v.get('name')!); + this.images = ImagesComponent.fromFeed(this._scroll.scrollHost!, 10, this.feed.bind(this)); + } + catch (e: any) { msgs.error(e); } // this.imagesEl.next(); }); } diff --git a/frontend/src/app/services/images.service.ts b/frontend/src/app/services/images.service.ts index 077a9ef..82b4576 100644 --- a/frontend/src/app/services/images.service.ts +++ b/frontend/src/app/services/images.service.ts @@ -16,6 +16,8 @@ export interface Image { visibility: Visibility; created: number; file: string; + likes: number; + liked?: boolean; } export interface CreateImageBody { name?: string; @@ -56,6 +58,13 @@ export class ImagesService { return await firstValueFrom(this.http.post(`${this.url}/change?id=${id}`, proto, this.users.httpOptions({}))); } + public async like(id: string) { + return await firstValueFrom(this.http.post(`${this.url}/like?id=${id}`, {}, this.users.httpOptions({}))); + } + public async unlike(id: string) { + return await firstValueFrom(this.http.post(`${this.url}/unlike?id=${id}`, {}, this.users.httpOptions({}))); + } + public constructor(private http: HttpClient, private users: UsersService) { } } diff --git a/frontend/src/app/services/messages.service.ts b/frontend/src/app/services/messages.service.ts new file mode 100644 index 0000000..7b64160 --- /dev/null +++ b/frontend/src/app/services/messages.service.ts @@ -0,0 +1,57 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { EventEmitter, Injectable } from '@angular/core'; + +export type Role = 'info' | 'success' | 'warning' | 'error'; + +export interface Message { + content: string; + role: string; + timestamp: number; + id: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class MessagesService { + private _messages: Message[] = []; + + constructor() { } + + public get messages() { + return this._messages; + } + + public added = new EventEmitter(); + public removed = new EventEmitter(); + + public add(message: string, role: Role) { + const id = Math.random(); + const msg = { + content: message, + role: role, + timestamp: Date.now(), + id, + } as Message; + this._messages.push(msg); + + if (msg.role === 'error') console.error('ERROR:', msg.content); + else console.log(msg.role, msg.content); + + const timeout = setTimeout(() => { + this.removed.next(this._messages.find(v => v.id === id)!); + this._messages = this._messages.filter(v => v.id !== id); + clearTimeout(timeout); + }, 2500); + + this.added.next(msg); + } + public error = (message: string | Error) => { + if (message instanceof HttpErrorResponse) this.add(message.error.error, 'error'); + else if (message instanceof Error) this.add(message.message, 'error'); + else this.add(message, 'error'); + } + public warn = (message: string) => this.add(message, 'warning'); + public success = (message: string) => this.add(message, 'success'); + public info = (message: string) => this.add(message, 'info'); +} diff --git a/frontend/src/assets/like-active.svg b/frontend/src/assets/like-active.svg new file mode 100644 index 0000000..4a7e952 --- /dev/null +++ b/frontend/src/assets/like-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/like-inactive.svg b/frontend/src/assets/like-inactive.svg new file mode 100644 index 0000000..df01cb8 --- /dev/null +++ b/frontend/src/assets/like-inactive.svg @@ -0,0 +1 @@ + \ No newline at end of file