added create page and updated auth
This commit is contained in:
@ -16,7 +16,7 @@
|
|||||||
</app-modal>
|
</app-modal>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class="w-screen h-full max-h-full px-5 sm:px-20 xl:px-44 3xl:px-96 pt-5 overflow-y-scroll"
|
class="w-screen h-full max-h-full px-5 pb-5 sm:px-20 xl:px-44 3xl:px-96 pt-5 overflow-y-scroll"
|
||||||
>
|
>
|
||||||
<router-outlet />
|
<router-outlet />
|
||||||
</main>
|
</main>
|
||||||
|
@ -4,10 +4,17 @@ import { PostComponent } from './routes/post/post.component';
|
|||||||
import { AdminComponent } from './routes/admin/admin.component';
|
import { AdminComponent } from './routes/admin/admin.component';
|
||||||
import { LoggedInGuard } from './shared/guards/logged-in.guard';
|
import { LoggedInGuard } from './shared/guards/logged-in.guard';
|
||||||
import { PostEditorComponent } from './components/post-editor/post-editor.component';
|
import { PostEditorComponent } from './components/post-editor/post-editor.component';
|
||||||
|
import { CreatePostComponent } from './routes/post/create-post/create-post.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: '', component: HomeComponent },
|
{ 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: 'admin', component: AdminComponent, canActivate: [LoggedInGuard] },
|
||||||
{ path: 'tst', component: PostEditorComponent },
|
{ path: 'tst', component: PostEditorComponent },
|
||||||
];
|
];
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
<label>Title:</label>
|
<label>Title:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
[(ngModel)]="data().title"
|
[ngModel]="data().title"
|
||||||
|
(ngModelChange)="title = $event"
|
||||||
class="p-1 border-2 rounded-md col-span-2"
|
class="p-1 border-2 rounded-md col-span-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -12,15 +13,17 @@
|
|||||||
<label>TL;DR;</label>
|
<label>TL;DR;</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
[(ngModel)]="data().tldr"
|
[ngModel]="data().tldr"
|
||||||
|
(ngModelChange)="tldr = $event"
|
||||||
class="p-1 border-2 rounded-md col-span-2"
|
class="p-1 border-2 rounded-md col-span-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="mb-5 italic text-sm">TL;DR; {{ data().tldr }}</p>
|
<p class="mb-5 italic text-sm">TL;DR; {{ data().tldr }}</p>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
[(ngModel)]="data().content"
|
[ngModel]="data().content"
|
||||||
class="border-2 rounded-md"
|
(ngModelChange)="content = $event"
|
||||||
|
class="border-2 rounded-md p-1"
|
||||||
rows="20"
|
rows="20"
|
||||||
></textarea>
|
></textarea>
|
||||||
<app-markdown [markdown]="data().content" />
|
<app-markdown [markdown]="data().content" />
|
||||||
|
@ -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 { MarkdownComponent } from '../markdown/markdown.component';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Post } from '../../shared/interfaces/post';
|
import { Post } from '../../shared/interfaces/post';
|
||||||
@ -8,19 +15,25 @@ import { Post } from '../../shared/interfaces/post';
|
|||||||
imports: [MarkdownComponent, FormsModule],
|
imports: [MarkdownComponent, FormsModule],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './post-editor.component.html',
|
templateUrl: './post-editor.component.html',
|
||||||
styleUrl: './post-editor.component.css',
|
|
||||||
})
|
})
|
||||||
export class PostEditorComponent {
|
export class PostEditorComponent {
|
||||||
data = signal<Post>({
|
@Input('post') data = signal<Post>({
|
||||||
id: 0,
|
id: 0,
|
||||||
title: '',
|
title: '',
|
||||||
tldr: '',
|
tldr: '',
|
||||||
content: '',
|
content: '',
|
||||||
});
|
});
|
||||||
|
@Output('postChange') dataChange = new EventEmitter<Post>();
|
||||||
|
|
||||||
constructor() {
|
set title(val: string) {
|
||||||
effect(() => {
|
this.data.update((d) => ({ ...d, title: val }));
|
||||||
console.log(this.data());
|
}
|
||||||
});
|
|
||||||
|
set tldr(val: string) {
|
||||||
|
this.data.update((d) => ({ ...d, tldr: val }));
|
||||||
|
}
|
||||||
|
|
||||||
|
set content(val: string) {
|
||||||
|
this.data.update((d) => ({ ...d, content: val }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,20 @@
|
|||||||
<app-post-editor />
|
<!-- <app-post-editor /> -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-2xl">Posts Overview</h2>
|
||||||
|
<a
|
||||||
|
routerLink="/post/new"
|
||||||
|
class="bg-amber-400 rounded-full p-1 px-5 hover:bg-amber-500 active:bg-amber-600 transition-colors"
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="grid grid-cols-1 gap-5 m-5">
|
||||||
|
<article
|
||||||
|
*ngFor="let post of posts()"
|
||||||
|
class="p-5 flex flex-col items-start rounded-s bg-white drop-shadow-md hover:drop-shadow-lg"
|
||||||
|
>
|
||||||
|
<h3 class="text-xl">{{ post.title }}</h3>
|
||||||
|
<p><strong>TL;DR; </strong>{{ post.tldr }}</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
@ -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 { 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({
|
@Component({
|
||||||
selector: 'app-admin',
|
selector: 'app-admin',
|
||||||
imports: [PostEditorComponent],
|
imports: [NgFor, RouterLink],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './admin.component.html',
|
templateUrl: './admin.component.html',
|
||||||
})
|
})
|
||||||
export class AdminComponent {}
|
export class AdminComponent {
|
||||||
|
posts = inject(PostsService).getPosts();
|
||||||
|
}
|
||||||
|
@ -10,9 +10,5 @@ import { RouterLink } from '@angular/router';
|
|||||||
templateUrl: './home.component.html',
|
templateUrl: './home.component.html',
|
||||||
})
|
})
|
||||||
export class HomeComponent {
|
export class HomeComponent {
|
||||||
private postsService = inject(PostsService);
|
posts = inject(PostsService).getPosts();
|
||||||
|
|
||||||
get posts() {
|
|
||||||
return this.postsService.getPosts();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<h1 class="text-3xl">Create a new Post</h1>
|
||||||
|
<button
|
||||||
|
class="bg-amber-400 rounded-full p-1 px-5 hover:bg-amber-500 active:bg-amber-600 transition-colors"
|
||||||
|
(click)="publish()"
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<app-post-editor [post]="post" (postChange)="post.set($event)" />
|
@ -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<CreatePostComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CreatePostComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CreatePostComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -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<Post>({
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -10,10 +10,10 @@ import { MarkdownComponent } from '../../components/markdown/markdown.component'
|
|||||||
templateUrl: './post.component.html',
|
templateUrl: './post.component.html',
|
||||||
})
|
})
|
||||||
export class PostComponent {
|
export class PostComponent {
|
||||||
private posts = inject(PostsService);
|
|
||||||
@Input() id!: string;
|
@Input() id!: string;
|
||||||
|
private postsService = inject(PostsService);
|
||||||
|
|
||||||
get post() {
|
get post() {
|
||||||
return this.posts.getPost(parseInt(this.id));
|
return this.postsService.getPost(parseInt(this.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,3 +5,6 @@ export interface User {
|
|||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
export interface Token {
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
@ -1,14 +1,64 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { inject, Injectable, signal, WritableSignal } from '@angular/core';
|
import {
|
||||||
import { LoginResponse, User } from '../interfaces/auth';
|
effect,
|
||||||
|
inject,
|
||||||
|
Injectable,
|
||||||
|
signal,
|
||||||
|
WritableSignal,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { LoginResponse, User, Token } from '../interfaces/auth';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
const JWT_KEY = 'token';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
jwt: WritableSignal<string | null> = signal(null);
|
jwt: WritableSignal<string | null> = 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) {
|
login(user: User) {
|
||||||
console.log(user);
|
console.log(user);
|
||||||
@ -17,4 +67,33 @@ export class AuthService {
|
|||||||
.post<LoginResponse>(`${environment.apiRoot}/login`, user)
|
.post<LoginResponse>(`${environment.apiRoot}/login`, user)
|
||||||
.subscribe((res) => this.jwt.set(res.token));
|
.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);
|
||||||
}
|
}
|
||||||
|
@ -31,10 +31,14 @@ export class PostsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPost(id: number): Signal<Post | undefined> {
|
getPost(id: number): Signal<Post | undefined> {
|
||||||
console.log(typeof id);
|
|
||||||
console.log(this.posts().has(id));
|
|
||||||
console.log(this.posts().has(2));
|
|
||||||
|
|
||||||
return computed(() => this.posts().get(id));
|
return computed(() => this.posts().get(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createPost(post: Post) {
|
||||||
|
this.http
|
||||||
|
.post<Post>(`${environment.apiRoot}/posts`, post)
|
||||||
|
.subscribe((res) => {
|
||||||
|
this.posts.set(this.posts().set(res.id, res));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
14
frontend/¨:w
Normal file
14
frontend/¨:w
Normal file
@ -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();
|
||||||
|
}
|
Reference in New Issue
Block a user