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

@ -14,8 +14,8 @@ import (
func Login(username, password string, secret []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var login model.Login
if err := json.NewDecoder(r.Response.Request.Body).Decode(&login); err != nil {
login := model.Login{}
if err := json.NewDecoder(r.Body).Decode(&login); err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}

Binary file not shown.

View File

@ -5,7 +5,7 @@ import "net/http"
func HandlerForOrigin(origin string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if origin := r.Header.Get("Origin"); origin != "" {
if o := r.Header.Get("Origin"); o != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
@ -13,6 +13,8 @@ func HandlerForOrigin(origin string) func(http.Handler) http.Handler {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)

View File

@ -28,11 +28,13 @@ func main() {
db := model.Init()
blg := blog.New(db)
r := mux.NewRouter()
r.Use(cors.HandlerForOrigin("*"))
r.Handle("/login", auth.Login(user, password, []byte(secret))).Methods("POST")
r.Handle("/posts", auth.Authenticated([]byte(secret))(blg.CreatePost)).Methods("POST")
r.Handle("/posts", http.HandlerFunc(blg.GetAllPosts)).Methods("GET")
r.Use(cors.HandlerForOrigin("*"))
r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// The CORS middleware should set up the headers for you
w.WriteHeader(http.StatusNoContent)
})
http.ListenAndServe(":8080", r)
}

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({

View File

@ -2,7 +2,7 @@
@import "tailwindcss";
/* apply todo css */
.todo {
app-markdown {
@layer base {
body {
@apply bg-gray-50 text-gray-800 font-sans leading-relaxed text-base;
@ -30,9 +30,11 @@
@apply text-blue-600 hover:text-blue-800 transition-colors underline;
}
ul,
ul {
@apply list-disc pl-6 mb-4;
}
ol {
@apply pl-6 mb-4;
@apply list-decimal pl-6 mb-4;
}
li {
@ -55,5 +57,11 @@
tr:nth-child(even) {
@apply bg-gray-50;
}
blockquote {
@apply italic bg-gray-200 rounded-md backdrop-blur-md p-3;
}
p {
@apply mb-2;
}
}
}