Commit ecf1ce08 authored by vtduong0912's avatar vtduong0912

update: pagination

parent aba3496f
export namespace Jobs {
export interface Response {
items: Job[];
total: number
}
export interface Job {
id: null | string;
type: string;
created_at: string;
company: string;
company_url: string;
location: string;
title: string;
description: string;
how_to_apply?: string;
company_logo?: null | string;
url?: string;
}
}
\ No newline at end of file
import { HttpClient, HttpParams, HttpRequest } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Jobs } from "../model/job.model";
import { ResponseResult, Rows } from "../../../shared/data-access/interface/response.type";
import { Observable, map } from "rxjs";
@Injectable({
providedIn: 'root'
})
export class JobService {
constructor(
private _http: HttpClient,
) { }
jobsGet(index: number, pageSize: number) {
return this._http
.get<ResponseResult<Rows<Jobs.Job>>>(
'jobs', {
params: {
page: index,
take: pageSize
}
});
}
}
\ No newline at end of file
<div class="tw-max-w-3xl tw-mx-auto">
<div class="tw-flex tw-flex-row tw-flex-wrap tw-mt-6">
<nz-card *ngFor="let job of jobList?.rows" class="tw-w-full tw-my-1 hover:tw-scale-105 tw-transition" [nzTitle]="job.title" [nzExtra]="companyLink">
<ng-template #companyLink>
<a href="{{ job.company_url }}">{{ job.company_url }}</a>
</ng-template>
<div class="tw-flex tw-flex-cols tw-flex-wrap tw-gap-3">
<div class="tw-pointer-events-auto tw-flex">
<nz-avatar class="tw-my-auto" nzSrc="{{ job.company_logo }}"></nz-avatar>
</div>
<div>
<p>Created at: {{ job.created_at | date: 'dd/mm/YYYY'}}</p>
<p>Company name: {{ job.company }}</p>
<p>Location: {{ job.location }}</p>
</div>
<div>
</div>
</div>
</nz-card>
<div class="tw-w-full tw-grid tw-justify-items-center tw-my-3">
<nz-pagination (nzPageIndexChange)="onPageIndexChange($event)" [nzPageIndex]="pageIndex" [nzPageSize]="pageSize" [nzTotal]="jobList?.itemCount || 0 * 10">
</nz-pagination>
</div>
</div>
</div>
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, OnInit, Output } from '@angular/core';
import { CommonModule, DatePipe, JsonPipe, NgFor } from '@angular/common';
import { JobService } from '../data-access/service/job.service'
import { ResponseResult, Rows } from '../../shared/data-access/interface/response.type';
import { Jobs } from '../data-access/model/job.model';
import { catchError, map, of, tap } from 'rxjs';
import { NzCardModule } from 'ng-zorro-antd/card';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
@Component({
selector: 'meu-job-list-page',
standalone: true,
templateUrl: './job.component.html',
styleUrls: ['./job.component.scss'],
imports: [
CommonModule,
NgFor,
JsonPipe,
NzCardModule,
NzAvatarModule,
NzPaginationModule,
DatePipe
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class JobComponent implements OnInit {
jobList: Rows<Jobs.Job> | null = null;
pageIndex: number = 1;
pageSize: number = 5;
@Output() pageIndexChange: EventEmitter<number> = new EventEmitter<number>;
constructor(
private _service: JobService,
private _changeDetectorRef: ChangeDetectorRef,
) {
}
ngOnInit(): void {
this.getAllJobs();
}
onPageIndexChange(index: number) {
this.getAllJobs(index, 5);
}
getAllJobs(index: number = 1, pageSize: number = 5) {
this._service.jobsGet(index, pageSize)
.pipe(
tap((response: ResponseResult<Rows<Jobs.Job>>) => {
this.jobList = response.responseData;
this._changeDetectorRef.markForCheck();
}),
catchError((error: ResponseResult<any>) => {
return of(null);
})
)
.subscribe();
}
}
import { Route } from '@angular/router';
const JOB_ROUTES: Route[] = [
{
path: '',
loadComponent: () =>
import('./feature/job.component').then((m) => m.JobComponent),
},
];
export default JOB_ROUTES;
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router'; import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../service/auth.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AdminGuard implements CanActivate { export class AdminGuard implements CanActivate {
constructor() {} constructor(private _service: AuthService, private _router: Router) {}
canActivate(): boolean { canActivate(): boolean {
const isLoggedIn = this._service.isLoggedIn();
const isAdmin = this._service.isAdmin();
if (!isLoggedIn || !isAdmin) {
this._router.navigate(['/']);
return false;
}
return true; return true;
// return false if user role is not admin
// return false;
} }
} }
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router'; import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../service/auth.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor() {} constructor(private _service: AuthService, private _router: Router) {}
canActivate(): boolean { canActivate(): boolean {
const isLoggedIn = this._service.isLoggedIn();
const isAdmin = this._service.isAdmin();
if (!isLoggedIn || !isAdmin) {
this._router.navigate(['/']);
return false;
}
return true; return true;
// return false if logged in
// return false;
} }
} }
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../service/auth.service';
@Injectable({
providedIn: 'root',
})
export class LoginGuard implements CanActivate {
constructor(private _service: AuthService, private _router: Router) {}
canActivate(): boolean {
return !this._service.isLoggedIn();
}
}
...@@ -15,10 +15,14 @@ export class AuthService { ...@@ -15,10 +15,14 @@ export class AuthService {
loginPost(request: Login.Request) { loginPost(request: Login.Request) {
return this._http return this._http
.post<ResponseResult<Login.Response>>('login', request) .post<ResponseResult<Login.Response>>('login', request)
.pipe( }
map((response) => {
console.log(response); isLoggedIn() {
return response; return !!localStorage.getItem("token")
})); }
isAdmin() {
const role = localStorage.getItem("role");
return role === 'admin';
} }
} }
\ No newline at end of file
...@@ -11,7 +11,7 @@ import { NzAlertModule } from 'ng-zorro-antd/alert'; ...@@ -11,7 +11,7 @@ import { NzAlertModule } from 'ng-zorro-antd/alert';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { AuthService } from '../data-access/service/auth.service'; import { AuthService } from '../data-access/service/auth.service';
import { validateUsername } from '../../shared/utils/form-validator/username.validator'; import { validateUsername } from '../../shared/utils/form-validator/username.validator';
import { catchError, of, tap } from 'rxjs'; import { catchError, map, of, tap } from 'rxjs';
import { ResponseResult } from '../../shared/data-access/interface/response.type'; import { ResponseResult } from '../../shared/data-access/interface/response.type';
import { Login } from '../data-access/model/login.model'; import { Login } from '../data-access/model/login.model';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
...@@ -38,7 +38,7 @@ import { Router } from '@angular/router'; ...@@ -38,7 +38,7 @@ import { Router } from '@angular/router';
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
passwordVisible: boolean = false; passwordVisible: boolean = false;
errorMsg: string = ""; errorMsg: string = "";
signInFormGroup!: FormGroup; signInFormGroup: FormGroup;
// isEdit: boolean = false; // isEdit: boolean = false;
constructor( constructor(
private _fb: FormBuilder, private _fb: FormBuilder,
...@@ -62,7 +62,7 @@ export class LoginComponent implements OnInit { ...@@ -62,7 +62,7 @@ export class LoginComponent implements OnInit {
Object.values(this.signInFormGroup.controls).forEach(control => { Object.values(this.signInFormGroup.controls).forEach(control => {
if (control.invalid) { if (control.invalid) {
control.markAsDirty(); control.markAsDirty();
control.updateValueAndValidity({ onlySelf: true }); control.updateValueAndValidity({onlySelf: true});
} }
}); });
return; return;
...@@ -82,13 +82,12 @@ export class LoginComponent implements OnInit { ...@@ -82,13 +82,12 @@ export class LoginComponent implements OnInit {
} }
onSuccess(response: ResponseResult<Login.Response>) { onSuccess(response: ResponseResult<Login.Response>) {
localStorage.setItem("token", JSON.stringify(response.responseData?.token) ?? ""); localStorage.setItem("token", response.responseData?.token ?? "");
localStorage.setItem("role", JSON.stringify(response.responseData?.role) ?? ""); localStorage.setItem("role", response.responseData?.role ?? "");
this._router.navigate(['/home']); this._router.navigate(['/home']);
} }
onError(error: ResponseResult<any>) { onError(error: ResponseResult<any>) {
this.errorMsg = "Invalid username or password!";
this._changeDetectorRef.markForCheck();
} }
} }
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { LayoutComponent } from '../ui/layout.component'; import { LayoutComponent } from '../ui/layout.component';
import { LoginComponent } from '../../+login/feature/login.component';
import { AuthGuard } from '../../+login/data-access/guard/auth.guard'; import { AuthGuard } from '../../+login/data-access/guard/auth.guard';
import { AdminGuard } from '../../+login/data-access/guard/admin.guard'; import { AdminGuard } from '../../+login/data-access/guard/admin.guard';
import { JobComponent } from '../../+job/feature/job.component';
import { LoginGuard } from '../../+login/data-access/guard/login.guard';
export const shellRoutes: Routes = [ export const shellRoutes: Routes = [
{ {
...@@ -37,9 +38,20 @@ export const shellRoutes: Routes = [ ...@@ -37,9 +38,20 @@ export const shellRoutes: Routes = [
children: [ children: [
{ {
path: '', path: '',
canActivate: [AuthGuard], canActivate: [LoginGuard],
loadChildren: () => import('../../+login/login.routes'), loadChildren: () => import('../../+login/login.routes'),
} }
] ]
}, },
{
path: 'jobs',
component: LayoutComponent,
children: [
{
path: '',
canActivate: [],
loadChildren: () => import('../../+job/job.routes')
}
]
}
]; ];
<nz-header class="tw-bg-white"> <nz-header class="tw-bg-white tw-flex tw-flex-wrap">
<a routerLink="/home" routerLinkActive="active"> <a routerLink="/home" routerLinkActive="active">
<img src="https://yt3.googleusercontent.com/y4J_Fs5ksRlxx6_LzT1VKxVqH_T8Vyn_RN_YYgLJhuMzBS5qxTgm7NlEcMkQd3hgCpfWtYcEUg=s900-c-k-c0x00ffffff-no-rj" class="tw-border tw-rounded-full tw-w-10 tw-h-10 tw-mt-3 tw-float-left"> <img src="https://yt3.googleusercontent.com/y4J_Fs5ksRlxx6_LzT1VKxVqH_T8Vyn_RN_YYgLJhuMzBS5qxTgm7NlEcMkQd3hgCpfWtYcEUg=s900-c-k-c0x00ffffff-no-rj" class="tw-border tw-rounded-full tw-w-10 tw-h-10 tw-mt-3 tw-float-left">
</a> </a>
<ul nz-menu nzMode="horizontal"> <ul nz-menu nzMode="horizontal">
<li nz-menu-item>Home</li> <li nz-menu-item>Home</li>
<ng-container *ngIf="!isLoggedIn()">
<li nz-menu-item routerLink="/login" routerLinkActive="active">Login</li> <li nz-menu-item routerLink="/login" routerLinkActive="active">Login</li>
<li nz-menu-item>Register</li> <li nz-menu-item>Register</li>
</ng-container>
<ng-container *ngIf="isLoggedIn()">
<li *ngIf="isAdmin()" nz-menu-item routerLink="/admin" routerLinkActive="active">Admin</li>
</ng-container>
</ul> </ul>
<ng-container *ngIf="isLoggedIn()">
<div class="tw-flex-grow">
<div class="tw-grid tw-justify-items-end">
<img nz-dropdown nzTrigger="click" [nzDropdownMenu]="menu" src="https://avatars.githubusercontent.com/u/92918544?v=4" class="tw-border tw-rounded-full tw-w-10 tw-h-10 tw-mt-3 tw-float-left">
<nz-dropdown-menu #menu="nzDropdownMenu">
<ul nz-menu>
<li nz-menu-item>Profile</li>
<li (click)="logOut()" nz-menu-item routerLink="/logout" routerLinkActive="active">Logout</li>
</ul>
</nz-dropdown-menu>
</div>
</div>
</ng-container>
</nz-header> </nz-header>
...@@ -3,15 +3,31 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; ...@@ -3,15 +3,31 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { NzLayoutModule } from 'ng-zorro-antd/layout'; import { NzLayoutModule } from 'ng-zorro-antd/layout';
import { NzCardModule } from 'ng-zorro-antd/card'; import { NzCardModule } from 'ng-zorro-antd/card';
import { NzMenuModule } from 'ng-zorro-antd/menu'; import { NzMenuModule } from 'ng-zorro-antd/menu';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown'
import { RouterLink, RouterLinkActive } from '@angular/router'; import { RouterLink, RouterLinkActive } from '@angular/router';
import { AuthService } from '../../../../../+login/data-access/service/auth.service';
@Component({ @Component({
selector: 'meu-header', selector: 'meu-header',
standalone: true, standalone: true,
imports: [CommonModule, NzLayoutModule, NzCardModule, NzMenuModule, RouterLink, RouterLinkActive], imports: [CommonModule, NzLayoutModule, NzCardModule, NzMenuModule, RouterLink, RouterLinkActive, NzDropDownModule],
templateUrl: './header.component.html', templateUrl: './header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class HeaderComponent implements OnInit { export class HeaderComponent implements OnInit {
constructor(private _service: AuthService) {}
ngOnInit(): void {} ngOnInit(): void {}
isLoggedIn(): boolean {
return this._service.isLoggedIn();
}
isAdmin(): boolean {
return this._service.isAdmin();
}
logOut() {
localStorage.removeItem("token");
localStorage.removeItem("role");
}
} }
...@@ -7,6 +7,7 @@ export interface ResponseResult<T> { ...@@ -7,6 +7,7 @@ export interface ResponseResult<T> {
path: string; path: string;
timestamp: number; timestamp: number;
} }
export interface Rows<T> { export interface Rows<T> {
rows: T[]; rows: T[];
page: number; page: number;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment