feat: add error popups and likes
This commit is contained in:
parent
ba41d147da
commit
d080822781
@ -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.");
|
||||
|
||||
|
@ -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);
|
||||
|
@ -15,3 +15,9 @@
|
||||
<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>
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
||||
|
@ -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>
|
@ -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 {
|
||||
|
@ -41,7 +41,6 @@ export class ImagesComponent {
|
||||
@Input() public images: Image[] | null = null;
|
||||
|
||||
public ended = false;
|
||||
public environment = environment;
|
||||
public i = 0;
|
||||
|
||||
public constructor() {
|
||||
|
5
frontend/src/app/message/message.component.html
Normal file
5
frontend/src/app/message/message.component.html
Normal file
@ -0,0 +1,5 @@
|
||||
<div class="card" *ngIf="!hasDied" [ngClass]="['container', message.role]">
|
||||
<div class="content">
|
||||
{{message.content}}
|
||||
</div>
|
||||
</div>
|
30
frontend/src/app/message/message.component.scss
Normal file
30
frontend/src/app/message/message.component.scss
Normal 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;
|
||||
}
|
57
frontend/src/app/message/message.component.ts
Normal file
57
frontend/src/app/message/message.component.ts
Normal 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
|
||||
) { }
|
||||
}
|
@ -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
|
||||
) { }
|
||||
|
@ -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
|
||||
) { }
|
||||
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
57
frontend/src/app/services/messages.service.ts
Normal file
57
frontend/src/app/services/messages.service.ts
Normal 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');
|
||||
}
|
1
frontend/src/assets/like-active.svg
Normal file
1
frontend/src/assets/like-active.svg
Normal 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 |
1
frontend/src/assets/like-inactive.svg
Normal file
1
frontend/src/assets/like-inactive.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user