basic feed scroll in place
This commit is contained in:
parent
ebe52b3aad
commit
650ae22418
@ -25,7 +25,7 @@ export default class ImageRouter extends AppRouter {
|
||||
}
|
||||
|
||||
@rest('GET', '/')
|
||||
async get(@schema('uuid') id: UUID, @jwt(v => v.salt, false) @auth() jwt?: JWTPayload) {
|
||||
async get(@schema('uuid') id: UUID, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||
const image = await this.db.images.findOne({ _id: new UUID(id) });
|
||||
|
||||
if (
|
||||
@ -36,7 +36,7 @@ export default class ImageRouter extends AppRouter {
|
||||
return ImageRouter.deserialize(image);
|
||||
}
|
||||
@rest('GET', '/img/:id')
|
||||
async file(id: string, @jwt(v => v.salt, false) @auth() jwt?: JWTPayload) {
|
||||
async file(id: string, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||
try {
|
||||
const start = await Deno.realPath("images");
|
||||
const file = await Deno.realPath(`images/${id}`);
|
||||
@ -52,12 +52,23 @@ export default class ImageRouter extends AppRouter {
|
||||
.body((await Deno.open(`images/${id}`)).readable)
|
||||
.contentType(id.split('.')[1]);
|
||||
}
|
||||
@rest('GET', '/feed')
|
||||
async self(@page() page: Page) {
|
||||
@rest('GET')
|
||||
async feed(@page() page: Page) {
|
||||
const res = await page.apply(this.db.images.find({ visibility: Visibility.Public })).sort({ created: -1 }).toArray();
|
||||
if (!res) throw new HttpError('User not found.');
|
||||
return res.map(v => ImageRouter.deserialize(v));
|
||||
}
|
||||
@rest('GET', '/feed/:username')
|
||||
async userFeed(username: string, @page() page: Page, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||
const user = await this.db.users.findOne({ username });
|
||||
if (user === undefined) throw new HttpError("User not found.");
|
||||
|
||||
let cursor;
|
||||
if (user.username === jwt?.name) cursor = this.db.images.find({ _id: { $in: user.images } });
|
||||
else cursor = this.db.images.find({ _id: { $in: user.images }, visibility: Visibility.Public });
|
||||
|
||||
return (await page.apply(cursor.sort({ created: -1 })).toArray()).map(v => ImageRouter.deserialize(v));
|
||||
}
|
||||
|
||||
@rest('POST', '/upload')
|
||||
async upload(@body() body: Blob, @headers() headers: Headers, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
||||
@ -94,7 +105,7 @@ export default class ImageRouter extends AppRouter {
|
||||
// Clean up request
|
||||
const req = await convert(rawReq, { name: 'string?', visibility: 'number?' });
|
||||
req.name ??= new UUID().toString();
|
||||
req.visibility = 0;
|
||||
req.visibility ??= 0;
|
||||
if (req.visibility < 0 || req.visibility > 2) throw new HttpError("body.visibility: Must be 0, 1, or 2");
|
||||
const id = new UUID();
|
||||
// Create file
|
||||
|
@ -31,11 +31,11 @@ export default class UserRouter extends AppRouter {
|
||||
}
|
||||
|
||||
@rest('GET', '/')
|
||||
async get(@schema('string') username: string, @jwt('salt', true) @auth() jwt: JWTPayload) {
|
||||
async get(@schema('string') username: string, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||
const res = await this.db.users.findOne({ username });
|
||||
if (res === undefined) throw new HttpError('User not found.');
|
||||
|
||||
return this.deserialize(res, jwt.name === username);
|
||||
return this.deserialize(res, jwt?.name === username);
|
||||
}
|
||||
@rest('GET', '/self')
|
||||
async self(@jwt('salt', true) @auth() auth: JWTPayload) {
|
||||
|
@ -109,7 +109,6 @@ export default class Router {
|
||||
for await (const req of Deno.serveHttp(conn)) {
|
||||
console.log(req.request.url);
|
||||
const r = await this.handle(RestRequest.fromMessage(req));
|
||||
console.log(req.request.url);
|
||||
if (r) req.respondWith(r.toFetchResponse());
|
||||
}
|
||||
})();
|
||||
|
6
frontend/.vscode/settings.json
vendored
Normal file
6
frontend/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.unstable": true,
|
||||
"deno.enablePaths": [ "backend" ],
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
@ -12,6 +12,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="content" #container (scroll)="onScroll(container)">
|
||||
<router-outlet #o="outlet"></router-outlet>
|
||||
</div>
|
@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { UsersService } from './services/users.service';
|
||||
import { ScrollService } from './services/scroll.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -7,5 +8,12 @@ import { UsersService } from './services/users.service';
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
constructor(public users: UsersService) {}
|
||||
public onScroll(el: HTMLDivElement) {
|
||||
if (el.scrollTop + el.clientHeight * 2 >= el.scrollHeight) this.scroll.signal(el.scrollHeight);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public users: UsersService,
|
||||
public scroll: ScrollService
|
||||
) { }
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { PageSignupComponent } from './page-signup/page-signup.component';
|
||||
import { PageUploadComponent } from './page-upload/page-upload.component';
|
||||
import { PageUserComponent } from './page-user/page-user.component';
|
||||
import { ImagesComponent } from './images/images.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -18,7 +19,8 @@ import { PageUserComponent } from './page-user/page-user.component';
|
||||
PageLoginComponent,
|
||||
PageSignupComponent,
|
||||
PageUploadComponent,
|
||||
PageUserComponent
|
||||
PageUserComponent,
|
||||
ImagesComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
9
frontend/src/app/images/images.component.html
Normal file
9
frontend/src/app/images/images.component.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="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 {{img.author}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 *ngIf="ended">No more photos :(</h3>
|
55
frontend/src/app/images/images.component.scss
Normal file
55
frontend/src/app/images/images.component.scss
Normal file
@ -0,0 +1,55 @@
|
||||
|
||||
.images {
|
||||
display: block;
|
||||
width: min(100%, 85rem);
|
||||
margin: auto;
|
||||
gap: 5rem;
|
||||
align-items: stretch;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5rem;
|
||||
align-items: stretch;
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
42
frontend/src/app/images/images.component.ts
Normal file
42
frontend/src/app/images/images.component.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Image, ImagesService } from '../services/images.service';
|
||||
import { ScrollService } from '../services/scroll.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-images',
|
||||
templateUrl: './images.component.html',
|
||||
styleUrls: ['./images.component.scss']
|
||||
})
|
||||
export class ImagesComponent {
|
||||
@Input() public n = 10;
|
||||
@Input() public feedFunc: (n: number, i: number) => Promise<Image[]> = async () => [];
|
||||
|
||||
public ended = false;
|
||||
public images: Image[] = [];
|
||||
public environment = environment;
|
||||
public i = 0;
|
||||
private sub: () => void;
|
||||
|
||||
public async next() {
|
||||
const res = await this.feedFunc(this.n, this.i);
|
||||
if (res.length === 0) {
|
||||
this.ended = true;
|
||||
this.sub();
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
this.images.push(...res);
|
||||
this.i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
public scroll: ScrollService
|
||||
) {
|
||||
this.sub = scroll.endReached.subscribe(() => {
|
||||
this.next();
|
||||
}).unsubscribe;
|
||||
}
|
||||
}
|
@ -1,10 +1,6 @@
|
||||
<button routerLink="/upload" *ngIf="users.$user()">Upload image</button>
|
||||
<!-- <button routerLink="/projects" *ngIf="users.$user()">Към проектите</button> -->
|
||||
|
||||
<div class="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 {{img.author}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<app-images [feedFunc]="feed.bind(this)" #imgs>
|
||||
|
||||
</app-images>
|
@ -12,59 +12,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.images {
|
||||
margin-top: 5rem;
|
||||
width: 85rem;
|
||||
max-width: 100%;
|
||||
gap: 5rem;
|
||||
align-items: stretch;
|
||||
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 {
|
||||
margin-top: 5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5rem;
|
||||
align-items: stretch;
|
||||
.image {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host {
|
||||
margin-top: 5rem;
|
||||
|
@ -1,21 +1,31 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { AfterViewInit, Component, ViewChild } from '@angular/core';
|
||||
import { UsersService } from '../services/users.service';
|
||||
import { Image, ImagesService } from '../services/images.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ImagesComponent } from '../images/images.component';
|
||||
import { ScrollService } from '../services/scroll.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-home',
|
||||
templateUrl: './page-home.component.html',
|
||||
styleUrls: ['./page-home.component.scss']
|
||||
})
|
||||
export class PageHomeComponent {
|
||||
public images: Image[] = [];
|
||||
public environment = environment;
|
||||
export class PageHomeComponent implements AfterViewInit {
|
||||
@ViewChild("imgs")
|
||||
public imagesEl!: ImagesComponent;
|
||||
|
||||
public feed(n: number, i: number) {
|
||||
return this.images.feed({ n, i });
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
this.imagesEl.next();
|
||||
}
|
||||
|
||||
|
||||
public constructor(
|
||||
images: ImagesService,
|
||||
public users: UsersService,
|
||||
public images: ImagesService,
|
||||
) {
|
||||
images.feed().then(v => this.images = v);
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,8 @@ export class PageUploadComponent {
|
||||
|
||||
public async submit() {
|
||||
await this.images.upload({
|
||||
name: this.form.value.name ?? ''
|
||||
name: this.form.value.name ?? undefined,
|
||||
visibility: this.form.value.visibility ?? undefined,
|
||||
}, this.form.value.file!);
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
|
@ -1 +1,5 @@
|
||||
<p>page-user works!</p>
|
||||
<h3>{{user.username}}</h3>
|
||||
|
||||
<app-images [feedFunc]="feed.bind(this)" #imgs>
|
||||
|
||||
</app-images>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { User, UsersService } from '../services/users.service';
|
||||
import { Image, ImagesService } from '../services/images.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ImagesComponent } from '../images/images.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-user',
|
||||
@ -8,14 +11,22 @@ import { User, UsersService } from '../services/users.service';
|
||||
styleUrls: ['./page-user.component.scss']
|
||||
})
|
||||
export class PageUserComponent {
|
||||
public user: User = { images: [], username: 'loading...' };
|
||||
@ViewChild("imgs")
|
||||
public imagesEl!: ImagesComponent;
|
||||
public user: User = { username: 'loading...' };
|
||||
|
||||
public feed(n: number, i: number) {
|
||||
return this.images.userFeed(this.user.username, { n, i });
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private route: ActivatedRoute,
|
||||
private users: UsersService
|
||||
route: ActivatedRoute,
|
||||
users: UsersService,
|
||||
private images: ImagesService,
|
||||
) {
|
||||
route.paramMap.subscribe(async v => {
|
||||
this.user = await users.get(v.get('name')!);
|
||||
this.imagesEl.next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ export interface CreateImageBody {
|
||||
name?: string;
|
||||
visibility?: Visibility;
|
||||
}
|
||||
export type Page = {} | { n: number; } | { n: number; i: number; };
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -34,11 +35,15 @@ export class ImagesService {
|
||||
params: { id }
|
||||
}));
|
||||
}
|
||||
public async feed(n?: number, i?: number) {
|
||||
return await firstValueFrom(this.http.get<Image[]>(`${this.url}/feed`, {
|
||||
...this.users.httpOptions,
|
||||
// params: { n, i }
|
||||
}));
|
||||
public async feed(page: Page = {}) {
|
||||
return await firstValueFrom(this.http.get<Image[]>(`${this.url}/feed`,
|
||||
this.users.httpOptions({ params: page })
|
||||
));
|
||||
}
|
||||
public async userFeed(username: string, page: Page = {}) {
|
||||
return await firstValueFrom(this.http.get<Image[]>(`${this.url}/feed/${username}`,
|
||||
this.users.httpOptions({ params: page })
|
||||
));
|
||||
}
|
||||
|
||||
public async upload(proto: CreateImageBody, file: File) {
|
||||
|
16
frontend/src/app/services/scroll.service.ts
Normal file
16
frontend/src/app/services/scroll.service.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { EventEmitter, Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ScrollService {
|
||||
public endReached = new EventEmitter();
|
||||
private prevHeight = -1;
|
||||
|
||||
public signal(height: number) {
|
||||
if (this.prevHeight !== height) this.endReached.emit();
|
||||
this.prevHeight = height;
|
||||
}
|
||||
|
||||
constructor() { }
|
||||
}
|
@ -5,7 +5,6 @@ import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
images: string[];
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
Loading…
Reference in New Issue
Block a user