feat: add error popups and likes

This commit is contained in:
TopchetoEU 2023-06-30 23:01:08 +03:00
parent ba41d147da
commit d080822781
22 changed files with 302 additions and 63 deletions

View File

@ -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.");

View File

@ -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);

View File

@ -14,4 +14,10 @@
</div>
<div class="content" #container>
<router-outlet #o="outlet"></router-outlet>
</div>
<div class="overlay">
<div class="messages-container">
<app-message class="msg" (despawned)="despawnMsg(msg.id)" [isDespawning]="true" [message]="msg" *ngFor="let msg of removedMessages"></app-message>
<app-message class="msg" [message]="msg" *ngFor="let msg of messages"></app-message>
</div>
</div>

View File

@ -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<HTMLDivElement>;
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);
});
}
}

View File

@ -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,

View File

@ -1 +1,10 @@
<p>image works!</p>
<img class="image" [src]="environment.apiURL + '/images/img/' + image.file" *ngIf="image">
<h3 class="name" *ngIf="image">{{image.name}}</h3>
<div class="stats" *ngIf="image">
<span class="likes">
<img class="like" src="assets/like-active.svg" *ngIf="image.liked" (click)="unlike()">
<img class="like" src="assets/like-inactive.svg" *ngIf="!image.liked" (click)="like()">
{{image.likes}}
</span>
<span class="author">By <a [routerLink]="'/user/' + image.author">{{image.author}}</a></span>
</div>

View File

@ -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;
}
}
}

View File

@ -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) {}
}

View File

@ -1,9 +1,10 @@
<div class="images">
<div class="image" *ngFor="let img of images">
<!-- <div class="image" *ngFor="let img of images">
<img [src]="environment.apiURL + '/images/img/' + img.file">
<h3 class="image-name">{{img.name}}</h3>
<h5 class="image-author">By <a [routerLink]="'/user/' + img.author">{{img.author}}</a></h5>
</div>
</div> -->
<app-image [image]="img" *ngFor="let img of images"></app-image>
</div>
<h3 *ngIf="ended">No more photos :(</h3>

View File

@ -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 {

View File

@ -41,7 +41,6 @@ export class ImagesComponent {
@Input() public images: Image[] | null = null;
public ended = false;
public environment = environment;
public i = 0;
public constructor() {

View File

@ -0,0 +1,5 @@
<div class="card" *ngIf="!hasDied" [ngClass]="['container', message.role]">
<div class="content">
{{message.content}}
</div>
</div>

View File

@ -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;
}

View File

@ -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<void>();
public constructor(
private ref: ElementRef
) { }
}

View File

@ -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
) { }

View File

@ -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
) { }

View File

@ -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,
) {}
}

View File

@ -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();
});
}

View File

@ -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<string>(`${this.url}/change?id=${id}`, proto, this.users.httpOptions({})));
}
public async like(id: string) {
return await firstValueFrom(this.http.post<Image>(`${this.url}/like?id=${id}`, {}, this.users.httpOptions({})));
}
public async unlike(id: string) {
return await firstValueFrom(this.http.post<Image>(`${this.url}/unlike?id=${id}`, {}, this.users.httpOptions({})));
}
public constructor(private http: HttpClient, private users: UsersService) {
}
}

View File

@ -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<Message>();
public removed = new EventEmitter<Message>();
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');
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 4.435c-1.989-5.399-12-4.597-12 3.568 0 4.068 3.06 9.481 12 14.997 8.94-5.516 12-10.929 12-14.997 0-8.118-10-8.999-12-3.568z"/></svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 9.229c.234-1.12 1.547-6.229 5.382-6.229 2.22 0 4.618 1.551 4.618 5.003 0 3.907-3.627 8.47-10 12.629-6.373-4.159-10-8.722-10-12.629 0-3.484 2.369-5.005 4.577-5.005 3.923 0 5.145 5.126 5.423 6.231zm-12-1.226c0 4.068 3.06 9.481 12 14.997 8.94-5.516 12-10.929 12-14.997 0-7.962-9.648-9.028-12-3.737-2.338-5.262-12-4.27-12 3.737z"/></svg>

After

Width:  |  Height:  |  Size: 429 B