Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions apps/lms/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
28 changes: 15 additions & 13 deletions apps/sale/features/favorite-tutors/hooks/useFavoriteTutors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -19,17 +20,7 @@ export function useFavoriteTutorIds() {
queryKey: queryKeys.favoriteTutors.ids,
queryFn: async (): Promise<string[]> => {
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,
Expand Down Expand Up @@ -136,6 +127,16 @@ export function useToggleFavoriteTutorMutation() {
});
},
onSuccess: (_data, variables) => {
queryClient.setQueryData<string[]>(
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ư",
Expand All @@ -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"],
});
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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 : [];
}
5 changes: 4 additions & 1 deletion apps/sale/features/tutor-list/components/TutorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="relative h-20 w-20 shrink-0 overflow-hidden rounded-2xl border border-[#dce3f0] bg-[#cfe1fa] sm:h-24 sm:w-24">
{failed || !tutor.avatarUrl ? (
{failed ||
!tutor.avatarUrl ||
tutor.avatarUrl.includes("demo.invalid") ? (
<div
className="flex h-full w-full items-center justify-center text-2xl font-extrabold text-[#280f91]"
aria-label={`Ảnh dự phòng của ${name}`}
Expand Down
47 changes: 27 additions & 20 deletions apps/sale/features/tutor-profile/components/TutorHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,26 +137,33 @@ export function TutorHero({ tutor }: TutorHeroProps) {
<div className="grid min-w-0 gap-5">
<div className="grid min-w-0 gap-5 sm:grid-cols-[156px_minmax(0,1fr)] sm:items-start lg:grid-cols-[172px_minmax(0,1fr)] lg:gap-6">
<div className="flex justify-center sm:justify-start">
<a
href={tutor.avatarUrl || "/images/Tutor/1.png"}
target="_blank"
rel="noreferrer"
aria-label={`Xem ảnh gia sư ${tutor.displayName}`}
className="group relative aspect-square h-36 overflow-hidden rounded-[1.75rem] border-4 border-white bg-[#fff3cb] shadow-2xl shadow-[#280f91]/14 outline-none ring-1 ring-[#cfe1fa] transition hover:shadow-[#280f91]/20 focus-visible:ring-2 focus-visible:ring-[#280f91] focus-visible:ring-offset-2 sm:h-40 lg:h-44"
>
<Image
src={tutor.avatarUrl || "/images/Tutor/1.png"}
alt={`Ảnh đại diện của gia sư ${tutor.displayName}`}
fill
priority
unoptimized={tutor.avatarUrl.startsWith("blob:")}
sizes="(min-width: 1024px) 176px, (min-width: 640px) 160px, 144px"
className="object-cover object-center transition duration-300 group-hover:scale-[1.03]"
/>
<span className="absolute bottom-3 right-3 flex h-8 w-8 items-center justify-center rounded-full border-2 border-white bg-[#447353] text-white shadow-lg">
<BadgeCheck size={15} aria-hidden="true" />
</span>
</a>
{(() => {
const avatarSrc =
tutor.avatarUrl && !tutor.avatarUrl.includes("demo.invalid")
? tutor.avatarUrl
: "/images/Tutor/1.png";
return (
<a
href={avatarSrc}
target="_blank"
rel="noreferrer"
aria-label={`Xem ảnh gia sư ${tutor.displayName}`}
className="group relative aspect-square h-36 overflow-hidden rounded-[1.75rem] border-4 border-white bg-[#fff3cb] shadow-2xl shadow-[#280f91]/14 outline-none ring-1 ring-[#cfe1fa] transition hover:shadow-[#280f91]/20 focus-visible:ring-2 focus-visible:ring-[#280f91] focus-visible:ring-offset-2 sm:h-40 lg:h-44"
>
<Image
src={avatarSrc}
alt={`Ảnh đại diện của gia sư ${tutor.displayName}`}
fill
priority
sizes="(min-width: 1024px) 176px, (min-width: 640px) 160px, 144px"
className="object-cover object-center transition duration-300 group-hover:scale-[1.03]"
/>
<span className="absolute bottom-3 right-3 flex h-8 w-8 items-center justify-center rounded-full border-2 border-white bg-[#447353] text-white shadow-lg">
<BadgeCheck size={15} aria-hidden="true" />
</span>
</a>
);
})()}
</div>

<div className="min-w-0 pt-0 sm:pt-1 lg:pt-2">
Expand Down
20 changes: 20 additions & 0 deletions apps/sale/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
20 changes: 20 additions & 0 deletions apps/staff/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
10 changes: 6 additions & 4 deletions packages/core/configs/navbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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" },
Expand Down
Loading