diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index b009cd4..fe855b6 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -16,7 +16,7 @@
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 0148fc8..a32cbff 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -4,10 +4,17 @@ import { PostComponent } from './routes/post/post.component'; import { AdminComponent } from './routes/admin/admin.component'; import { LoggedInGuard } from './shared/guards/logged-in.guard'; import { PostEditorComponent } from './components/post-editor/post-editor.component'; +import { CreatePostComponent } from './routes/post/create-post/create-post.component'; export const routes: Routes = [ { path: '', component: HomeComponent }, - { path: 'post', children: [{ path: ':id', component: PostComponent }] }, + { + path: 'post', + children: [ + { path: 'new', component: CreatePostComponent }, + { path: ':id', component: PostComponent }, + ], + }, { path: 'admin', component: AdminComponent, canActivate: [LoggedInGuard] }, { path: 'tst', component: PostEditorComponent }, ]; diff --git a/frontend/src/app/components/post-editor/post-editor.component.css b/frontend/src/app/components/post-editor/post-editor.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/app/components/post-editor/post-editor.component.html b/frontend/src/app/components/post-editor/post-editor.component.html index a465289..3fd9509 100644 --- a/frontend/src/app/components/post-editor/post-editor.component.html +++ b/frontend/src/app/components/post-editor/post-editor.component.html @@ -3,7 +3,8 @@ @@ -12,15 +13,17 @@

TL;DR; {{ data().tldr }}

diff --git a/frontend/src/app/components/post-editor/post-editor.component.ts b/frontend/src/app/components/post-editor/post-editor.component.ts index 967618a..4a4b1e0 100644 --- a/frontend/src/app/components/post-editor/post-editor.component.ts +++ b/frontend/src/app/components/post-editor/post-editor.component.ts @@ -1,4 +1,11 @@ -import { Component, effect, signal } from '@angular/core'; +import { + Component, + effect, + EventEmitter, + Input, + Output, + signal, +} from '@angular/core'; import { MarkdownComponent } from '../markdown/markdown.component'; import { FormsModule } from '@angular/forms'; import { Post } from '../../shared/interfaces/post'; @@ -8,19 +15,25 @@ import { Post } from '../../shared/interfaces/post'; imports: [MarkdownComponent, FormsModule], standalone: true, templateUrl: './post-editor.component.html', - styleUrl: './post-editor.component.css', }) export class PostEditorComponent { - data = signal({ + @Input('post') data = signal({ id: 0, title: '', tldr: '', content: '', }); + @Output('postChange') dataChange = new EventEmitter(); - constructor() { - effect(() => { - console.log(this.data()); - }); + set title(val: string) { + this.data.update((d) => ({ ...d, title: val })); + } + + set tldr(val: string) { + this.data.update((d) => ({ ...d, tldr: val })); + } + + set content(val: string) { + this.data.update((d) => ({ ...d, content: val })); } } diff --git a/frontend/src/app/routes/admin/admin.component.html b/frontend/src/app/routes/admin/admin.component.html index 95c91bd..3a12edd 100644 --- a/frontend/src/app/routes/admin/admin.component.html +++ b/frontend/src/app/routes/admin/admin.component.html @@ -1 +1,20 @@ - + +
+

Posts Overview

+ + New + +
+ +
+
+

{{ post.title }}

+

TL;DR; {{ post.tldr }}

+
+
diff --git a/frontend/src/app/routes/admin/admin.component.ts b/frontend/src/app/routes/admin/admin.component.ts index 2610029..f326132 100644 --- a/frontend/src/app/routes/admin/admin.component.ts +++ b/frontend/src/app/routes/admin/admin.component.ts @@ -1,10 +1,15 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { PostEditorComponent } from '../../components/post-editor/post-editor.component'; +import { NgFor } from '@angular/common'; +import { PostsService } from '../../shared/services/posts.service'; +import { RouterLink, RouterOutlet } from '@angular/router'; @Component({ selector: 'app-admin', - imports: [PostEditorComponent], + imports: [NgFor, RouterLink], standalone: true, templateUrl: './admin.component.html', }) -export class AdminComponent {} +export class AdminComponent { + posts = inject(PostsService).getPosts(); +} diff --git a/frontend/src/app/routes/home/home.component.ts b/frontend/src/app/routes/home/home.component.ts index 37f2357..01847c1 100644 --- a/frontend/src/app/routes/home/home.component.ts +++ b/frontend/src/app/routes/home/home.component.ts @@ -10,9 +10,5 @@ import { RouterLink } from '@angular/router'; templateUrl: './home.component.html', }) export class HomeComponent { - private postsService = inject(PostsService); - - get posts() { - return this.postsService.getPosts(); - } + posts = inject(PostsService).getPosts(); } diff --git a/frontend/src/app/routes/post/create-post/create-post.component.html b/frontend/src/app/routes/post/create-post/create-post.component.html new file mode 100644 index 0000000..f0ed7d7 --- /dev/null +++ b/frontend/src/app/routes/post/create-post/create-post.component.html @@ -0,0 +1,10 @@ +
+

Create a new Post

+ +
+ diff --git a/frontend/src/app/routes/post/create-post/create-post.component.spec.ts b/frontend/src/app/routes/post/create-post/create-post.component.spec.ts new file mode 100644 index 0000000..f6b9a8c --- /dev/null +++ b/frontend/src/app/routes/post/create-post/create-post.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreatePostComponent } from './create-post.component'; + +describe('CreatePostComponent', () => { + let component: CreatePostComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CreatePostComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CreatePostComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/routes/post/create-post/create-post.component.ts b/frontend/src/app/routes/post/create-post/create-post.component.ts new file mode 100644 index 0000000..b032e08 --- /dev/null +++ b/frontend/src/app/routes/post/create-post/create-post.component.ts @@ -0,0 +1,36 @@ +import { Component, effect, inject, signal } from '@angular/core'; +import { PostEditorComponent } from '../../../components/post-editor/post-editor.component'; +import { Post } from '../../../shared/interfaces/post'; +import { PostsService } from '../../../shared/services/posts.service'; +import { Location } from '@angular/common'; + +@Component({ + selector: 'app-create-post', + imports: [PostEditorComponent], + templateUrl: './create-post.component.html', +}) +export class CreatePostComponent { + private postsService = inject(PostsService); + private location = inject(Location); + post = signal({ + id: 0, + title: '', + tldr: '', + content: '', + }); + + constructor() { + effect(() => { + console.log('create', this.post()); + }); + + setTimeout(() => { + this.post.set({ ...this.post(), title: 'adf' }); + }, 1000); + } + + publish() { + this.postsService.createPost(this.post()); + this.location.back(); + } +} diff --git a/frontend/src/app/routes/post/post.component.ts b/frontend/src/app/routes/post/post.component.ts index d26446f..7a54e8c 100644 --- a/frontend/src/app/routes/post/post.component.ts +++ b/frontend/src/app/routes/post/post.component.ts @@ -10,10 +10,10 @@ import { MarkdownComponent } from '../../components/markdown/markdown.component' templateUrl: './post.component.html', }) export class PostComponent { - private posts = inject(PostsService); @Input() id!: string; + private postsService = inject(PostsService); get post() { - return this.posts.getPost(parseInt(this.id)); + return this.postsService.getPost(parseInt(this.id)); } } diff --git a/frontend/src/app/shared/interfaces/auth.ts b/frontend/src/app/shared/interfaces/auth.ts index 3676e97..58d2c79 100644 --- a/frontend/src/app/shared/interfaces/auth.ts +++ b/frontend/src/app/shared/interfaces/auth.ts @@ -5,3 +5,6 @@ export interface User { export interface LoginResponse { token: string; } +export interface Token { + exp: number; +} diff --git a/frontend/src/app/shared/services/auth.service.ts b/frontend/src/app/shared/services/auth.service.ts index ec30409..a0daa81 100644 --- a/frontend/src/app/shared/services/auth.service.ts +++ b/frontend/src/app/shared/services/auth.service.ts @@ -1,14 +1,64 @@ import { HttpClient } from '@angular/common/http'; -import { inject, Injectable, signal, WritableSignal } from '@angular/core'; -import { LoginResponse, User } from '../interfaces/auth'; +import { + effect, + inject, + Injectable, + signal, + WritableSignal, +} from '@angular/core'; +import { LoginResponse, User, Token } from '../interfaces/auth'; import { environment } from '../../../environments/environment'; +const JWT_KEY = 'token'; + @Injectable({ providedIn: 'root', }) export class AuthService { private http = inject(HttpClient); jwt: WritableSignal = signal(null); + timeout: any | null = null; + + constructor() { + // read from localStorage + const token = window.localStorage.getItem(JWT_KEY); + if (token) { + this.jwt.set(token); + } + + // set localStorage when this.jwt changes + effect(() => { + const token = this.jwt(); + if (token) { + window.localStorage.setItem(JWT_KEY, token); + return; + } + window.localStorage.removeItem(JWT_KEY); + }); + + // if token expired -> set this.jwt to null + effect(() => { + const token = this.jwt(); + if (!token) { + return; + } + + const remainingMilli = + extractExpiration(token).valueOf() - new Date().valueOf(); + + // if expired set to null + if (remainingMilli < 0) { + this.clearTimeout(); + this.jwt.set(null); + return; + } + + this.clearTimeout(); + this.timeout = setTimeout(() => { + this.jwt.set(null); + }, remainingMilli); + }); + } login(user: User) { console.log(user); @@ -17,4 +67,33 @@ export class AuthService { .post(`${environment.apiRoot}/login`, user) .subscribe((res) => this.jwt.set(res.token)); } + + private clearTimeout() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + } +} + +// https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-in-javascript-without-using-a-library +function parseJwt(token: string): Token { + var base64Url = token.split('.')[1]; + var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + var jsonPayload = decodeURIComponent( + window + .atob(base64) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join(''), + ); + + return JSON.parse(jsonPayload); +} + +function extractExpiration(token: string): Date { + const jwt = parseJwt(token); + return new Date(jwt.exp * 1000); } diff --git a/frontend/src/app/shared/services/posts.service.ts b/frontend/src/app/shared/services/posts.service.ts index 4ef2e6e..79e96c4 100644 --- a/frontend/src/app/shared/services/posts.service.ts +++ b/frontend/src/app/shared/services/posts.service.ts @@ -31,10 +31,14 @@ export class PostsService { } getPost(id: number): Signal { - console.log(typeof id); - console.log(this.posts().has(id)); - console.log(this.posts().has(2)); - return computed(() => this.posts().get(id)); } + + createPost(post: Post) { + this.http + .post(`${environment.apiRoot}/posts`, post) + .subscribe((res) => { + this.posts.set(this.posts().set(res.id, res)); + }); + } } diff --git a/frontend/¨:w b/frontend/¨:w new file mode 100644 index 0000000..01847c1 --- /dev/null +++ b/frontend/¨:w @@ -0,0 +1,14 @@ +import { Component, inject, OnInit, Signal } from '@angular/core'; +import { PostsService } from '../../shared/services/posts.service'; +import { NgForOf } from '@angular/common'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-home', + imports: [NgForOf, RouterLink], + standalone: true, + templateUrl: './home.component.html', +}) +export class HomeComponent { + posts = inject(PostsService).getPosts(); +}