started with post editor

This commit is contained in:
u80864958
2025-04-11 15:38:04 +02:00
parent fb2d784421
commit efe902639e
29 changed files with 387 additions and 16 deletions

View File

@ -4,8 +4,19 @@
<a routerLink="/">
<h1 class="text-2xl">My Blog</h1>
</a>
<nav></nav>
<nav class="flex gap-5">
<a routerLink="/admin" *ngIf="loggedIn()">admin</a>
<button *ngIf="!loggedIn()" (click)="toggleLogin()">login</button>
<button *ngIf="loggedIn()" (click)="logOut()">logout</button>
</nav>
</header>
<main class="w-screen h-full px-5 sm:px-20 xl:px-96 pt-5">
<app-modal [open]="loginOpen" (openChange)="loginOpen = $event">
<app-login />
</app-modal>
<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"
>
<router-outlet />
</main>

View File

@ -1,11 +1,39 @@
import { Component } from '@angular/core';
import {
Component,
computed,
effect,
inject,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import { ModalComponent } from './components/modal/modal.component';
import { LoginComponent } from './components/login/login.component';
import { AuthService } from './shared/services/auth.service';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-root',
imports: [RouterOutlet, RouterLink],
imports: [RouterOutlet, RouterLink, ModalComponent, LoginComponent, NgIf],
templateUrl: './app.component.html',
})
export class AppComponent {
title = 'frontend';
private auth = inject(AuthService);
loginOpen: boolean = false;
loggedIn = computed(() => this.auth.jwt() !== null);
constructor() {
effect(() => {
if (this.auth.jwt()) {
this.loginOpen = false;
}
});
}
toggleLogin() {
this.loginOpen = !this.loginOpen;
}
logOut() {
this.auth.jwt.set(null);
}
}

View File

@ -1,8 +1,13 @@
import { Routes } from '@angular/router';
import { HomeComponent } from './routes/home/home.component';
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';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'post', children: [{ path: ':id', component: PostComponent }] },
{ path: 'admin', component: AdminComponent, canActivate: [LoggedInGuard] },
{ path: 'tst', component: PostEditorComponent },
];

View File

@ -0,0 +1,22 @@
<form [formGroup]="form" (ngSubmit)="submit()" class="grid grid-cols-3 gap-5">
<label>Name:</label>
<input
type="text"
formControlName="name"
class="p-1 border-2 rounded-md col-span-2"
/>
<label>Password:</label>
<input
type="password"
formControlName="password"
required
class="p-1 border-2 rounded-md col-span-2"
/>
<button
type="submit"
required
class="bg-blue-300 p-2 rounded-md drop-shadow-md hover:drop-shadow-xl active:bg-blue-600"
>
Login
</button>
</form>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,39 @@
import { Component, inject } from '@angular/core';
import {
FormGroup,
Validators,
ReactiveFormsModule,
FormControl,
} from '@angular/forms';
import { AuthService } from '../../shared/services/auth.service';
import { User } from '../../shared/interfaces/auth';
@Component({
selector: 'app-login',
imports: [ReactiveFormsModule],
templateUrl: './login.component.html',
})
export class LoginComponent {
private auth = inject(AuthService);
form = new FormGroup({
name: new FormControl('', [Validators.required]),
password: new FormControl('', [Validators.required]),
});
get name() {
return this.form.controls.name;
}
get password() {
return this.form.controls.password;
}
submit() {
if (this.form.invalid) {
return;
}
const user: User = {
name: this.form.controls.name.value!,
password: this.form.controls.password.value!,
};
this.auth.login(user);
}
}

View File

@ -0,0 +1,9 @@
<div
*ngIf="open === true"
class="fixed flex top-0 justify-center items-center w-screen h-screen z-50 backdrop-blur-md bg-black/25"
>
<div class="p-10 bg-white drop-shadow-md rounded-md">
<button (click)="toggleOpen()" class="absolute top-0 right-1 p-3">X</button>
<ng-content></ng-content>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModalComponent } from './modal.component';
describe('ModalComponent', () => {
let component: ModalComponent;
let fixture: ComponentFixture<ModalComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ModalComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import { NgIf } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-modal',
imports: [NgIf],
standalone: true,
templateUrl: './modal.component.html',
})
export class ModalComponent {
@Input() open: boolean = false;
@Output() openChange = new EventEmitter<boolean>();
toggleOpen() {
this.open = !this.open;
this.openChange.emit(this.open);
}
}

View File

@ -0,0 +1,27 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
<div class="grid grid-cols-3">
<label>Title:</label>
<input
type="text"
[(ngModel)]="data().title"
class="p-1 border-2 rounded-md col-span-2"
/>
</div>
<p class="text-3xl">{{ data().title }}</p>
<div class="grid grid-cols-3">
<label>TL;DR;</label>
<input
type="text"
[(ngModel)]="data().tldr"
class="p-1 border-2 rounded-md col-span-2"
/>
</div>
<p class="mb-5 italic text-sm">TL;DR; {{ data().tldr }}</p>
<textarea
[(ngModel)]="data().content"
class="border-2 rounded-md"
rows="20"
></textarea>
<app-markdown [markdown]="data().content" />
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PostEditorComponent } from './post-editor.component';
describe('PostEditorComponent', () => {
let component: PostEditorComponent;
let fixture: ComponentFixture<PostEditorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PostEditorComponent]
})
.compileComponents();
fixture = TestBed.createComponent(PostEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { Component, effect, signal } from '@angular/core';
import { MarkdownComponent } from '../markdown/markdown.component';
import { FormsModule } from '@angular/forms';
import { Post } from '../../shared/interfaces/post';
@Component({
selector: 'app-post-editor',
imports: [MarkdownComponent, FormsModule],
standalone: true,
templateUrl: './post-editor.component.html',
styleUrl: './post-editor.component.css',
})
export class PostEditorComponent {
data = signal<Post>({
id: 0,
title: '',
tldr: '',
content: '',
});
constructor() {
effect(() => {
console.log(this.data());
});
}
}

View File

@ -0,0 +1 @@
<app-post-editor />

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminComponent } from './admin.component';
describe('AdminComponent', () => {
let component: AdminComponent;
let fixture: ComponentFixture<AdminComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AdminComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AdminComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { PostEditorComponent } from '../../components/post-editor/post-editor.component';
@Component({
selector: 'app-admin',
imports: [PostEditorComponent],
standalone: true,
templateUrl: './admin.component.html',
})
export class AdminComponent {}

View File

@ -2,7 +2,6 @@ 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';
import { Post } from '../../shared/services/interfaces/post';
@Component({
selector: 'app-home',

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { LoggedInGuard } from './logged-in.guard';
describe('LoggedInGuard', () => {
let guard: LoggedInGuard;
beforeEach(() => {
TestBed.configureTestingModule({});
guard = TestBed.inject(LoggedInGuard);
});
it('should be created', () => {
expect(guard).toBeTruthy();
});
});

View File

@ -0,0 +1,13 @@
import { inject, Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root',
})
export class LoggedInGuard implements CanActivate {
private auth = inject(AuthService);
canActivate(): boolean {
return this.auth.jwt() !== null;
}
}

View File

@ -0,0 +1,7 @@
export interface User {
name: string;
password: string;
}
export interface LoginResponse {
token: string;
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { HttpClient } from '@angular/common/http';
import { inject, Injectable, signal, WritableSignal } from '@angular/core';
import { LoginResponse, User } from '../interfaces/auth';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private http = inject(HttpClient);
jwt: WritableSignal<string | null> = signal(null);
login(user: User) {
console.log(user);
this.http
.post<LoginResponse>(`${environment.apiRoot}/login`, user)
.subscribe((res) => this.jwt.set(res.token));
}
}

View File

@ -7,7 +7,7 @@ import {
signal,
WritableSignal,
} from '@angular/core';
import { Post } from './interfaces/post';
import { Post } from '../interfaces/post';
import { environment } from '../../../environments/environment';
@Injectable({