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}}
+
\ 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