diff --git a/src/app/core/interceptors/handle/error.interceptor.ts b/src/app/core/interceptors/handle/error.interceptor.ts index b36f29ec..ff56de97 100644 --- a/src/app/core/interceptors/handle/error.interceptor.ts +++ b/src/app/core/interceptors/handle/error.interceptor.ts @@ -80,6 +80,8 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { cancelText: 'Hủy', onConfirm: () => { router.navigate(['/auth/identity/login']); + localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); }, }, }) diff --git a/src/app/core/router-manager/horizontal-menu.ts b/src/app/core/router-manager/horizontal-menu.ts index c589483c..2cca7818 100644 --- a/src/app/core/router-manager/horizontal-menu.ts +++ b/src/app/core/router-manager/horizontal-menu.ts @@ -23,6 +23,7 @@ export function getNavHorizontalItems(roles: string[]): SidebarItem[] { path: '/resource-learning/list-resource', label: 'Kho tài liệu', icon: 'fas fa-book', + isVisible: !(roles.length !== 0), }, { id: 'message', @@ -50,6 +51,7 @@ export function getNavHorizontalItems(roles: string[]): SidebarItem[] { path: '/service-and-payment/payment', label: 'Thanh toán', icon: 'fas fa-credit-card', + isVisible: !(roles.length !== 0), }, { id: 'organization ', @@ -59,6 +61,7 @@ export function getNavHorizontalItems(roles: string[]): SidebarItem[] { label: 'Tổ chức', icon: 'fa-solid fa-building-user', // isVisible: !roles.includes(auth_lv2[0]), + isVisible: !(roles.length !== 0), }, ]; } diff --git a/src/app/core/services/api-service/exercise.service.ts b/src/app/core/services/api-service/exercise.service.ts index 1d83ed5d..ed0f283c 100644 --- a/src/app/core/services/api-service/exercise.service.ts +++ b/src/app/core/services/api-service/exercise.service.ts @@ -123,7 +123,8 @@ export class ExerciseService { tags, difficulty, search - ) + ), + true ); } diff --git a/src/app/core/services/api-service/post.service.ts b/src/app/core/services/api-service/post.service.ts index 42a5e5fe..b37fe790 100644 --- a/src/app/core/services/api-service/post.service.ts +++ b/src/app/core/services/api-service/post.service.ts @@ -25,71 +25,19 @@ export class PostService { ); } + searchPosts(page: number, size: number, search?: string | null) { + return this.api.get>>( + API_CONFIG.ENDPOINTS.GET.SEARCH_POST(page, size, search), + true + ); + } + getPostDetails(id: string) { return this.api.get>( API_CONFIG.ENDPOINTS.GET.GET_POST_DETAILS(id) ); } - //deprecated - // addPost(data: CreatePostRequest) { - // const formData = new FormData(); - - // // Helper append (chỉ thêm nếu có giá trị thật sự) - // const safeAppend = (key: string, value: any) => { - // if ( - // value !== undefined && - // value !== null && - // !(typeof value === 'string' && value.trim() === '') - // ) { - // formData.append(key, value); - // } - // }; - - // // basic fields - // safeAppend('title', data.title); - // safeAppend('orgId', data.orgId); - // safeAppend('content', data.content); - // safeAppend('isPublic', String(data.isPublic)); - // safeAppend('allowComment', String(data.allowComment)); - // safeAppend('postType', data.postType); - // safeAppend('hashtag', data.hashtag); - - // // array fields - // safeAppend(`oldImgesUrls`, data.oldImgesUrls); - - // // optional status - // safeAppend('status', data.status); - - // // fileDocument - // if (data.fileDocument) { - // const fd = data.fileDocument; - // safeAppend('fileDocument.file', fd.file); - // safeAppend('fileDocument.category', fd.category); - // safeAppend('fileDocument.description', fd.description); - // fd.tags?.forEach((tag, i) => safeAppend(`fileDocument.tags[${i}]`, tag)); - // if (typeof fd.isLectureVideo === 'boolean') { - // safeAppend('fileDocument.isLectureVideo', String(fd.isLectureVideo)); - // } - // if (typeof fd.isTextBook === 'boolean') { - // safeAppend('fileDocument.isTextBook', String(fd.isTextBook)); - // } - // safeAppend('fileDocument.orgId', fd.orgId); - // } - - // // Debug log - // for (const [key, val] of formData.entries()) { - // console.log(key, val); - // } - - // // Gửi form-data (KHÔNG ép Content-Type) - // return this.api.post( - // API_CONFIG.ENDPOINTS.POST.ADD_POST, - // formData, - // true - // ); - // } - //sử dụng postWithFormData createPost(data: CreatePostRequest) { const { fileDocument, ...otherData } = data; diff --git a/src/app/core/services/config-service/api.enpoints.ts b/src/app/core/services/config-service/api.enpoints.ts index a99a04bd..9c3a7fc6 100644 --- a/src/app/core/services/config-service/api.enpoints.ts +++ b/src/app/core/services/config-service/api.enpoints.ts @@ -134,6 +134,12 @@ export const API_CONFIG = { GET_CHAT_MESSAGES: (page: number, size: number, conversationId: string) => `/chat/messages?page=${page}&size=${size}&conversationId=${conversationId}`, GET_POST_DETAILS: (postId: string) => `/post/${postId}`, + SEARCH_POST: (page: number, size: number, search?: string | null) => { + let query = `/search/posts/filter?page=${page}&size=${size}`; + if (search) query += `&q=${encodeURIComponent(search)}`; + + return query; + }, GET_SAVED_POSTS: (page: number, size: number) => `/profile/posts/saved?page=${page}&size=${size}`, GET_COMMENT_BY_POST_ID: ( diff --git a/src/app/features/excercise/exercise-layout/exercise-layout.component.html b/src/app/features/excercise/exercise-layout/exercise-layout.component.html index 4955b2b1..576d82e2 100644 --- a/src/app/features/excercise/exercise-layout/exercise-layout.component.html +++ b/src/app/features/excercise/exercise-layout/exercise-layout.component.html @@ -1,10 +1,10 @@
- @if (showSidebar) { - - + @if (showSidebar && isAuthenticated) { + + }
diff --git a/src/app/features/excercise/exercise-layout/exercise-layout.component.ts b/src/app/features/excercise/exercise-layout/exercise-layout.component.ts index 1fcf5fee..c5b3affb 100644 --- a/src/app/features/excercise/exercise-layout/exercise-layout.component.ts +++ b/src/app/features/excercise/exercise-layout/exercise-layout.component.ts @@ -8,6 +8,7 @@ import { filter } from 'rxjs/internal/operators/filter'; import { sidebarExercises } from '../../../core/router-manager/vetical-menu-dynamic/exercise-vetical-menu'; import { decodeJWT } from '../../../shared/utils/stringProcess'; import { SidebarItem } from '../../../core/models/data-handle'; +import { checkAuthenticated } from '../../../shared/utils/authenRoleActions'; @Component({ selector: 'app-exercise-layout', @@ -20,6 +21,7 @@ export class ExerciseLayoutComponent implements OnInit, OnDestroy { sidebarData: SidebarItem[] = []; showSidebar = true; + isAuthenticated = false; // Danh sách các route cần ẩn sidebar private routesToHideSidebar: string[] = [ @@ -32,6 +34,7 @@ export class ExerciseLayoutComponent implements OnInit, OnDestroy { constructor(private router: Router) { const roles = decodeJWT(localStorage.getItem('token') ?? '')?.payload.roles; this.sidebarData = sidebarExercises(roles); + this.isAuthenticated = checkAuthenticated(); } ngOnInit() { diff --git a/src/app/features/post/pages/post-list/post-list.html b/src/app/features/post/pages/post-list/post-list.html index c4cd94f7..86d4b936 100644 --- a/src/app/features/post/pages/post-list/post-list.html +++ b/src/app/features/post/pages/post-list/post-list.html @@ -12,7 +12,7 @@ [isSvg]="false" [icon]="'fa fa-search'" > - + > --> + @if (authenticated) {
+ }
- @if (posts && posts.length > 0) { -
- @for (post of posts; track trackByPostId($index, post)) { -
- -
- } + @if (posts && posts.length > 0 && !(isLoadingInitial || isLoadingMore || + isLoading)) { +
+ @for (post of posts; track trackByPostId($index, post)) { +
+
+ } +
} - @if (isLoadingInitial || isLoadingMore) { -
- -
+ @if (isLoadingInitial || isLoadingMore || isLoading) { +
+ +
} @if (!isLoading && (!posts || posts.length === 0)) { -
- - -

Không có dữ liệu

-
+
+ + +

Không có dữ liệu

+
}
@@ -112,7 +109,7 @@ [items]="trendingData" title="Popular Technologies" [maxItems]="15" - > + >
diff --git a/src/app/features/post/pages/post-list/post-list.ts b/src/app/features/post/pages/post-list/post-list.ts index 52b87acf..467c6a87 100644 --- a/src/app/features/post/pages/post-list/post-list.ts +++ b/src/app/features/post/pages/post-list/post-list.ts @@ -1,6 +1,5 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { InputComponent } from '../../../../shared/components/fxdonad-shared/input/input'; -import { DropdownButtonComponent } from '../../../../shared/components/fxdonad-shared/dropdown/dropdown.component'; import { PostCardComponent } from '../../../../shared/components/my-shared/post-card/post-card'; import { @@ -24,6 +23,7 @@ import { openModalNotification, sendNotification, } from '../../../../shared/utils/notification'; +import { checkAuthenticated } from '../../../../shared/utils/authenRoleActions'; @Component({ selector: 'app-post-list', @@ -32,7 +32,6 @@ import { standalone: true, imports: [ InputComponent, - DropdownButtonComponent, PostCardComponent, PopularPostComponent, SkeletonLoadingComponent, @@ -46,6 +45,7 @@ import { schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class PostListComponent { + private debounceTimer: ReturnType | null = null; lottieOptions = { path: 'assets/lottie-animation/nodata.json', autoplay: true, @@ -54,6 +54,8 @@ export class PostListComponent { posts: PostCardInfo[] = []; postsraw!: PostResponse[]; + authenticated = false; + trendingData: TrendingItem[] = [ { name: 'Angular', views: 15000 }, { name: 'React', views: 12000 }, @@ -66,6 +68,7 @@ export class PostListComponent { hasMore = true; postname = ''; tag: { value: string; label: string }[] = []; + pendingVote: { [postId: string]: boolean } = {}; status: { value: string; label: string }[] = []; selectedOptions: { [key: string]: any } = {}; activeDropdown: string | null = null; @@ -82,26 +85,28 @@ export class PostListComponent { private postservice: PostService, private store: Store ) { - this.tag = [ - { value: '1', label: 'react' }, - { value: '0', label: 'javascript' }, - { value: '2', label: 'C#' }, - { value: '3', label: 'java' }, - { value: '4', label: 'python' }, - ]; - // Mock data for status - this.status = [ - { value: '0', label: 'Reject' }, - { value: '1', label: 'Accepted' }, - { value: '2', label: 'Pendding' }, - ]; + // this.tag = [ + // { value: '1', label: 'react' }, + // { value: '0', label: 'javascript' }, + // { value: '2', label: 'C#' }, + // { value: '3', label: 'java' }, + // { value: '4', label: 'python' }, + // ]; + // // Mock data for status + // this.status = [ + // { value: '0', label: 'Reject' }, + // { value: '1', label: 'Accepted' }, + // { value: '2', label: 'Pendding' }, + // ]; + + this.authenticated = checkAuthenticated(); } ngOnInit(): void { this.fetchPostList(true); } - fetchPostList(isInitialLoad = false) { + fetchPostList(isInitialLoad = false, search = false) { this.isLoading = true; if (isInitialLoad) { @@ -110,31 +115,38 @@ export class PostListComponent { this.isLoadingNextPage = true; } - this.postservice.getVisiblePosts(this.pageIndex, this.size).subscribe({ - next: (res) => { - const newPostsRaw = res.result.data; - this.totalPages = res.result.totalPages; - - // ✅ Chỉ map phần dữ liệu mới - const newPosts = this.mapPostdatatoPostCardInfo(newPostsRaw); - if (isInitialLoad) { - this.posts = []; // reset khi load mới - } - this.posts.push(...newPosts); // append thêm + this.postservice + .searchPosts(this.pageIndex, this.size, this.postname) + .subscribe({ + next: (res) => { + const newPostsRaw = res.result.data; + this.totalPages = res.result.totalPages; + + // ✅ Chỉ map phần dữ liệu mới + const newPosts = this.mapPostdatatoPostCardInfo(newPostsRaw); + if (isInitialLoad) { + this.posts = []; // reset khi load mới + } + if (!search) { + this.posts.push(...newPosts); // append thêm + } else { + this.posts = []; // reset khi load mới + this.posts = newPosts; + } - if (isInitialLoad) { - this.isLoadingInitial = false; - } else { - this.isLoadingNextPage = false; - } + if (isInitialLoad) { + this.isLoadingInitial = false; + } else { + this.isLoadingNextPage = false; + } - this.isLoading = false; - }, - error: (err) => { - console.log(err); - this.isLoading = false; - }, - }); + this.isLoading = false; + }, + error: (err) => { + console.log(err); + this.isLoading = false; + }, + }); } loadNextPage() { @@ -154,12 +166,15 @@ export class PostListComponent { } // thêm field quản lý loading theo post - pendingVote: { [postId: string]: boolean } = {}; handleUpVote(id: string) { if (this.pendingVote[id]) return; // chặn spam const post = this.posts.find((p) => p.id === id); if (!post) return; + if (!this.authenticated) { + this.openModalNeedLogin(); + return; + } const prevVote = this.voteStates[id] ?? null; this.pendingVote[id] = true; @@ -203,6 +218,10 @@ export class PostListComponent { if (this.pendingVote[id]) return; const post = this.posts.find((p) => p.id === id); if (!post) return; + if (!this.authenticated) { + this.openModalNeedLogin(); + return; + } const prevVote = this.voteStates[id] ?? null; this.pendingVote[id] = true; @@ -245,6 +264,10 @@ export class PostListComponent { handleToggleSave(postId: string) { const post = this.posts.find((p) => p.id === postId); if (!post) return; + if (!this.authenticated) { + this.openModalNeedLogin(); + return; + } // Nếu đang lưu thì gọi unSave if (post.isSaved) { @@ -277,27 +300,36 @@ export class PostListComponent { } handleInputChange(value: string | number): void { + this.isLoading = true; + this.pageIndex = 1; + + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + } + this.postname = value.toString(); - console.log('Input changed:', this.postname); + this.debounceTimer = setTimeout(() => { + this.fetchPostList(false, true); + }, 500); // chờ 500ms sau khi dừng gõ mới gọi } - handleSelect(dropdownKey: string, selected: any): void { - // Reset toàn bộ các lựa chọn trước đó - this.selectedOptions = {}; + // handleSelect(dropdownKey: string, selected: any): void { + // // Reset toàn bộ các lựa chọn trước đó + // this.selectedOptions = {}; - // Lưu lại option vừa chọn - this.selectedOptions[dropdownKey] = selected; + // // Lưu lại option vừa chọn + // this.selectedOptions[dropdownKey] = selected; - // this.router.navigate(['/', dropdownKey, selected.label]); + // // this.router.navigate(['/', dropdownKey, selected.label]); - console.log(this.selectedOptions); - } + // console.log(this.selectedOptions); + // } - toggleDropdown(id: string): void { - // Nếu bạn muốn chỉ mở 1 dropdown tại một thời điểm - this.activeDropdown = this.activeDropdown === id ? null : id; - } + // toggleDropdown(id: string): void { + // // Nếu bạn muốn chỉ mở 1 dropdown tại một thời điểm + // this.activeDropdown = this.activeDropdown === id ? null : id; + // } deletePost(id: string) { this.postservice.deletePost(id).subscribe({ @@ -321,12 +353,27 @@ export class PostListComponent { ); } + openModalNeedLogin() { + openModalNotification( + this.store, + 'Chưa đăng nhập', + 'Bạn cần đăng nhập để tiếp tục', + 'Đồng ý', + 'Hủy', + () => this.router.navigate(['/auth/identity/login']) + ); + } + handleAdd = () => { this.router.navigate(['/post-features/post-create']); }; goToDetail = ($event: string) => { - this.router.navigate(['/post-features/post-details', $event]); + if (!this.authenticated) { + this.openModalNeedLogin(); + } else { + this.router.navigate(['/post-features/post-details', $event]); + } }; // ...existing code... } diff --git a/src/app/features/post/post-layout/post-layout.component.html b/src/app/features/post/post-layout/post-layout.component.html index b6e9465f..d0c26e77 100644 --- a/src/app/features/post/post-layout/post-layout.component.html +++ b/src/app/features/post/post-layout/post-layout.component.html @@ -1,10 +1,10 @@
- @if (showSidebar) { - - + @if (showSidebar && isAuthenticated) { + + }
diff --git a/src/app/features/post/post-layout/post-layout.component.ts b/src/app/features/post/post-layout/post-layout.component.ts index fc6c0afd..cdd58c07 100644 --- a/src/app/features/post/post-layout/post-layout.component.ts +++ b/src/app/features/post/post-layout/post-layout.component.ts @@ -5,6 +5,7 @@ import { Router, RouterModule } from '@angular/router'; import { SidebarItem } from '../../../core/models/data-handle'; import { decodeJWT } from '../../../shared/utils/stringProcess'; import { sidebarPosts } from '../../../core/router-manager/vetical-menu-dynamic/post-vertical-menu'; +import { checkAuthenticated } from '../../../shared/utils/authenRoleActions'; @Component({ selector: 'app-post-layout', @@ -17,9 +18,11 @@ export class PostLayoutComponent { sidebarData: SidebarItem[] = []; showSidebar = true; + isAuthenticated = false; constructor(private router: Router) { const roles = decodeJWT(localStorage.getItem('token') ?? '')?.payload.roles; this.sidebarData = sidebarPosts(roles); + this.isAuthenticated = checkAuthenticated(); } } diff --git a/src/app/features/student-statistic/test-statistic/student-statistic.component.html b/src/app/features/student-statistic/test-statistic/student-statistic.component.html index c090ff5b..713db50a 100644 --- a/src/app/features/student-statistic/test-statistic/student-statistic.component.html +++ b/src/app/features/student-statistic/test-statistic/student-statistic.component.html @@ -63,7 +63,7 @@ > - + > [alt]="data.uploader.name" class="avatar" (error)="handleImageError($event)" - /> - {{ data.uploader.name }} -
-
- {{ getTimeAgo(data.uploadTime) }} -
+ /> + {{ data.uploader.name }} +
+
+ {{ getTimeAgo(data.uploadTime) }}
+ -
-

- {{ data.description }} -

- @if (data.description.length > 300) { - - } -
+
+

+ {{ data.description }} +

+ @if (data.description.length > 300) { + + } +
-
-
-
- {{ +
+
+
+ {{ data.difficulty === "EASY" - ? "Dễ" - : data.difficulty === "MEDIUM" - ? "Trung bình" - : data.difficulty === "HARD" - ? "Khó" - : (data.difficulty | titlecase) - }} -
-
- -
+
- - +