Fixes and adjustments to demo
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
displayName: 'api-e2e',
|
||||
preset: '../../jest.preset.js',
|
||||
globalSetup: '<rootDir>/src/support/global-setup.ts',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
41
apps/api/src/modules/users/posts.controller.ts
Normal file
41
apps/api/src/modules/users/posts.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
40
apps/api/src/modules/users/posts.service.ts
Normal file
40
apps/api/src/modules/users/posts.service.ts
Normal 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 } });
|
||||
}
|
||||
}
|
||||
@@ -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()],
|
||||
};
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
<app-nx-welcome></app-nx-welcome>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[] = [];
|
||||
}
|
||||
|
||||
32
apps/web-app/src/app/features/home/home.html
Normal file
32
apps/web-app/src/app/features/home/home.html
Normal 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>
|
||||
56
apps/web-app/src/app/features/home/home.scss
Normal file
56
apps/web-app/src/app/features/home/home.scss
Normal 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;
|
||||
}
|
||||
|
||||
34
apps/web-app/src/app/features/home/home.spec.ts
Normal file
34
apps/web-app/src/app/features/home/home.spec.ts
Normal 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();
|
||||
})
|
||||
});
|
||||
39
apps/web-app/src/app/features/home/home.ts
Normal file
39
apps/web-app/src/app/features/home/home.ts
Normal 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();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user