From f0b1dc673ef56ea2b28cd24dc815689a78bb6219 Mon Sep 17 00:00:00 2001 From: Vy Ngo Chi Date: Sat, 12 Sep 2026 16:46:38 +0700 Subject: [PATCH 1/4] feat: implement sale layout with message FAB, guest favorite modal, and navigation components --- apps/sale/app/layout.tsx | 2 + .../components/GuestFavoriteModal.tsx | 3 - .../messages/components/MessageFAB.tsx | 60 + apps/sale/features/messages/index.ts | 1 + .../tutor-list/hooks/useTutorSearch.ts | 7 +- .../demos/authenticated-navigation.html | 1060 +++++++++++++++++ .../public/icons/beewise-message-icon.svg | 1 + .../assets/icons/beewise-message-icon.svg | 1 + packages/core/configs/navbar.ts | 6 +- packages/ui/components/layout/Header.tsx | 68 +- 10 files changed, 1189 insertions(+), 20 deletions(-) create mode 100644 apps/sale/features/messages/components/MessageFAB.tsx create mode 100644 apps/sale/public/demos/authenticated-navigation.html create mode 100644 apps/sale/public/icons/beewise-message-icon.svg create mode 100644 packages/core/assets/icons/beewise-message-icon.svg diff --git a/apps/sale/app/layout.tsx b/apps/sale/app/layout.tsx index 4322fba..932b441 100644 --- a/apps/sale/app/layout.tsx +++ b/apps/sale/app/layout.tsx @@ -4,6 +4,7 @@ import "@workspace/ui/globals.css"; import { Metadata } from "next"; import { GuestFavoriteModal } from "@/features/favorite-tutors/components/GuestFavoriteModal"; import { NotificationDrawer } from "@/features/notifications/components/NotificationDrawer"; +import { MessageFAB } from "@/features/messages"; import { saleSeoConfig } from "@/configs/seo"; export const metadata: Metadata = { @@ -27,6 +28,7 @@ export default function RootLayout({ {children} + diff --git a/apps/sale/features/favorite-tutors/components/GuestFavoriteModal.tsx b/apps/sale/features/favorite-tutors/components/GuestFavoriteModal.tsx index ce1bd8c..4651a00 100644 --- a/apps/sale/features/favorite-tutors/components/GuestFavoriteModal.tsx +++ b/apps/sale/features/favorite-tutors/components/GuestFavoriteModal.tsx @@ -33,9 +33,6 @@ export function GuestFavoriteModal() { {/* Heart icon with badge */}
-
- -
diff --git a/apps/sale/features/messages/components/MessageFAB.tsx b/apps/sale/features/messages/components/MessageFAB.tsx new file mode 100644 index 0000000..7b3228f --- /dev/null +++ b/apps/sale/features/messages/components/MessageFAB.tsx @@ -0,0 +1,60 @@ +"use client"; + +import Link from "next/link"; +import Image from "next/image"; +import { usePathname } from "next/navigation"; +import { useAuthStore } from "@workspace/core/store/useAuthStore"; + +/** + * Floating message button shown at the bottom-right of the screen. + * Only visible for authenticated LEARNER users. + * Excluded on the messages page itself. + */ +export function MessageFAB() { + const pathname = usePathname(); + const user = useAuthStore((state) => state.user); + const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + const isAuthLoading = useAuthStore((state) => state.isAuthLoading); + + const unreadChatCount = Math.max(0, user?.unreadChatCount ?? 0); + const isLearner = user?.role?.trim().toUpperCase() === "LEARNER"; + + // Hide when: loading, unauthenticated, not learner, or already on messages page + const isOnMessagesPage = + pathname?.startsWith("/learner/messages") ?? false; + + if (isAuthLoading || !isAuthenticated || !isLearner || isOnMessagesPage) { + return null; + } + + return ( + + {/* Icon */} + Tin nhắn + + {/* Unread badge */} + {unreadChatCount > 0 && ( + + {unreadChatCount > 99 ? "99+" : unreadChatCount} + + )} + + ); +} diff --git a/apps/sale/features/messages/index.ts b/apps/sale/features/messages/index.ts index b44ccdb..1a0afef 100644 --- a/apps/sale/features/messages/index.ts +++ b/apps/sale/features/messages/index.ts @@ -4,6 +4,7 @@ export * from "./components/ChatRoomScreen"; export * from "./components/ChatRoomPanel"; export * from "./components/ChatSidebar"; export * from "./components/LearnerMessagesRoute"; +export * from "./components/MessageFAB"; export * from "./hooks/useMessages"; export * from "./hooks/useChatRoom"; diff --git a/apps/sale/features/tutor-list/hooks/useTutorSearch.ts b/apps/sale/features/tutor-list/hooks/useTutorSearch.ts index 733b951..b436a90 100644 --- a/apps/sale/features/tutor-list/hooks/useTutorSearch.ts +++ b/apps/sale/features/tutor-list/hooks/useTutorSearch.ts @@ -26,7 +26,7 @@ const mapFiltersToManualQuery = ( const manualQuery: ManualSearchQuery = { keyword: query || undefined, page, - pageSize: 50, + pageSize: 6, }; if (filters.teachingMode !== "all") { @@ -134,9 +134,8 @@ export function useTutorSearch() { const initialQuery = searchParams.get("q"); const [searchMode, setSearchMode] = useState(initialMode); - const [filtersByMode, setFiltersByMode] = useState< - Record - >(cachedFiltersByMode); + const [filtersByMode, setFiltersByMode] = + useState>(cachedFiltersByMode); const [queries, setQueries] = useState>(() => initialQuery === null ? cachedQueries diff --git a/apps/sale/public/demos/authenticated-navigation.html b/apps/sale/public/demos/authenticated-navigation.html new file mode 100644 index 0000000..f6b354b --- /dev/null +++ b/apps/sale/public/demos/authenticated-navigation.html @@ -0,0 +1,1060 @@ + + + + + + + BeeWise - Demo điều hướng sau đăng nhập + + + + + +
+
+ + BeeWise Gia sư +
+ +
+
+

Không gian làm việc hôm nay

+

Chào buổi sáng, Minh Anh

+

+ Bạn có một buổi học và hai tin nhắn mới cần phản hồi. +

+
+ +
+ + +
+
+ +
+
+
+

Buổi học tuần này

+

04

+
+
+

Học viên đang học

+

07

+
+
+

Đánh giá trung bình

+

4.9

+
+
+ +
+
+
+
+

Lịch dạy tiếp theo

+

Hôm nay, Thứ Năm 11 tháng 9

+
+ +
+ +
+
19:30
+
+

Toán lớp 9 với Nguyễn Gia Huy

+

90 phút, học trực tuyến

+
+ +
+
+ + +
+ +
+
+
+

Hoạt động cần chú ý

+

Ưu tiên những việc có ảnh hưởng đến học viên.

+
+
+
    +
  • + +
    + Nguyễn Gia Huy vừa gửi tin nhắn + Em muốn ôn lại phần phương trình trước buổi học. +
    + +
  • +
  • + +
    + Bạn nhận được một đánh giá mới + Phụ huynh đánh giá 5 sao sau buổi học thử. +
    + +
  • +
+
+
+
+ +
+ +
+ +
+ + +
+ + + + diff --git a/apps/sale/public/icons/beewise-message-icon.svg b/apps/sale/public/icons/beewise-message-icon.svg new file mode 100644 index 0000000..6321126 --- /dev/null +++ b/apps/sale/public/icons/beewise-message-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/core/assets/icons/beewise-message-icon.svg b/packages/core/assets/icons/beewise-message-icon.svg new file mode 100644 index 0000000..6321126 --- /dev/null +++ b/packages/core/assets/icons/beewise-message-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/core/configs/navbar.ts b/packages/core/configs/navbar.ts index 8100c3b..7891500 100644 --- a/packages/core/configs/navbar.ts +++ b/packages/core/configs/navbar.ts @@ -110,11 +110,9 @@ const guestMenu: NavbarItem[] = [ ]; const learnerMenu: NavbarItem[] = [ - { label: "Tìm gia sư", href: "/tutors" }, + { label: "Gia sư 1:1", href: "/tutors" }, { 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" }, ]; @@ -164,7 +162,7 @@ export function getNavbarConfig({ }): NavbarConfig { if (state === "LEARNER") { return { - homeHref: "/tutors", + homeHref: "/", centerItems: learnerMenu, rightItems: lmsAccessEnabled ? [{ label: "Vào LMS", href: "/lms/learner", variant: "primary" }] diff --git a/packages/ui/components/layout/Header.tsx b/packages/ui/components/layout/Header.tsx index ce35023..4b257c0 100644 --- a/packages/ui/components/layout/Header.tsx +++ b/packages/ui/components/layout/Header.tsx @@ -172,18 +172,24 @@ export function Header({ ); }; + const isScrollActive = scrolled && !isAuthenticated; + return (
From 898e3bfe1f4f048cc47bf996d4b44598c2f29e5d Mon Sep 17 00:00:00 2001 From: Vy Ngo Chi Date: Sat, 12 Sep 2026 22:30:51 +0700 Subject: [PATCH 3/4] feat: implement tutor profile and profile editor features with components --- apps/sale/app/(guest)/tutors/[id]/page.tsx | 5 +- .../components/TutorEvidenceEditorPreview.tsx | 3 +- .../components/TutorProfileEditorScreen.tsx | 8 +- .../components/TutorVideoEditorPreview.tsx | 3 - .../constants/profile-editor.fixture.ts | 19 +- .../utils/profile-editor.mapper.ts | 17 +- .../components/TutorAchievements.tsx | 150 ++++++ .../components/TutorBioSection.tsx | 40 +- .../components/TutorCertificates.tsx | 64 --- .../components/TutorConnectCard.tsx | 4 +- .../components/TutorEducationAchievements.tsx | 63 --- .../components/TutorFeedback.tsx | 86 ++-- .../tutor-profile/components/TutorHero.tsx | 444 ++++++++++-------- .../components/TutorIntroVideo.tsx | 106 +++-- .../components/TutorMobileCTA.tsx | 7 +- .../components/TutorOwnProfileScreen.tsx | 18 +- .../components/TutorProfilePage.tsx | 26 +- .../components/TutorProfilePrimitives.tsx | 70 ++- .../tutor-profile/components/TutorStats.tsx | 60 --- .../components/TutorTeachingHistory.tsx | 68 ++- .../components/TutorTeachingMethods.tsx | 44 +- .../tutor-profile/types/mockTutorProfile.ts | 145 ++---- .../tutor-profile/types/tutor.type.ts | 88 ++-- 23 files changed, 761 insertions(+), 777 deletions(-) create mode 100644 apps/sale/features/tutor-profile/components/TutorAchievements.tsx delete mode 100644 apps/sale/features/tutor-profile/components/TutorCertificates.tsx delete mode 100644 apps/sale/features/tutor-profile/components/TutorEducationAchievements.tsx delete mode 100644 apps/sale/features/tutor-profile/components/TutorStats.tsx diff --git a/apps/sale/app/(guest)/tutors/[id]/page.tsx b/apps/sale/app/(guest)/tutors/[id]/page.tsx index 3cd3d8a..a8e850b 100644 --- a/apps/sale/app/(guest)/tutors/[id]/page.tsx +++ b/apps/sale/app/(guest)/tutors/[id]/page.tsx @@ -1,6 +1,5 @@ import type { Metadata } from "next"; import { Footer } from '@workspace/ui/components/layout/Footer'; -import { Header } from '@workspace/ui/components/layout/Header'; import { TutorProfilePage } from '@/features/tutor-profile/components/TutorProfilePage'; export const metadata: Metadata = { @@ -12,11 +11,11 @@ export const metadata: Metadata = { export default function TutorProfileRoutePage() { return (
-
-
+
); } + diff --git a/apps/sale/features/tutor-profile-editor/components/TutorEvidenceEditorPreview.tsx b/apps/sale/features/tutor-profile-editor/components/TutorEvidenceEditorPreview.tsx index 04cd0af..80e1d16 100644 --- a/apps/sale/features/tutor-profile-editor/components/TutorEvidenceEditorPreview.tsx +++ b/apps/sale/features/tutor-profile-editor/components/TutorEvidenceEditorPreview.tsx @@ -1,12 +1,11 @@ import Image from "next/image"; -import { BadgeCheck } from "lucide-react"; import type { CertificateItem } from "../../tutor-profile/types/mockTutorProfile"; import { SectionShell } from "../../tutor-profile/components/TutorProfilePrimitives"; import { isLocalPreviewUrl } from "../utils/file-preview"; export function TutorEvidenceEditorPreview({ certificates }: { certificates: CertificateItem[] }) { return ( - +
{certificates.map((item, index) => (
diff --git a/apps/sale/features/tutor-profile-editor/components/TutorProfileEditorScreen.tsx b/apps/sale/features/tutor-profile-editor/components/TutorProfileEditorScreen.tsx index 6887cc2..01fa38a 100644 --- a/apps/sale/features/tutor-profile-editor/components/TutorProfileEditorScreen.tsx +++ b/apps/sale/features/tutor-profile-editor/components/TutorProfileEditorScreen.tsx @@ -2,7 +2,7 @@ import { CheckCircleIcon } from "@phosphor-icons/react"; import { TutorBioSection } from "../../tutor-profile/components/TutorBioSection"; -import { TutorEducationAchievements } from "../../tutor-profile/components/TutorEducationAchievements"; +import { TutorAchievements } from "../../tutor-profile/components/TutorAchievements"; import { TutorHero } from "../../tutor-profile/components/TutorHero"; import { TutorTeachingHistory } from "../../tutor-profile/components/TutorTeachingHistory"; import { TutorTeachingMethods } from "../../tutor-profile/components/TutorTeachingMethods"; @@ -43,8 +43,8 @@ export function TutorProfileEditorScreen() { editor.openBlock("methods")}> - editor.openBlock("academic")}> - + editor.openBlock("academic")}> + editor.openBlock("video")}> @@ -53,7 +53,7 @@ export function TutorProfileEditorScreen() { editor.openBlock("evidence")}> - +
diff --git a/apps/sale/features/tutor-profile-editor/components/TutorVideoEditorPreview.tsx b/apps/sale/features/tutor-profile-editor/components/TutorVideoEditorPreview.tsx index e7892a2..9d98ed4 100644 --- a/apps/sale/features/tutor-profile-editor/components/TutorVideoEditorPreview.tsx +++ b/apps/sale/features/tutor-profile-editor/components/TutorVideoEditorPreview.tsx @@ -1,13 +1,10 @@ -import { Video } from "lucide-react"; import { SectionShell } from "../../tutor-profile/components/TutorProfilePrimitives"; export function TutorVideoEditorPreview({ src }: { src: string }) { return (