start frontend
This commit is contained in:
parent
9a63331a95
commit
ebe52b3aad
@ -1,5 +1,5 @@
|
||||
import clonegur from "./clonegur.ts";
|
||||
|
||||
const app = await clonegur();
|
||||
const server = Deno.listen({ port: 4000, hostname: 'localhost' });
|
||||
const server = Deno.listen({ port: 80, hostname: '127.0.0.1' });
|
||||
app.attach(server);
|
||||
|
@ -12,4 +12,5 @@ export default interface Image {
|
||||
name: string;
|
||||
visibility: Visibility;
|
||||
created: number;
|
||||
file: string;
|
||||
}
|
@ -5,7 +5,7 @@ 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 { Page } from "../server/decorators/page.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";
|
||||
@ -20,6 +20,7 @@ export default class ImageRouter extends AppRouter {
|
||||
name: image.name,
|
||||
visibility: image.visibility,
|
||||
id: image._id,
|
||||
file: image.file,
|
||||
};
|
||||
}
|
||||
|
||||
@ -53,7 +54,7 @@ export default class ImageRouter extends AppRouter {
|
||||
}
|
||||
@rest('GET', '/feed')
|
||||
async self(@page() page: Page) {
|
||||
const res = await page.apply(this.db.images.find({})).toArray();
|
||||
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));
|
||||
}
|
||||
@ -95,11 +96,18 @@ export default class ImageRouter extends AppRouter {
|
||||
req.name ??= new UUID().toString();
|
||||
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
|
||||
const img: Image = { _id: new UUID(), author: user.username, created: now(), name: req.name!, visibility: req.visibility };
|
||||
const img: Image = {
|
||||
_id: id,
|
||||
author: user.username,
|
||||
created: now(),
|
||||
name: req.name!,
|
||||
visibility: req.visibility,
|
||||
file: `${id}.${ext}`
|
||||
};
|
||||
await Deno.mkdir('images', { recursive: true });
|
||||
const out = await Deno.open(`images/${img._id}.${ext}`, { write: true, create: true });
|
||||
const out = await Deno.open(`images/${id}.${ext}`, { write: true, create: true });
|
||||
|
||||
for await (const bit of rawFile.stream()) out.write(bit);
|
||||
out.close();
|
||||
@ -107,29 +115,30 @@ export default class ImageRouter extends AppRouter {
|
||||
// Write to DB
|
||||
try {
|
||||
await this.db.images.insertOne(img);
|
||||
await this.db.users.updateOne({ username: user.username }, { $push: { images: img._id } });
|
||||
await this.db.users.updateOne({ username: user.username }, { $push: { images: id } });
|
||||
return ImageRouter.deserialize(img);
|
||||
}
|
||||
catch (e) {
|
||||
await Deno.remove(`images/${img._id}.${ext}`);
|
||||
await Deno.remove(`images/${id}.${ext}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@rest('POST', '/change')
|
||||
async change(@body() raw: unknown, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
||||
const body = await convert(raw, { id: 'uuid', name: 'string?', visibility: 'number?' });
|
||||
async change(@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.");
|
||||
|
||||
const img = await this.db.images.findOne({ _id: body.id });
|
||||
const img = await this.db.images.findOne({ _id: id });
|
||||
if (!img) throw new HttpError("Image doesn't exist.");
|
||||
if (user.username !== img.author) throw new HttpError("You don't own the image.");
|
||||
|
||||
await this.db.images.updateOne({ _id: body.id }, { $set: { name: body.name, visibility: body.visibility } });
|
||||
await this.db.images.updateOne({ _id: id }, { $set: { name: body.name, visibility: body.visibility } });
|
||||
return ImageRouter.deserialize(img);
|
||||
}
|
||||
|
||||
public constructor(private db: AppDatabase) {
|
||||
super();
|
||||
db.images.createIndexes({ indexes: [ { key: { created: -1 }, name: 'Image Order' } ] });
|
||||
}
|
||||
}
|
@ -19,6 +19,10 @@ export class RootRouter extends AppRouter {
|
||||
}
|
||||
@route('/*') static;
|
||||
|
||||
@rest('OPTIONS', '*')
|
||||
options() {
|
||||
return new RestResponse();
|
||||
}
|
||||
@rest('*', '*')
|
||||
default() {
|
||||
return new RestResponse().body(stream('Page not found :/')).status(404);
|
||||
|
@ -65,7 +65,12 @@ export default class Router {
|
||||
if (_req) {
|
||||
try {
|
||||
const res = await hnd.handler.handle(_req);
|
||||
if (res) return res;
|
||||
if (res) {
|
||||
// damn you cors
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
res.header('Access-Control-Allow-Headers', '*')
|
||||
return res;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const res = await this.onError(_req, e);
|
||||
|
@ -6,7 +6,10 @@ import Router, { makeParameterModifier } from "../Router.ts";
|
||||
|
||||
export default function jwt<T extends Router = any>(salt: ((self: T) => string) | string, required = false) {
|
||||
return makeParameterModifier<T>(function (_req, val?: string) {
|
||||
if (val === undefined) return undefined;
|
||||
if (val === undefined) {
|
||||
if (required) throw new HttpError('You are not logged in.');
|
||||
else return undefined;
|
||||
}
|
||||
const s = typeof salt === 'function' ? salt(this) : (this as any)[salt] as string;
|
||||
try {
|
||||
const res = JWT.decode(val, s);
|
||||
|
@ -5,7 +5,7 @@ import RestResponse from "../RestResponse.ts";
|
||||
import Router, { ProcessFunc, addMetaQuery } from "../Router.ts";
|
||||
import serialize from "../serialize.ts";
|
||||
|
||||
export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE';
|
||||
export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE' | 'OPTIONS';
|
||||
|
||||
type Base<KeyT extends string> = Router & { [X in KeyT]: Function; };
|
||||
type ModArray<T extends Router> = ([ProcessFunc<T>, ...ProcessFunc<T>[]] | undefined)[];
|
||||
|
@ -19,7 +19,6 @@ export default async function serialize(val: unknown, depth = 16): Promise<Reada
|
||||
|
||||
if (val === undefined) return undefinedBuff.stream();
|
||||
if (val === null) return nullBuff.stream();
|
||||
if (typeof val === 'string') return stream(val);
|
||||
if (val instanceof Blob) return val.stream();
|
||||
if (val instanceof ReadableStream) return val;
|
||||
if (val instanceof Uint8Array) return new Blob([val]).stream();
|
||||
|
18
frontend/.vscode/launch.json
vendored
Normal file
18
frontend/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Frontend",
|
||||
"type": "msedge",
|
||||
"request": "launch",
|
||||
// "preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
42
frontend/.vscode/tasks.json
vendored
Normal file
42
frontend/.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,10 +1,21 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { PageHomeComponent } from './page-home/page-home.component';
|
||||
import { PageLoginComponent } from './page-login/page-login.component';
|
||||
import { PageSignupComponent } from './page-signup/page-signup.component';
|
||||
import { PageUploadComponent } from './page-upload/page-upload.component';
|
||||
import { PageUserComponent } from './page-user/page-user.component';
|
||||
|
||||
const routes: Routes = [];
|
||||
const routes: Routes = [
|
||||
{ path: '', component: PageHomeComponent },
|
||||
{ path: 'login', component: PageLoginComponent },
|
||||
{ path: 'signup', component: PageSignupComponent },
|
||||
{ path: 'upload', component: PageUploadComponent },
|
||||
{ path: 'user/:name', component: PageUserComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
@ -0,0 +1,17 @@
|
||||
<div class="navbar-container">
|
||||
<div class="navbar">
|
||||
<div>
|
||||
<a class="selectable" routerLink="/">clonegur</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a class="selectable" (click)="users.logoff()" *ngIf="users.$user()">Logoff</a>
|
||||
<a class="selectable" [routerLink]="'/user/' + users.$user()!.username" *ngIf="users.$user()">My Profile</a>
|
||||
<a class="selectable" routerLink="/login" *ngIf="!users.$user()">Log in</a>
|
||||
<a class="selectable" routerLink="/signup" *ngIf="!users.$user()">Sign up</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<router-outlet #o="outlet"></router-outlet>
|
||||
</div>
|
@ -0,0 +1,41 @@
|
||||
:host {
|
||||
display: grid;
|
||||
grid-template-rows: max-content auto;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.navbar-container {
|
||||
z-index: 100;
|
||||
background-color: #fff;
|
||||
box-shadow: #666 0px -10px 10px 10px;
|
||||
|
||||
.navbar {
|
||||
width: 100%;
|
||||
// width: 1000px;
|
||||
justify-content: space-around;
|
||||
display: flex;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
div {
|
||||
a {
|
||||
user-select: none;
|
||||
font-size: 1.2rem;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
padding: .75rem 1.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { UsersService } from './services/users.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -6,5 +7,5 @@ import { Component } from '@angular/core';
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'frontend';
|
||||
constructor(public users: UsersService) {}
|
||||
}
|
||||
|
@ -3,14 +3,28 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { PageHomeComponent } from './page-home/page-home.component';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { PageLoginComponent } from './page-login/page-login.component';
|
||||
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';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
AppComponent,
|
||||
PageHomeComponent,
|
||||
PageLoginComponent,
|
||||
PageSignupComponent,
|
||||
PageUploadComponent,
|
||||
PageUserComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule
|
||||
AppRoutingModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientModule,
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
10
frontend/src/app/page-home/page-home.component.html
Normal file
10
frontend/src/app/page-home/page-home.component.html
Normal file
@ -0,0 +1,10 @@
|
||||
<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>
|
75
frontend/src/app/page-home/page-home.component.scss
Normal file
75
frontend/src/app/page-home/page-home.component.scss
Normal file
@ -0,0 +1,75 @@
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
h2 {
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
21
frontend/src/app/page-home/page-home.component.ts
Normal file
21
frontend/src/app/page-home/page-home.component.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { UsersService } from '../services/users.service';
|
||||
import { Image, ImagesService } from '../services/images.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-home',
|
||||
templateUrl: './page-home.component.html',
|
||||
styleUrls: ['./page-home.component.scss']
|
||||
})
|
||||
export class PageHomeComponent {
|
||||
public images: Image[] = [];
|
||||
public environment = environment;
|
||||
|
||||
public constructor(
|
||||
images: ImagesService,
|
||||
public users: UsersService,
|
||||
) {
|
||||
images.feed().then(v => this.images = v);
|
||||
}
|
||||
}
|
5
frontend/src/app/page-login/page-login.component.html
Normal file
5
frontend/src/app/page-login/page-login.component.html
Normal file
@ -0,0 +1,5 @@
|
||||
<form class="content" [formGroup]="form" (ngSubmit)="login()">
|
||||
<input type="text" formControlName="username" placeholder="Username">
|
||||
<input type="password" formControlName="password" placeholder="Password">
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
22
frontend/src/app/page-login/page-login.component.scss
Normal file
22
frontend/src/app/page-login/page-login.component.scss
Normal file
@ -0,0 +1,22 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
width: 20rem;
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
button {
|
||||
margin: auto
|
||||
}
|
||||
}
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
26
frontend/src/app/page-login/page-login.component.ts
Normal file
26
frontend/src/app/page-login/page-login.component.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { UsersService } from '../services/users.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-login',
|
||||
templateUrl: './page-login.component.html',
|
||||
styleUrls: ['./page-login.component.scss']
|
||||
})
|
||||
export class PageLoginComponent {
|
||||
public form = new FormGroup({
|
||||
username: new FormControl(''),
|
||||
password: new FormControl(''),
|
||||
});
|
||||
|
||||
public async login() {
|
||||
await this.users.login(this.form.value.username ?? '', this.form.value.password ?? '');
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private users: UsersService,
|
||||
private router: Router
|
||||
) { }
|
||||
}
|
5
frontend/src/app/page-signup/page-signup.component.html
Normal file
5
frontend/src/app/page-signup/page-signup.component.html
Normal file
@ -0,0 +1,5 @@
|
||||
<form class="content" [formGroup]="form" (ngSubmit)="signup()">
|
||||
<input type="text" formControlName="username" placeholder="Username">
|
||||
<input type="password" formControlName="password" placeholder="Password">
|
||||
<button type="submit">Sign up</button>
|
||||
</form>
|
22
frontend/src/app/page-signup/page-signup.component.scss
Normal file
22
frontend/src/app/page-signup/page-signup.component.scss
Normal file
@ -0,0 +1,22 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
width: 20rem;
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
button {
|
||||
margin: auto
|
||||
}
|
||||
}
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
29
frontend/src/app/page-signup/page-signup.component.ts
Normal file
29
frontend/src/app/page-signup/page-signup.component.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { UsersService } from '../services/users.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-signup',
|
||||
templateUrl: './page-signup.component.html',
|
||||
styleUrls: ['./page-signup.component.scss']
|
||||
})
|
||||
export class PageSignupComponent {
|
||||
public form = new FormGroup({
|
||||
username: new FormControl<string>(''),
|
||||
password: new FormControl<string>(''),
|
||||
});
|
||||
|
||||
public async signup() {
|
||||
await this.users.signup(
|
||||
this.form.value.username ?? '',
|
||||
this.form.value.password ?? ''
|
||||
);
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private users: UsersService,
|
||||
private router: Router
|
||||
) { }
|
||||
}
|
10
frontend/src/app/page-upload/page-upload.component.html
Normal file
10
frontend/src/app/page-upload/page-upload.component.html
Normal file
@ -0,0 +1,10 @@
|
||||
<form class="content" [formGroup]="form" (ngSubmit)="submit()">
|
||||
<input type="text" placeholder="Име" formControlName="name">
|
||||
<input type="file" (change)="setFile($event)"/>
|
||||
<select formControlName="visibility">
|
||||
<option [value]="0">Public</option>
|
||||
<option [value]="1">Unlisted</option>
|
||||
<option [value]="2">Private</option>
|
||||
</select>
|
||||
<button type="submit">Създай проект</button>
|
||||
</form>
|
28
frontend/src/app/page-upload/page-upload.component.scss
Normal file
28
frontend/src/app/page-upload/page-upload.component.scss
Normal file
@ -0,0 +1,28 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
width: 30rem;
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
textarea {
|
||||
resize: none;
|
||||
height: 10rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
button {
|
||||
margin: auto
|
||||
}
|
||||
}
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
33
frontend/src/app/page-upload/page-upload.component.ts
Normal file
33
frontend/src/app/page-upload/page-upload.component.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ImagesService, Visibility } from '../services/images.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-upload',
|
||||
templateUrl: './page-upload.component.html',
|
||||
styleUrls: ['./page-upload.component.scss']
|
||||
})
|
||||
export class PageUploadComponent {
|
||||
public form = new FormGroup({
|
||||
name: new FormControl<string | null>(null, [ Validators.required ]),
|
||||
file: new FormControl<File | null>(null, [ Validators.required ]),
|
||||
visibility: new FormControl(Visibility.Unlisted, [ Validators.required ]),
|
||||
});
|
||||
|
||||
public setFile($event: any) {
|
||||
this.form.patchValue({ file: $event.target.files[0] });
|
||||
}
|
||||
|
||||
public async submit() {
|
||||
await this.images.upload({
|
||||
name: this.form.value.name ?? ''
|
||||
}, this.form.value.file!);
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private images: ImagesService,
|
||||
private router: Router
|
||||
) {}
|
||||
}
|
1
frontend/src/app/page-user/page-user.component.html
Normal file
1
frontend/src/app/page-user/page-user.component.html
Normal file
@ -0,0 +1 @@
|
||||
<p>page-user works!</p>
|
0
frontend/src/app/page-user/page-user.component.scss
Normal file
0
frontend/src/app/page-user/page-user.component.scss
Normal file
21
frontend/src/app/page-user/page-user.component.ts
Normal file
21
frontend/src/app/page-user/page-user.component.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { User, UsersService } from '../services/users.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-user',
|
||||
templateUrl: './page-user.component.html',
|
||||
styleUrls: ['./page-user.component.scss']
|
||||
})
|
||||
export class PageUserComponent {
|
||||
public user: User = { images: [], username: 'loading...' };
|
||||
|
||||
public constructor(
|
||||
private route: ActivatedRoute,
|
||||
private users: UsersService
|
||||
) {
|
||||
route.paramMap.subscribe(async v => {
|
||||
this.user = await users.get(v.get('name')!);
|
||||
});
|
||||
}
|
||||
}
|
56
frontend/src/app/services/images.service.ts
Normal file
56
frontend/src/app/services/images.service.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
export enum Visibility {
|
||||
Public,
|
||||
Unlisted,
|
||||
Private,
|
||||
}
|
||||
export interface Image {
|
||||
id: string;
|
||||
author: string;
|
||||
name: string;
|
||||
visibility: Visibility;
|
||||
created: number;
|
||||
file: string;
|
||||
}
|
||||
export interface CreateImageBody {
|
||||
name?: string;
|
||||
visibility?: Visibility;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ImagesService {
|
||||
private readonly url = environment.apiURL + '/images';
|
||||
|
||||
public async get(id: string) {
|
||||
return await firstValueFrom(this.http.get<Image>(`${this.url}`, {
|
||||
...this.users.httpOptions,
|
||||
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 upload(proto: CreateImageBody, file: File) {
|
||||
const data = new FormData();
|
||||
data.append('file', file, file.name);
|
||||
data.append('body', JSON.stringify(proto));
|
||||
return await firstValueFrom(this.http.post<string>(`${this.url}/upload`, data, this.users.httpOptions({})));
|
||||
}
|
||||
public async change(id: string, proto: CreateImageBody) {
|
||||
return await firstValueFrom(this.http.post<string>(`${this.url}/change?id=${id}`, proto, this.users.httpOptions({})));
|
||||
}
|
||||
|
||||
public constructor(private http: HttpClient, private users: UsersService) {
|
||||
}
|
||||
}
|
@ -1,24 +1,11 @@
|
||||
import { Injectable, WritableSignal, signal } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { first, firstValueFrom } from 'rxjs';
|
||||
|
||||
export enum Role {
|
||||
Admin = 3,
|
||||
Employer = 2,
|
||||
User = 1,
|
||||
API = 0,
|
||||
Deactivated = -1,
|
||||
}
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
role: Role;
|
||||
projects: string[];
|
||||
}
|
||||
export interface ThisUser extends User {
|
||||
chats: string[];
|
||||
email: string;
|
||||
images: string[];
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
@ -26,7 +13,7 @@ export interface ThisUser extends User {
|
||||
})
|
||||
export class UsersService {
|
||||
private readonly url = environment.apiURL + '/users';
|
||||
public $user = signal<ThisUser | undefined>(undefined);
|
||||
public $user = signal<User | undefined>(undefined);
|
||||
|
||||
public get token() {
|
||||
return localStorage.getItem('token') ?? undefined;
|
||||
@ -36,43 +23,46 @@ export class UsersService {
|
||||
else localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
public httpOptions<T extends object>(other: T): T & { headers?: HttpHeaders } {
|
||||
if (this.token) {
|
||||
(other as any).headers ??= {};
|
||||
(other as any).headers.authorization = 'Bearer ' + this.token;
|
||||
}
|
||||
|
||||
return other;
|
||||
}
|
||||
|
||||
public async updateUser() {
|
||||
if (this.token) {
|
||||
const user = await firstValueFrom(this.http.get<ThisUser>(`${this.url}/get?token=${this.token}`));
|
||||
const user = await firstValueFrom(this.http.get<User>(`${this.url}/self`, {
|
||||
headers: { authorization: 'Bearer ' + this.token }
|
||||
}));
|
||||
this.$user.set(user);
|
||||
}
|
||||
else this.$user.set(undefined);
|
||||
}
|
||||
public async get(username: string) {
|
||||
const user = await firstValueFrom(this.http.get<User>(`${this.url}?username=${username}`, this.httpOptions({})));
|
||||
return user;
|
||||
}
|
||||
|
||||
public async logoff() {
|
||||
if (!this.token) return;
|
||||
await firstValueFrom(this.http.post(this.url + `/logout?token=${this.token}`, {}));
|
||||
this.token = undefined;
|
||||
this.$user.set(undefined);
|
||||
}
|
||||
public async login(username: string, password: string) {
|
||||
await this.logoff();
|
||||
const token = (await firstValueFrom(this.http.post<any>(this.url + `/login`, { username, password }))).token;
|
||||
const token = await firstValueFrom(this.http.post<string>(`${this.url}/login`, { username, password }));
|
||||
this.token = token;
|
||||
await this.updateUser();
|
||||
}
|
||||
public async signup(username: string, password: string, email: string) {
|
||||
public async signup(username: string, password: string) {
|
||||
await this.logoff();
|
||||
await firstValueFrom(this.http.post(this.url + `/signup`, { username, password, email }));
|
||||
await firstValueFrom(this.http.post(`${this.url}/signup`, { username, password }));
|
||||
await this.updateUser();
|
||||
}
|
||||
|
||||
public async requestCode(username: string) {
|
||||
await firstValueFrom(this.http.post<string>(this.url + `/requestCode`, { username }));
|
||||
}
|
||||
public async confirm(username: string, code: string) {
|
||||
await firstValueFrom(this.http.post<string>(this.url + `/confirm`, { username, code }));
|
||||
}
|
||||
|
||||
public async all() {
|
||||
return await firstValueFrom(this.http.get<User[]>(this.url + `/all`));
|
||||
}
|
||||
|
||||
public constructor(private http: HttpClient) {
|
||||
this.updateUser();
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
declare function $__getHTTP(): string;
|
||||
declare function $__getWS(): string;
|
||||
|
||||
export const environment = {
|
||||
production: true,
|
||||
apiURL: $__getHTTP(),
|
||||
wsURL: $__getWS()
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiURL: 'http://127.0.0.1/api',
|
||||
wsURL: 'ws://127.0.0.1/api'
|
||||
};
|
||||
|
@ -1 +1,39 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid #777;
|
||||
background-color: #fff;
|
||||
padding: .5rem 1rem;
|
||||
border-radius: 100rem;
|
||||
transition: 100ms;
|
||||
}
|
||||
input, textarea {
|
||||
font-family: inherit;
|
||||
border: 1px solid #777;
|
||||
background-color: #fff;
|
||||
padding: .5rem 1rem;
|
||||
border-radius: .5rem;
|
||||
transition: 100ms;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button, .hoverable, .selectable {
|
||||
transition: 250ms background-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button, .hoverable, .selectable {
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
button:enabled, .selectable {
|
||||
&:active {
|
||||
background-color: #aaa;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user