Fixes and adjustments to demo

This commit is contained in:
Toni Koskinen
2026-03-27 10:37:42 +02:00
parent 51a4b84476
commit 4de890ba77
44 changed files with 1076 additions and 104 deletions

View File

@@ -1,4 +1,4 @@
export default {
module.exports = {
displayName: 'api-e2e',
preset: '../../jest.preset.js',
globalSetup: '<rootDir>/src/support/global-setup.ts',

View File

@@ -9,6 +9,9 @@ import { AppModule } from './app/app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: ['http://localhost:4200'],
});
const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
const port = process.env.PORT || 3000;

View File

@@ -1,25 +0,0 @@
import { Controller, Get, NotFoundException, Param } from '@nestjs/common';
import { Post } from '@shared/prisma-generated/src/types';
import { prisma } from '@shared/prisma-generated/src/prisma';
@Controller('posts')
export class PostsController {
@Get()
async findAll(): Promise<Post[]> {
const posts = await prisma.post.findMany();
return posts;
}
@Get(':id')
async findById(@Param('id') id: string): Promise<Post> {
const post = await prisma.post.findUnique({
where: { id },
});
if (!post) {
throw new NotFoundException(`Post with id ${id} not found`);
}
return post;
}
}

View File

@@ -0,0 +1,41 @@
import { Controller, Get, Param, Post, Body, Put, Delete, NotFoundException } from '@nestjs/common';
import { PostsService } from './posts.service';
import { Post as PostEntity } from '@shared/prisma-generated/src/types';
import { PostDto } from '@shared/shared-dto';
import { UpdatePostDto } from '@shared/shared-dto';
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Get()
async findAll(): Promise<PostEntity[]> {
const posts = this.postsService.findAll();
if (!posts) {
throw new NotFoundException("No posts found")
}
return posts
}
@Get(':id')
async findById(@Param('id') id: string): Promise<PostEntity> {
return this.postsService.findById(id);
}
@Post()
async create(@Body() data: PostDto): Promise<PostEntity> {
return this.postsService.create(data);
}
@Put(':id')
async update(@Param('id') id: string, @Body() data: UpdatePostDto): Promise<PostEntity> {
return this.postsService.update(id, data);
}
@Delete(':id')
async delete(@Param('id') id: string): Promise<PostEntity> {
return this.postsService.delete(id);
}
}

View File

@@ -1,7 +1,10 @@
import { Module } from '@nestjs/common';
import { PostsController } from './controllers/posts/posts.controller';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
@Module({
controllers: [PostsController],
providers: [PostsService],
exports: [PostsService],
})
export class PostsModule {}
export class PostsModule {}

View File

@@ -0,0 +1,40 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Post } from '@shared/prisma-generated/src/types';
import { prisma } from '@shared/prisma-generated/src/prisma';
import { PostDto } from '@shared/shared-dto';
import { UpdatePostDto } from '@shared/shared-dto';
@Injectable()
export class PostsService {
async findAll(): Promise<Post[]> {
const posts = await prisma.post.findMany();
return posts;
}
async findById(id: string): Promise<Post> {
const post = await prisma.post.findUnique({
where: { id },
});
if (!post) {
throw new NotFoundException(`Post with id ${id} not found`);
}
return post;
}
async create(data: PostDto): Promise<Post> {
return prisma.post.create({ data });
}
async update(id: string, data: UpdatePostDto): Promise<Post> {
return prisma.post.update({
where: { id },
data,
});
}
async delete(id: string): Promise<Post> {
return prisma.post.delete({ where: { id } });
}
}

View File

@@ -4,7 +4,8 @@ import {
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { appRoutes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideBrowserGlobalErrorListeners(), provideRouter(appRoutes)],
providers: [provideBrowserGlobalErrorListeners(), provideRouter(appRoutes), provideHttpClient()],
};

View File

@@ -1,2 +1 @@
<app-nx-welcome></app-nx-welcome>
<router-outlet></router-outlet>

View File

@@ -1,3 +1,9 @@
import { Route } from '@angular/router';
import { Home } from './features/home/home';
export const appRoutes: Route[] = [];
export const appRoutes: Route[] = [
{
path: '',
component: Home,
},
];

View File

@@ -1,20 +1,31 @@
import { TestBed } from '@angular/core/testing';
import { provideRouter, Router } from '@angular/router';
import { App } from './app';
import { NxWelcome } from './nx-welcome';
import { appRoutes } from './app.routes';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App, NxWelcome],
imports: [App],
providers: [provideRouter(appRoutes)],
}).compileComponents();
});
it('should render title', async () => {
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render Home page on default route', async () => {
const fixture = TestBed.createComponent(App);
const router = TestBed.inject(Router);
await router.navigateByUrl('/');
fixture.detectChanges();
await fixture.whenStable();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain(
'Welcome web-app',
);
expect(compiled.textContent).toContain('Posts');
});
});

View File

@@ -1,15 +1,12 @@
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { NxWelcome } from './nx-welcome';
import { Post } from '@shared/prisma-generated/src/types'
@Component({
imports: [NxWelcome, RouterModule],
imports: [RouterModule],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.scss',
})
export class App {
protected title = 'web-app';
protected posts: Post[] = [];
}

View File

@@ -0,0 +1,32 @@
<section class="home">
<h1>Posts</h1>
@if (isLoading) {
<p>Loading posts...</p>
} @else if (errorMessage) {
<p class="error-message">{{ errorMessage }}</p>
} @else if (posts.length === 0) {
<p class="no-posts-message">No posts found.</p>
} @else {
<div class="cards-container">
@for (post of posts; track post) {
<mat-card class="post-card" appearance="outlined">
<mat-card-header>
<mat-card-title>{{ post.title }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>{{ post.content || 'No content' }}</p>
<p>UserId: {{ post.userId }}</p>
</mat-card-content>
</mat-card>
}
</div>
}
<div class="button-container" data-testid="fetch-button">
<button mat-raised-button color="primary" (click)="fetchPosts()" [disabled]="isLoading">
{{ isLoading ? 'Loading...' : 'Fetch Posts' }}
</button>
</div>
</section>

View File

@@ -0,0 +1,56 @@
.button-container {
margin: 20px 0;
display: flex;
justify-content: center;
gap: 10px;
}
.cards-container {
display: flex;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-top: 20px;
justify-items: center;
width: 100%;
place-items: center;
}
.post-card {
height: 100%;
width: 100%;
max-width: 350px;
display: flex;
flex-direction: column;
text-align: center;
mat-card-header {
padding-bottom: 16px;
}
mat-card-content {
flex: 1;
overflow-y: auto;
max-height: 200px;
}
}
.error-message {
color: #d32f2f;
font-weight: 500;
margin-top: 16px;
}
.no-posts-message {
color: #666;
font-style: italic;
margin-top: 16px;
}
.home {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding: 20px;
}

View File

@@ -0,0 +1,34 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Home } from './home';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
describe('Home', () => {
let component: Home;
let fixture: ComponentFixture<Home>;
let debugElement: DebugElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Home],
}).compileComponents();
fixture = TestBed.createComponent(Home);
component = fixture.componentInstance;
await fixture.whenStable();
debugElement = fixture.debugElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have button', () => {
const fetchButton = debugElement.query(
By.css('[data-testid="fetch-button"]')
);
expect(fetchButton).toBeDefined();
})
});

View File

@@ -0,0 +1,39 @@
import { ChangeDetectorRef, Component, inject } from '@angular/core';
import { PostDto } from '@shared/shared-dto'
import { PostService } from '../../services/PostService/PostService/post-service';
import { timeout } from 'rxjs';
import { MatButtonModule } from '@angular/material/button'
import {MatCardModule} from '@angular/material/card';
@Component({
selector: 'app-home',
imports: [MatButtonModule, MatCardModule],
templateUrl: './home.html',
styleUrl: './home.scss',
})
export class Home {
posts: PostDto[] = [];
isLoading = false;
errorMessage = '';
postService = inject(PostService);
cdr = inject(ChangeDetectorRef);
fetchPosts(): void {
this.isLoading = true;
this.errorMessage = '';
this.postService.getPosts().pipe(timeout(8000)).subscribe({
next: (posts) => {
this.posts = posts;
this.isLoading = false;
this.cdr.detectChanges();
},
error: () => {
this.errorMessage = 'Failed to load posts.';
this.isLoading = false;
this.cdr.detectChanges();
},
});
}
}

View File

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

View File

@@ -0,0 +1,16 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs';
import { PostDto } from '@shared/shared-dto';
@Injectable({
providedIn: 'root',
})
export class PostService {
private httpClient = inject(HttpClient);
private baseUrl = 'http://localhost:3000/api';
getPosts(): Observable<PostDto[]> {
return this.httpClient.get<PostDto[]>(`${this.baseUrl}/posts`);
}
}

View File

@@ -6,7 +6,11 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<app-root></app-root>
</body>

View File

@@ -1 +1,41 @@
// Include theming for Angular Material with `mat.theme()`.
// This Sass mixin will define CSS variables that are used for styling Angular Material
// components according to the Material 3 design spec.
// Learn more about theming and how to use it for your application's
// custom components at https://material.angular.dev/guide/theming
@use '@angular/material' as mat;
html {
height: 100%;
@include mat.theme((
color: (
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
),
typography: Roboto,
density: 0,
));
}
body {
display: flex;
align-items: center;
justify-content: center;
// Default the application to a light color theme. This can be changed to
// `dark` to enable the dark color theme, or to `light dark` to defer to the
// user's system settings.
color-scheme: light;
// Set a default background, font and text colors for the application using
// Angular Material's system-level CSS variables. Learn more about these
// variables at https://material.angular.dev/guide/system-variables
background-color: var(--mat-sys-surface);
color: var(--mat-sys-on-surface);
font: var(--mat-sys-body-medium);
// Reset the user agent margin.
margin: 0;
height: 100%;
}
/* You can add global styles to this file, and also import other style files */