From 639abe89a24bcd82855e09c2838b9ddae3f89f46 Mon Sep 17 00:00:00 2001 From: Vy Ngo Chi Date: Thu, 3 Sep 2026 19:09:14 +0700 Subject: [PATCH] feat: implement dynamic navbar configuration, favorite tutor schemas, and API rewrites for LMS and sale apps --- apps/lms/next.config.ts | 20 ++++++++ .../hooks/useFavoriteTutors.ts | 28 +++++------ .../schemas/favorite-tutor-ids.schema.ts | 41 +++++++++++++++++ .../tutor-list/components/TutorCard.tsx | 5 +- .../tutor-profile/components/TutorHero.tsx | 46 +++++++++++-------- apps/sale/next.config.ts | 20 ++++++++ apps/staff/next.config.ts | 20 ++++++++ packages/core/configs/navbar.ts | 10 ++-- 8 files changed, 153 insertions(+), 37 deletions(-) create mode 100644 apps/sale/features/favorite-tutors/schemas/favorite-tutor-ids.schema.ts diff --git a/apps/lms/next.config.ts b/apps/lms/next.config.ts index a9bd400..b57985b 100644 --- a/apps/lms/next.config.ts +++ b/apps/lms/next.config.ts @@ -17,6 +17,26 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "res.cloudinary.com", }, + { + protocol: "https", + hostname: "demo.invalid", + }, + { + protocol: "https", + hostname: "images.unsplash.com", + }, + { + protocol: "https", + hostname: "lh3.googleusercontent.com", + }, + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + }, + { + protocol: "https", + hostname: "api.dicebear.com", + }, ], }, async rewrites() { diff --git a/apps/sale/features/favorite-tutors/hooks/useFavoriteTutors.ts b/apps/sale/features/favorite-tutors/hooks/useFavoriteTutors.ts index f57cc05..cb0749e 100644 --- a/apps/sale/features/favorite-tutors/hooks/useFavoriteTutors.ts +++ b/apps/sale/features/favorite-tutors/hooks/useFavoriteTutors.ts @@ -8,6 +8,7 @@ import { useToastStore } from "@workspace/ui/components/ui/bee-toast/useToastSto import { useFavoriteAuthModalStore } from "../store/useFavoriteAuthModalStore"; import type { ApiTutorProfile } from "../../tutor-list/data/types"; import type { FavoriteTutorsListParams } from "@workspace/core/types/favorite-tutors.type"; +import { parseFavoriteTutorIds } from "../schemas/favorite-tutor-ids.schema"; /** * Hook to query all favorite tutor IDs for quick lookup (heart active/inactive state). @@ -19,17 +20,7 @@ export function useFavoriteTutorIds() { queryKey: queryKeys.favoriteTutors.ids, queryFn: async (): Promise => { const response = await favoriteTutorsService.getFavoriteTutorIds(); - const rawData = response?.data; - - if (Array.isArray(rawData)) { - return rawData; - } - - if (rawData && typeof rawData === "object" && "ids" in rawData && Array.isArray((rawData as { ids: string[] }).ids)) { - return (rawData as { ids: string[] }).ids; - } - - return []; + return parseFavoriteTutorIds(response?.data); }, enabled: isAuthenticated, staleTime: 5 * 60 * 1000, @@ -136,6 +127,16 @@ export function useToggleFavoriteTutorMutation() { }); }, onSuccess: (_data, variables) => { + queryClient.setQueryData( + queryKeys.favoriteTutors.ids, + (currentIds = []) => + variables.isCurrentlySaved + ? currentIds.filter((id) => id !== variables.tutorId) + : currentIds.includes(variables.tutorId) + ? currentIds + : [...currentIds, variables.tutorId], + ); + if (variables.isCurrentlySaved) { addToast({ title: "Đã bỏ lưu gia sư", @@ -155,8 +156,9 @@ export function useToggleFavoriteTutorMutation() { } }, onSettled: () => { - // Invalidate queries to sync with backend - queryClient.invalidateQueries({ queryKey: queryKeys.favoriteTutors.all }); + queryClient.invalidateQueries({ + queryKey: [...queryKeys.favoriteTutors.all, "list"], + }); }, }); diff --git a/apps/sale/features/favorite-tutors/schemas/favorite-tutor-ids.schema.ts b/apps/sale/features/favorite-tutors/schemas/favorite-tutor-ids.schema.ts new file mode 100644 index 0000000..1947f84 --- /dev/null +++ b/apps/sale/features/favorite-tutors/schemas/favorite-tutor-ids.schema.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +const favoriteTutorIdValueSchema = z + .union([z.string(), z.number()]) + .transform((value) => String(value)); + +const favoriteTutorIdItemSchema = z.union([ + favoriteTutorIdValueSchema, + z + .object({ + tutorProfileId: favoriteTutorIdValueSchema.optional(), + profileId: favoriteTutorIdValueSchema.optional(), + id: favoriteTutorIdValueSchema.optional(), + }) + .transform((value) => value.tutorProfileId ?? value.profileId ?? value.id), +]); + +const favoriteTutorIdsArraySchema = z + .array(favoriteTutorIdItemSchema) + .transform((items) => [ + ...new Set(items.filter((item): item is string => Boolean(item))), + ]); + +const favoriteTutorIdsPayloadSchema = z + .union([ + favoriteTutorIdsArraySchema, + z.object({ ids: favoriteTutorIdsArraySchema }), + z.object({ tutorProfileIds: favoriteTutorIdsArraySchema }), + z.object({ favoriteTutorIds: favoriteTutorIdsArraySchema }), + ]) + .transform((payload) => { + if (Array.isArray(payload)) return payload; + if ("ids" in payload) return payload.ids; + if ("tutorProfileIds" in payload) return payload.tutorProfileIds; + return payload.favoriteTutorIds; + }); + +export function parseFavoriteTutorIds(payload: unknown): string[] { + const result = favoriteTutorIdsPayloadSchema.safeParse(payload); + return result.success ? result.data : []; +} diff --git a/apps/sale/features/tutor-list/components/TutorCard.tsx b/apps/sale/features/tutor-list/components/TutorCard.tsx index cc78964..d8d93ee 100644 --- a/apps/sale/features/tutor-list/components/TutorCard.tsx +++ b/apps/sale/features/tutor-list/components/TutorCard.tsx @@ -43,9 +43,12 @@ const getTeachingModeInfo = (modes: string[]) => { function TutorAvatar({ tutor }: { tutor: ApiTutorProfile }) { const [failed, setFailed] = useState(false); const name = tutor.displayName || "Gia Sư"; + return (
- {failed || !tutor.avatarUrl ? ( + {failed || + !tutor.avatarUrl || + tutor.avatarUrl.includes("demo.invalid") ? (
- - {`Ảnh - - - + {(() => { + const avatarSrc = + tutor.avatarUrl && !tutor.avatarUrl.includes("demo.invalid") + ? tutor.avatarUrl + : "/images/Tutor/1.png"; + return ( + + {`Ảnh + + + + ); + })()}
diff --git a/apps/sale/next.config.ts b/apps/sale/next.config.ts index a9bd400..b57985b 100644 --- a/apps/sale/next.config.ts +++ b/apps/sale/next.config.ts @@ -17,6 +17,26 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "res.cloudinary.com", }, + { + protocol: "https", + hostname: "demo.invalid", + }, + { + protocol: "https", + hostname: "images.unsplash.com", + }, + { + protocol: "https", + hostname: "lh3.googleusercontent.com", + }, + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + }, + { + protocol: "https", + hostname: "api.dicebear.com", + }, ], }, async rewrites() { diff --git a/apps/staff/next.config.ts b/apps/staff/next.config.ts index 56ee7e0..113d8d3 100644 --- a/apps/staff/next.config.ts +++ b/apps/staff/next.config.ts @@ -17,6 +17,26 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "res.cloudinary.com", }, + { + protocol: "https", + hostname: "demo.invalid", + }, + { + protocol: "https", + hostname: "images.unsplash.com", + }, + { + protocol: "https", + hostname: "lh3.googleusercontent.com", + }, + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + }, + { + protocol: "https", + hostname: "api.dicebear.com", + }, ], }, async rewrites() { diff --git a/packages/core/configs/navbar.ts b/packages/core/configs/navbar.ts index 957ff58..8100c3b 100644 --- a/packages/core/configs/navbar.ts +++ b/packages/core/configs/navbar.ts @@ -102,15 +102,17 @@ export function resolveNavbarState({ } const guestMenu: NavbarItem[] = [ - { label: "Gia sư", href: "/tutors" }, - { label: "Cộng đồng", href: "/community" }, + { label: "Gia sư 1:1", href: "/tutors" }, + { label: "Tìm lớp", href: "/classes" }, + // { label: "Cộng đồng", href: "/community" }, { label: "Trở thành gia sư", href: "/tutor-guide" }, { label: "Về chúng tôi", href: "/about-us" }, ]; const learnerMenu: NavbarItem[] = [ { label: "Tìm gia sư", href: "/tutors" }, - { label: "Cộng đồng", href: "/community" }, + { label: "Tìm lớp", href: "/classes" }, + // { label: "Cộng đồng", href: "/community" }, { label: "Tin nhắn", href: "/learner/messages", badgeKey: "unreadChatCount" }, { label: "Lịch sử kết nối", href: "/connections" }, { label: "Gia sư yêu thích", href: "/favorite-tutors" }, @@ -122,7 +124,7 @@ const tutorOnboardingMenu: NavbarItem[] = [ const tutorApprovedMenu: NavbarItem[] = [ { label: "Trang chủ", href: "/tutor/home" }, - { label: "Cộng đồng", href: "/community" }, + // { label: "Cộng đồng", href: "/community" }, { label: "Hồ sơ của tôi", href: "/tutor/profile" }, { label: "Tin nhắn", href: "/tutor/messages", badgeKey: "unreadChatCount" }, { label: "Lịch rảnh", href: "/tutor/availability" },