diff --git a/apps/sale/app/community/applications/page.tsx b/apps/sale/app/community/applications/page.tsx
new file mode 100644
index 0000000..c1bafc7
--- /dev/null
+++ b/apps/sale/app/community/applications/page.tsx
@@ -0,0 +1,133 @@
+"use client";
+
+import { useCommunityStore } from "../../../features/community/store/community-store";
+import { ApplicationCard } from "../../../features/community/components/ApplicationCard";
+import { EmptyState } from "@workspace/ui/components/ui/empty-state";
+import Link from "next/link";
+import { Button } from "@workspace/ui/components/ui/button";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import { Inbox, Briefcase } from "lucide-react";
+import { useToastStore } from "@workspace/ui/components/ui/bee-toast/useToastStore";
+
+export default function ApplicationsPage() {
+ const { posts, currentUser, acceptApplication } = useCommunityStore();
+ const { add } = useToastStore();
+ const router = useRouter();
+
+ // If GUEST, redirect back
+ useEffect(() => {
+ if (currentUser.role === "GUEST") {
+ router.push("/community");
+ }
+ }, [currentUser.role, router]);
+
+ if (currentUser.role === "GUEST") {
+ return null;
+ }
+
+ const isLearner = currentUser.role === "LEARNER";
+
+ // Prepare list of { application, post }
+ const displayApplications: { application: any; post: any }[] = [];
+
+ if (isLearner) {
+ posts.forEach((post) => {
+ if (post.author.id === currentUser.id && post.applications.length > 0) {
+ post.applications.forEach((app) => {
+ displayApplications.push({ application: app, post });
+ });
+ }
+ });
+ // Sort applications by newest first (descending)
+ displayApplications.sort(
+ (a, b) =>
+ new Date(b.application.createdAt).getTime() -
+ new Date(a.application.createdAt).getTime(),
+ );
+ } else {
+ posts.forEach((post) => {
+ const myApp = post.applications.find(
+ (app) => app.tutor.id === currentUser.id,
+ );
+ if (myApp) {
+ displayApplications.push({ application: myApp, post });
+ }
+ });
+ // Sort applications by newest first
+ displayApplications.sort(
+ (a, b) =>
+ new Date(b.application.createdAt).getTime() -
+ new Date(a.application.createdAt).getTime(),
+ );
+ }
+
+ const handleAccept = (applicationId: string, postId: string) => {
+ acceptApplication(postId, applicationId);
+ add({
+ title: "Đã kết nối thành công!",
+ description:
+ "Đang chuyển hướng vào Phòng Chat 3 bên (Học viên - Gia sư - Cố vấn)...",
+ variant: "success",
+ });
+ };
+
+ return (
+
+
+
+ {isLearner ? (
+ <>
+
+ Gia sư yêu cầu kết nối
+ >
+ ) : (
+ <>
+
+ Hồ sơ đã gửi
+ >
+ )}
+
+
+ {isLearner
+ ? "Danh sách các hồ sơ gia sư đã gửi đến bài đăng của bạn."
+ : "Theo dõi trạng thái các hồ sơ bạn đã gửi đến học viên."}
+
+
+
+
+ {displayApplications.length === 0 ? (
+
+
+
+ }
+ />
+ ) : (
+ displayApplications.map(({ application, post }) => (
+ handleAccept(appId, post.id) : undefined
+ }
+ />
+ ))
+ )}
+
+
+ );
+}
diff --git a/apps/sale/app/community/layout.tsx b/apps/sale/app/community/layout.tsx
new file mode 100644
index 0000000..bd93cbc
--- /dev/null
+++ b/apps/sale/app/community/layout.tsx
@@ -0,0 +1,15 @@
+import { ReactNode } from "react";
+import { RoleSwitcherMockBar } from "../../features/community/components/RoleSwitcherMockBar";
+import { CommunityBottomBar } from "../../features/community/components/CommunityBottomBar";
+
+export default function CommunityLayout({ children }: { children: ReactNode }) {
+ return (
+
+ );
+}
diff --git a/apps/sale/app/community/my-posts/page.tsx b/apps/sale/app/community/my-posts/page.tsx
new file mode 100644
index 0000000..b603422
--- /dev/null
+++ b/apps/sale/app/community/my-posts/page.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { useCommunityStore } from "../../../features/community/store/community-store";
+import { PostCard } from "../../../features/community/components/PostCard";
+import { EmptyState } from "@workspace/ui/components/ui/empty-state";
+import { PlusCircle } from "lucide-react";
+import Link from "next/link";
+import { Button } from "@workspace/ui/components/ui/button";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+export default function MyPostsPage() {
+ const { posts, currentUser } = useCommunityStore();
+ const router = useRouter();
+
+ // If not a learner, redirect back
+ useEffect(() => {
+ if (currentUser.role !== "LEARNER") {
+ router.push("/community");
+ }
+ }, [currentUser.role, router]);
+
+ const myPosts = posts.filter(post => post.author.id === currentUser.id);
+
+ if (currentUser.role !== "LEARNER") {
+ return null;
+ }
+
+ return (
+
+
+
Bài đăng của tôi
+
+
+
+
+
+
+ {myPosts.length === 0 ? (
+
+
+
+ }
+ />
+ ) : (
+ myPosts.map(post => (
+
+ ))
+ )}
+
+
+ );
+}
diff --git a/apps/sale/app/community/page.tsx b/apps/sale/app/community/page.tsx
new file mode 100644
index 0000000..60b500e
--- /dev/null
+++ b/apps/sale/app/community/page.tsx
@@ -0,0 +1,135 @@
+"use client";
+
+import { FilterSidebar } from "../../features/community/components/FilterSidebar";
+import { PostCreateBox } from "../../features/community/components/PostCreateBox";
+import { InfiniteFeedList } from "../../features/community/components/InfiniteFeedList";
+import { useCommunityStore } from "../../features/community/store/community-store";
+import { Award, BookOpen } from "lucide-react";
+
+export default function CommunityPage() {
+ const { currentUser, posts } = useCommunityStore();
+ const myPostsCount = posts.filter(
+ (p) => p.author.id === currentUser.id,
+ ).length;
+
+ return (
+
+ {/* Tiêu đề trang */}
+
+
+ Cộng đồng tìm gia sư
+
+
+ Kết nối nhanh chóng với hàng ngàn gia sư chất lượng cao
+
+
+
+
+ {/* Cột trái: Bộ lọc (Desktop) */}
+
+
+ {/* Cột giữa: Feed chính */}
+
+
+
+ {/* Bộ lọc trên Mobile (Có thể mở rộng thành Drawer sau, tạm thời hiện inline) */}
+
+
+
+
+
+
+
+ {/* Cột phải: Widget phụ trợ */}
+
+
+ {/* Widget: Bài đăng của tôi (Chỉ Learner mới thấy có ý nghĩa) */}
+ {currentUser.role === "LEARNER" && (
+
+
+
+ Quản lý bài đăng
+
+
+ Bài đăng của tôi:
+
+ {myPostsCount} bài
+
+
+ {myPostsCount > 0 && (
+
+ )}
+
+ )}
+
+ {/* Widget: Gia sư tiêu biểu */}
+
+
+
+ Gia sư tiêu biểu tuần
+
+
+
+
+

+
+
+ Trần Lê Huy
+
+
+ IELTS 8.0 • 50+ giờ dạy
+
+
+
+
+

+
+
+ Nguyễn Mai Hoa
+
+
+ Toán LTĐH • Đánh giá 5.0
+
+
+
+
+

+
+
+ Phạm Văn A
+
+
+ Lập trình cơ bản • 30+ hs
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/sale/features/community/components/ApplicantQueueDrawer.tsx b/apps/sale/features/community/components/ApplicantQueueDrawer.tsx
new file mode 100644
index 0000000..95820ce
--- /dev/null
+++ b/apps/sale/features/community/components/ApplicantQueueDrawer.tsx
@@ -0,0 +1,94 @@
+"use client";
+
+import { CommunityPost, TutorApplication } from "../types/community";
+import { useCommunityStore } from "../store/community-store";
+import { useToastStore } from "@workspace/ui/components/ui/bee-toast/useToastStore";
+import { Button } from "@workspace/ui/components/ui/button";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@workspace/ui/components/ui/avatar";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetDescription,
+} from "@workspace/ui/components/ui/sheet";
+import { CheckCircle2, Clock, DollarSign, Quote } from "lucide-react";
+import { formatDistanceToNow } from "date-fns";
+import { vi } from "date-fns/locale";
+import { ApplicationCard } from "./ApplicationCard";
+
+interface ApplicantQueueDrawerProps {
+ post: CommunityPost;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export function ApplicantQueueDrawer({
+ post,
+ open,
+ onOpenChange,
+}: ApplicantQueueDrawerProps) {
+ const { acceptApplication } = useCommunityStore();
+ const { add } = useToastStore();
+
+ const handleAccept = (applicationId: string) => {
+ acceptApplication(post.id, applicationId);
+ add({
+ title: "Đã kết nối thành công!",
+ description:
+ "Đang chuyển hướng vào Phòng Chat 3 bên (Học viên - Gia sư - Cố vấn)...",
+ variant: "success",
+ });
+ onOpenChange(false);
+ };
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat("vi-VN", {
+ style: "currency",
+ currency: "VND",
+ }).format(amount);
+ };
+
+ return (
+
+
+
+ Danh sách hồ sơ ({post.applications.length})
+
+ Xem hồ sơ các gia sư đã ứng tuyển và chọn người phù hợp nhất cho lớp{" "}
+ {post.subject}
+ .
+
+
+
+ {post.applications.length === 0 ? (
+
+
+
+
+
+ Chưa có hồ sơ nào.
+
+ Vui lòng chờ thêm một thời gian nữa nhé!
+
+
+ ) : (
+
+ {post.applications.map((app: TutorApplication) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/apps/sale/features/community/components/ApplicationCard.tsx b/apps/sale/features/community/components/ApplicationCard.tsx
new file mode 100644
index 0000000..b50e259
--- /dev/null
+++ b/apps/sale/features/community/components/ApplicationCard.tsx
@@ -0,0 +1,129 @@
+import { CommunityPost, TutorApplication } from "../types/community";
+import { Avatar, AvatarFallback, AvatarImage } from "@workspace/ui/components/ui/avatar";
+import { Clock, DollarSign, Quote, CheckCircle2, BookOpen } from "lucide-react";
+import { formatDistanceToNow } from "date-fns";
+import { vi } from "date-fns/locale";
+import { Button } from "@workspace/ui/components/ui/button";
+import Link from "next/link";
+
+interface ApplicationCardProps {
+ application: TutorApplication;
+ post: CommunityPost;
+ onAccept?: (applicationId: string) => void;
+ showPostInfo?: boolean;
+}
+
+export function ApplicationCard({ application: app, post, onAccept, showPostInfo }: ApplicationCardProps) {
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat("vi-VN", {
+ style: "currency",
+ currency: "VND",
+ }).format(amount);
+ };
+
+ return (
+
+
+
+
+
+ {app.tutor.displayName?.charAt(0) || "U"}
+
+
+
+
+
+
+ {app.tutor.displayName}
+
+
+
+ {formatDistanceToNow(new Date(app.createdAt), {
+ addSuffix: true,
+ locale: vi,
+ })}
+
+
+ {app.status === "ACCEPTED" && (
+
+ Đã chọn
+
+ )}
+ {app.status === "PENDING" && (
+
+ Đang chờ
+
+ )}
+
+
+
+
+ {showPostInfo && (
+
+
+
+ Ứng tuyển cho bài đăng: {post.subject}
+
+
+ )}
+
+
+
+
+ "{app.pitchNote}"
+
+
+
+
+
+
+
+
+
+
+ Thù lao đề xuất
+
+
+ {formatCurrency(app.proposedRate)}
+
+
+
+
+
+
+
+
+
+ Lịch trống
+
+
+ {app.availableSlots.length > 0
+ ? app.availableSlots.join(", ")
+ : "Chưa cập nhật"}
+
+
+
+
+
+
+
+
+
+ {onAccept && post.status === "OPEN" && app.status === "PENDING" && (
+
+ )}
+
+
+ );
+}
diff --git a/apps/sale/features/community/components/CommunityBottomBar.tsx b/apps/sale/features/community/components/CommunityBottomBar.tsx
new file mode 100644
index 0000000..840fc34
--- /dev/null
+++ b/apps/sale/features/community/components/CommunityBottomBar.tsx
@@ -0,0 +1,101 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { motion, AnimatePresence } from "motion/react";
+import {
+ Home,
+ ClipboardList,
+ Briefcase,
+ ChevronRight,
+ Menu,
+} from "lucide-react";
+import { useCommunityStore } from "../store/community-store";
+
+export function CommunityBottomBar() {
+ const [isExpanded, setIsExpanded] = useState(true);
+ const pathname = usePathname();
+ const { currentUser } = useCommunityStore();
+
+ const navItems = [
+ {
+ label: "Trang chủ",
+ href: "/community",
+ icon: ,
+ show: true,
+ },
+ {
+ label: "Bài đăng của tôi",
+ href: "/community/my-posts",
+ icon: ,
+ show: currentUser.role === "LEARNER",
+ },
+ {
+ label: "Quản lý kết nối",
+ href: "/community/applications",
+ icon: ,
+ show: currentUser.role === "LEARNER" || currentUser.role === "TUTOR",
+ },
+ ].filter((item) => item.show);
+
+ if (currentUser.role === "GUEST") {
+ return null; // Do not show bottom bar for guests
+ }
+
+ return (
+
+
+ {isExpanded ? (
+
+ {navItems.map((item) => {
+ const isActive = pathname === item.href;
+ return (
+
+ {item.icon}
+ {item.label}
+
+ );
+ })}
+
+
+
+ ) : (
+ setIsExpanded(true)}
+ className="bg-white/60 backdrop-blur-xl text-primary border border-white/40 p-4 rounded-full shadow-[0_8px_32px_rgba(0,0,0,0.1)] hover:scale-105 transition-transform"
+ title="Mở menu cộng đồng"
+ >
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/sale/features/community/components/FilterSidebar.tsx b/apps/sale/features/community/components/FilterSidebar.tsx
new file mode 100644
index 0000000..b9935ed
--- /dev/null
+++ b/apps/sale/features/community/components/FilterSidebar.tsx
@@ -0,0 +1,120 @@
+"use client";
+
+import { useCommunityStore } from "../store/community-store";
+import { Search, RotateCcw, ShieldAlert } from "lucide-react";
+import { Button } from "@workspace/ui/components/ui/button";
+import { Input } from "@workspace/ui/components/ui/input";
+import { TeachingMode } from "../types/community";
+
+export function FilterSidebar() {
+ const { filters, setFilters } = useCommunityStore();
+
+ const handleReset = () => {
+ setFilters({
+ searchQuery: "",
+ subject: "",
+ gradeLevel: "",
+ teachingMode: "ALL",
+ district: "",
+ minBudget: undefined,
+ maxBudget: undefined,
+ });
+ };
+
+ return (
+
+
+
Bộ lọc tìm kiếm
+
+
+ setFilters({ searchQuery: e.target.value })}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Quy tắc cộng đồng
+
+ Vui lòng không chia sẻ SĐT/Zalo trong bài đăng để bảo mật thông tin. Mọi tương tác sẽ được kết nối an toàn qua nền tảng.
+
+
+
+
+
+ );
+}
diff --git a/apps/sale/features/community/components/InfiniteFeedList.tsx b/apps/sale/features/community/components/InfiniteFeedList.tsx
new file mode 100644
index 0000000..fbf9e5a
--- /dev/null
+++ b/apps/sale/features/community/components/InfiniteFeedList.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { useCommunityStore } from "../store/community-store";
+import { PostCard } from "./PostCard";
+import { Loader2 } from "lucide-react";
+import { useEffect, useState } from "react";
+import { EmptyState } from "@workspace/ui/components/ui/empty-state";
+
+export function InfiniteFeedList() {
+ const { posts, filters } = useCommunityStore();
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Giả lập loading lần đầu
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setIsLoading(false);
+ }, 800);
+ return () => clearTimeout(timer);
+ }, []);
+
+ const filteredPosts = posts.filter(post => {
+ // Lọc theo search query
+ if (filters.searchQuery) {
+ const q = filters.searchQuery.toLowerCase();
+ const matchSearch =
+ post.content.toLowerCase().includes(q) ||
+ post.subject.toLowerCase().includes(q) ||
+ (post.district && post.district.toLowerCase().includes(q));
+ if (!matchSearch) return false;
+ }
+
+ // Lọc theo môn học
+ if (filters.subject && post.subject !== filters.subject) return false;
+
+ // Lọc theo hình thức học
+ if (filters.teachingMode && filters.teachingMode !== "ALL" && post.teachingMode !== filters.teachingMode) return false;
+
+ // Lọc theo ngân sách
+ if (filters.minBudget && post.budgetPerSession < filters.minBudget) return false;
+ if (filters.maxBudget && post.budgetPerSession > filters.maxBudget) return false;
+
+ return true;
+ });
+
+ if (isLoading) {
+ return (
+
+
+
Đang tải danh sách bài đăng...
+
+ );
+ }
+
+ if (filteredPosts.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {filteredPosts.map(post => (
+
+ ))}
+
+ {/* Mock Infinite Scroll Loader */}
+ {filteredPosts.length > 0 && (
+
+
+ Đang tải thêm...
+
+ )}
+
+ );
+}
diff --git a/apps/sale/features/community/components/PostCard.tsx b/apps/sale/features/community/components/PostCard.tsx
new file mode 100644
index 0000000..7809d86
--- /dev/null
+++ b/apps/sale/features/community/components/PostCard.tsx
@@ -0,0 +1,293 @@
+"use client";
+
+import { useState } from "react";
+import { CommunityPost } from "../types/community";
+import { useCommunityStore } from "../store/community-store";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@workspace/ui/components/ui/avatar";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@workspace/ui/components/ui/dropdown-menu";
+import { Button } from "@workspace/ui/components/ui/button";
+import {
+ BookOpen,
+ GraduationCap,
+ MapPin,
+ Video,
+ Users,
+ CheckCircle2,
+ MoreVertical,
+ Edit,
+ Trash2,
+} from "lucide-react";
+import { formatDistanceToNow } from "date-fns";
+import { vi } from "date-fns/locale";
+import { TutorApplyModal } from "./TutorApplyModal";
+import { ApplicantQueueDrawer } from "./ApplicantQueueDrawer";
+import { PostEditModal } from "./PostEditModal";
+import { useToastStore } from "@workspace/ui/components/ui/bee-toast/useToastStore";
+
+interface PostCardProps {
+ post: CommunityPost;
+}
+
+export function PostCard({ post }: PostCardProps) {
+ const { currentUser, deletePost } = useCommunityStore();
+ const [isApplyModalOpen, setIsApplyModalOpen] = useState(false);
+ const [isQueueDrawerOpen, setIsQueueDrawerOpen] = useState(false);
+ const [isEditModalOpen, setIsEditModalOpen] = useState(false);
+ const { add } = useToastStore();
+
+ const isAuthor = currentUser.id === post.author.id;
+ const isTutor = currentUser.role === "TUTOR";
+ const isGuest = currentUser.role === "GUEST";
+
+ const hasApplied = post.applications.some(
+ (app) => app.tutor.id === currentUser.id,
+ );
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat("vi-VN", {
+ style: "currency",
+ currency: "VND",
+ }).format(amount);
+ };
+
+ const handleActionClick = () => {
+ if (isGuest) {
+ add({
+ title: "Yêu cầu đăng nhập",
+ description: "Vui lòng đăng nhập để thực hiện chức năng này.",
+ variant: "warning",
+ });
+ return;
+ }
+
+ if (isTutor) {
+ setIsApplyModalOpen(true);
+ } else if (isAuthor) {
+ setIsQueueDrawerOpen(true);
+ }
+ };
+
+ const handleDelete = () => {
+ if (window.confirm("Bạn có chắc chắn muốn xóa bài đăng này?")) {
+ deletePost(post.id);
+ add({
+ title: "Đã xóa",
+ description: "Bài đăng đã được xóa thành công.",
+ variant: "success",
+ });
+ }
+ };
+
+ const renderStatusBadge = () => {
+ switch (post.status) {
+ case "OPEN":
+ return (
+
+ Đang tìm gia sư
+
+ );
+ case "IN_SESSION":
+ return (
+
+ Đã kết nối
+
+ );
+ case "CLOSED":
+ return (
+
+ Đã đóng
+
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+ <>
+
+ {/* Header */}
+
+
+
+
+
+ {post.author.displayName?.charAt(0) || "U"}
+
+
+
+
+ {post.author.displayName}
+
+
+
+ {formatDistanceToNow(new Date(post.createdAt), {
+ addSuffix: true,
+ locale: vi,
+ })}
+
+ {post.city && post.teachingMode !== "ONLINE" && (
+ <>
+
+
+
+ {post.city}
+
+ >
+ )}
+
+
+
+
+ {renderStatusBadge()}
+ {isAuthor && (
+
+
+
+
+
+ setIsEditModalOpen(true)}>
+
+ Sửa bài đăng
+
+
+
+ Xóa bài đăng
+
+
+
+ )}
+
+
+
+ {/* Content */}
+
+ {post.content}
+
+
+ {/* Images if any */}
+ {post.imageUrls && post.imageUrls.length > 0 && (
+
1 ? "grid-cols-2" : "grid-cols-1"}`}
+ >
+ {post.imageUrls.map((url, index) => (
+

+ ))}
+
+ )}
+
+ {/* Tags */}
+
+
+ {post.subject}
+
+
+ Lớp {post.gradeLevel}
+
+
+ {post.teachingMode === "ONLINE" ? (
+
+ ) : (
+
+ )}
+ {post.teachingMode === "ONLINE"
+ ? "Online"
+ : post.teachingMode === "HYBRID"
+ ? "Linh hoạt"
+ : "Tại nhà"}
+
+ {(post.district || post.city) && post.teachingMode !== "ONLINE" && (
+
+ {post.district}
+ {post.district && post.city ? ", " : ""}
+ {post.city}
+
+ )}
+
+ {formatCurrency(post.budgetPerSession)}/buổi
+
+
+
+ {/* Footer Actions */}
+
+
+
+ {post.applicationsCount} hồ sơ đã gửi
+
+
+
+ {isAuthor && (
+
+ )}
+
+ {!isAuthor && (isTutor || isGuest) && (
+
+ )}
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/sale/features/community/components/PostCreateBox.tsx b/apps/sale/features/community/components/PostCreateBox.tsx
new file mode 100644
index 0000000..888bf09
--- /dev/null
+++ b/apps/sale/features/community/components/PostCreateBox.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { useState } from "react";
+import { useCommunityStore } from "../store/community-store";
+import { Avatar, AvatarFallback, AvatarImage } from "@workspace/ui/components/ui/avatar";
+import { AlertCircle } from "lucide-react";
+import { Button } from "@workspace/ui/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@workspace/ui/components/ui/dialog";
+import { PostCreateModal } from "./PostCreateModal";
+
+export function PostCreateBox() {
+ const { currentUser } = useCommunityStore();
+ const [showActiveSessionWarning, setShowActiveSessionWarning] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ if (currentUser.role !== "LEARNER") {
+ return null; // Only Learner can see this box
+ }
+
+ const handleFocus = () => {
+ if (currentUser.hasActiveLearnerClass) {
+ setShowActiveSessionWarning(true);
+ } else {
+ setIsModalOpen(true);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ {currentUser.displayName?.charAt(0) || "U"}
+
+
+
+ Bạn cần tìm gia sư môn gì, mục tiêu học tập ra sao?
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/sale/features/community/components/PostCreateModal.tsx b/apps/sale/features/community/components/PostCreateModal.tsx
new file mode 100644
index 0000000..b7f6b2a
--- /dev/null
+++ b/apps/sale/features/community/components/PostCreateModal.tsx
@@ -0,0 +1,226 @@
+"use client";
+
+import { useState } from "react";
+import { useCommunityStore } from "../store/community-store";
+import { Button } from "@workspace/ui/components/ui/button";
+import {
+ BookOpen,
+ GraduationCap,
+ MapPin,
+ Video,
+ DollarSign,
+ ImagePlus,
+ Send,
+ Loader2,
+} from "lucide-react";
+import { TeachingMode } from "../types/community";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from "@workspace/ui/components/ui/dialog";
+import { provinces } from "tinhthanhvn";
+import { useToastStore } from "@workspace/ui/components/ui/bee-toast/useToastStore";
+
+interface PostCreateModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export function PostCreateModal({ open, onOpenChange }: PostCreateModalProps) {
+ const { createPost } = useCommunityStore();
+ const { add } = useToastStore();
+
+ const [content, setContent] = useState("");
+ const [subject, setSubject] = useState("");
+ const [gradeLevel, setGradeLevel] = useState("");
+ const [teachingMode, setTeachingMode] = useState("ONLINE");
+ const [budgetPerSession, setBudgetPerSession] = useState("");
+ const [city, setCity] = useState("");
+
+ const provinceList = provinces.all(); // usually returns an array of { code, name, ... }
+
+ const handleSubmit = () => {
+ // Only content and subject are strictly required. Others are optional or have defaults.
+ if (!content.trim() || !subject) return;
+
+ createPost({
+ content,
+ imageUrls: [],
+ subject,
+ gradeLevel: gradeLevel || "Khác",
+ teachingMode,
+ budgetPerSession: budgetPerSession ? parseInt(budgetPerSession) : 0,
+ city: city || undefined,
+ });
+
+ add({
+ title: "Đăng bài thành công",
+ description: "Bài đăng của bạn đã được xuất bản.",
+ variant: "success",
+ });
+
+ // Reset
+ setContent("");
+ setSubject("");
+ setGradeLevel("");
+ setBudgetPerSession("");
+ setCity("");
+ onOpenChange(false);
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/sale/features/community/components/PostEditModal.tsx b/apps/sale/features/community/components/PostEditModal.tsx
new file mode 100644
index 0000000..6966a4d
--- /dev/null
+++ b/apps/sale/features/community/components/PostEditModal.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { useState } from "react";
+import { CommunityPost } from "../types/community";
+import { useCommunityStore } from "../store/community-store";
+import { useToastStore } from "@workspace/ui/components/ui/bee-toast/useToastStore";
+import { Button } from "@workspace/ui/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from "@workspace/ui/components/ui/dialog";
+import { Input } from "@workspace/ui/components/ui/input";
+
+interface PostEditModalProps {
+ post: CommunityPost;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export function PostEditModal({ post, open, onOpenChange }: PostEditModalProps) {
+ const { updatePost } = useCommunityStore();
+ const { add } = useToastStore();
+
+ const [subject, setSubject] = useState(post.subject);
+ const [content, setContent] = useState(post.content);
+ const [budgetPerSession, setBudgetPerSession] = useState(post.budgetPerSession.toString());
+
+ const handleUpdate = () => {
+ if (!subject.trim() || !content.trim() || !budgetPerSession) return;
+
+ updatePost(post.id, {
+ subject,
+ content,
+ budgetPerSession: parseInt(budgetPerSession),
+ });
+
+ add({
+ title: "Cập nhật thành công",
+ description: "Bài đăng của bạn đã được cập nhật.",
+ variant: "success",
+ });
+
+ onOpenChange(false);
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/sale/features/community/components/RoleSwitcherMockBar.tsx b/apps/sale/features/community/components/RoleSwitcherMockBar.tsx
new file mode 100644
index 0000000..fb0a54e
--- /dev/null
+++ b/apps/sale/features/community/components/RoleSwitcherMockBar.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { useCommunityStore } from "../store/community-store";
+import {
+ mockGuest,
+ mockLearnerNoSession,
+ mockLearnerWithActiveSession,
+ mockTutor,
+} from "../lib/mock-community-data";
+import { UserProfile } from "../types/community";
+import { Button } from "@workspace/ui/components/ui/button";
+
+export function RoleSwitcherMockBar() {
+ const { currentUser, setCurrentUser } = useCommunityStore();
+
+ const handleSwitchRole = (user: UserProfile) => {
+ setCurrentUser(user);
+ };
+
+ return (
+
+
+
+ Role Tester:
+
+ {currentUser.displayName} ({currentUser.role})
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/sale/features/community/components/TutorApplyModal.tsx b/apps/sale/features/community/components/TutorApplyModal.tsx
new file mode 100644
index 0000000..aaec919
--- /dev/null
+++ b/apps/sale/features/community/components/TutorApplyModal.tsx
@@ -0,0 +1,127 @@
+"use client";
+
+import { useState } from "react";
+import { CommunityPost } from "../types/community";
+import { useCommunityStore } from "../store/community-store";
+import { useToastStore } from "@workspace/ui/components/ui/bee-toast/useToastStore";
+import { Button } from "@workspace/ui/components/ui/button";
+import { Input } from "@workspace/ui/components/ui/input";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@workspace/ui/components/ui/dialog";
+import { Avatar, AvatarFallback, AvatarImage } from "@workspace/ui/components/ui/avatar";
+
+interface TutorApplyModalProps {
+ post: CommunityPost;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export function TutorApplyModal({ post, open, onOpenChange }: TutorApplyModalProps) {
+ const { currentUser, applyForPost } = useCommunityStore();
+ const { add } = useToastStore();
+
+ const [pitchNote, setPitchNote] = useState("");
+ const [proposedRate, setProposedRate] = useState(post.budgetPerSession.toString());
+ const [availableSlots, setAvailableSlots] = useState("");
+
+ const handleSubmit = () => {
+ if (!pitchNote.trim() || !proposedRate) return;
+
+ applyForPost(
+ post.id,
+ pitchNote,
+ parseInt(proposedRate),
+ availableSlots.split(",").map(s => s.trim()).filter(Boolean)
+ );
+
+ add({
+ title: "Đã gửi hồ sơ",
+ description: "Hồ sơ của bạn đã được gửi đến học viên.",
+ variant: "success",
+ });
+
+ onOpenChange(false);
+
+ // Reset form
+ setPitchNote("");
+ setProposedRate(post.budgetPerSession.toString());
+ setAvailableSlots("");
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/sale/features/community/lib/mock-community-data.ts b/apps/sale/features/community/lib/mock-community-data.ts
new file mode 100644
index 0000000..20d4bc8
--- /dev/null
+++ b/apps/sale/features/community/lib/mock-community-data.ts
@@ -0,0 +1,191 @@
+import { CommunityPost, UserProfile } from "../types/community";
+
+export const mockGuest: UserProfile = {
+ id: "guest-1",
+ email: null,
+ firstName: "Khách",
+ lastName: "Vô danh",
+ fullName: "Khách",
+ phoneNumber: null,
+ role: "GUEST",
+ status: "ACTIVE",
+ permissions: [],
+ displayName: "Khách",
+};
+
+export const mockLearnerNoSession: UserProfile = {
+ id: "learner-1",
+ email: "learner1@example.com",
+ firstName: "Nguyễn",
+ lastName: "An",
+ fullName: "Nguyễn An",
+ phoneNumber: "0901234567",
+ role: "LEARNER",
+ status: "ACTIVE",
+ permissions: ["CREATE_POST"],
+ hasActiveLearnerClass: false,
+ displayName: "Nguyễn An",
+ avatarUrl: "https://i.pravatar.cc/150?u=learner-1",
+};
+
+export const mockLearnerWithActiveSession: UserProfile = {
+ id: "learner-2",
+ email: "learner2@example.com",
+ firstName: "Trần",
+ lastName: "Bình",
+ fullName: "Trần Bình",
+ phoneNumber: "0912345678",
+ role: "LEARNER",
+ status: "ACTIVE",
+ permissions: ["CREATE_POST"],
+ hasActiveLearnerClass: true, // This maps to "active session" for our mock
+ displayName: "Trần Bình",
+ avatarUrl: "https://i.pravatar.cc/150?u=learner-2",
+};
+
+export const mockTutor: UserProfile = {
+ id: "tutor-1",
+ email: "tutor1@example.com",
+ firstName: "Lê",
+ lastName: "Cường",
+ fullName: "Lê Cường",
+ phoneNumber: "0923456789",
+ role: "TUTOR",
+ status: "ACTIVE",
+ permissions: ["APPLY_POST"],
+ tutorProfileId: "tutor-profile-1",
+ displayName: "Gia sư Lê Cường",
+ avatarUrl: "https://i.pravatar.cc/150?u=tutor-1",
+};
+
+export const mockTutor2: UserProfile = {
+ id: "tutor-2",
+ email: "tutor2@example.com",
+ firstName: "Phạm",
+ lastName: "Dung",
+ fullName: "Phạm Dung",
+ phoneNumber: "0934567890",
+ role: "TUTOR",
+ status: "ACTIVE",
+ permissions: ["APPLY_POST"],
+ tutorProfileId: "tutor-profile-2",
+ displayName: "Gia sư Phạm Dung",
+ avatarUrl: "https://i.pravatar.cc/150?u=tutor-2",
+};
+
+export const initialMockPosts: CommunityPost[] = [
+ {
+ id: "post-1",
+ author: mockLearnerNoSession,
+ content: "Mình cần tìm gia sư môn Toán lớp 12 luyện thi đại học, ưu tiên các bạn có kinh nghiệm luyện thi khối A. Khu vực Quận 10.",
+ imageUrls: [],
+ subject: "Toán",
+ gradeLevel: "12",
+ teachingMode: "OFFLINE",
+ city: "Hồ Chí Minh",
+ district: "Quận 10",
+ budgetPerSession: 250000,
+ status: "OPEN",
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(), // 2 hours ago
+ applicationsCount: 3,
+ applications: [
+ {
+ id: "app-1",
+ postId: "post-1",
+ tutor: mockTutor,
+ pitchNote: "Chào bạn, mình có 3 năm kinh nghiệm luyện thi đại học khối A, điểm số học sinh trung bình 8+.",
+ proposedRate: 250000,
+ availableSlots: ["Thứ 2,4,6 tối"],
+ createdAt: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
+ status: "PENDING",
+ }
+ ],
+ },
+ {
+ id: "post-2",
+ author: mockLearnerWithActiveSession,
+ content: "Cần tìm giáo viên dạy tiếng Anh giao tiếp cho bé lớp 5. Học online qua Zoom, cần kiên nhẫn, vui vẻ.",
+ imageUrls: ["https://placehold.co/600x400/png?text=English+Class"],
+ subject: "Tiếng Anh",
+ gradeLevel: "5",
+ teachingMode: "ONLINE",
+ budgetPerSession: 200000,
+ status: "OPEN",
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5).toISOString(),
+ applicationsCount: 5,
+ applications: [],
+ },
+ {
+ id: "post-3",
+ author: { ...mockLearnerNoSession, id: "learner-3", displayName: "Phụ huynh bé Hoa" },
+ content: "Tìm gia sư Hóa học 9 ôn thi vào lớp 10 chuyên Hóa. Yêu cầu sinh viên Sư Phạm Hóa hoặc KHTN. Dạy tại Cầu Giấy.",
+ imageUrls: [],
+ subject: "Hóa học",
+ gradeLevel: "9",
+ teachingMode: "OFFLINE",
+ city: "Hà Nội",
+ district: "Quận Cầu Giấy",
+ budgetPerSession: 300000,
+ status: "IN_SESSION",
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
+ applicationsCount: 1,
+ applications: [
+ {
+ id: "app-2",
+ postId: "post-3",
+ tutor: mockTutor2,
+ pitchNote: "Đã đạt giải QG Hóa, thủ khoa đầu vào SP Hóa.",
+ proposedRate: 300000,
+ availableSlots: ["Thứ 7, CN"],
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 20).toISOString(),
+ status: "ACCEPTED",
+ }
+ ],
+ },
+ {
+ id: "post-4",
+ author: { ...mockLearnerNoSession, id: "learner-4", displayName: "Hoàng Minh" },
+ content: "Mình cần người hướng dẫn IELTS Speaking & Writing mục tiêu 6.5. Đã thi được 5.5 cách đây 1 năm.",
+ imageUrls: [],
+ subject: "IELTS",
+ gradeLevel: "Khác",
+ teachingMode: "ONLINE",
+ budgetPerSession: 300000,
+ status: "OPEN",
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
+ applicationsCount: 0,
+ applications: [],
+ },
+ {
+ id: "post-5",
+ author: { ...mockLearnerNoSession, id: "learner-5", displayName: "Trương Vô Kỵ" },
+ content: "Bé nhà mình bị mất gốc Vật lý 10, cần gia sư đến kèm tại nhà để củng cố kiến thức trên lớp. Nhà ở Hải Châu.",
+ imageUrls: [],
+ subject: "Vật lý",
+ gradeLevel: "10",
+ teachingMode: "OFFLINE",
+ city: "Đà Nẵng",
+ district: "Quận Hải Châu",
+ budgetPerSession: 180000,
+ status: "OPEN",
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 72).toISOString(),
+ applicationsCount: 0,
+ applications: [],
+ },
+ {
+ id: "post-6",
+ author: { ...mockLearnerNoSession, id: "learner-6", displayName: "Ngô Quyền" },
+ content: "Tìm sinh viên Sư phạm Văn dạy kèm cho em gái lớp 8 môn Ngữ Văn.",
+ imageUrls: [],
+ subject: "Ngữ Văn",
+ gradeLevel: "8",
+ teachingMode: "HYBRID",
+ city: "Hồ Chí Minh",
+ district: "Quận Gò Vấp",
+ budgetPerSession: 220000,
+ status: "CLOSED",
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 120).toISOString(),
+ applicationsCount: 2,
+ applications: [],
+ }
+];
diff --git a/apps/sale/features/community/store/community-store.ts b/apps/sale/features/community/store/community-store.ts
new file mode 100644
index 0000000..1adfb6a
--- /dev/null
+++ b/apps/sale/features/community/store/community-store.ts
@@ -0,0 +1,129 @@
+import { create } from "zustand";
+import {
+ CommunityPost,
+ CommunityFilters,
+ CreatePostInput,
+ TutorApplication,
+ UserProfile,
+} from "../types/community";
+import {
+ initialMockPosts,
+ mockLearnerNoSession,
+} from "../lib/mock-community-data";
+
+interface CommunityState {
+ currentUser: UserProfile;
+ posts: CommunityPost[];
+ filters: CommunityFilters;
+
+ // Actions
+ setCurrentUser: (user: UserProfile) => void;
+ setFilters: (filters: Partial) => void;
+ createPost: (input: CreatePostInput) => void;
+ applyForPost: (
+ postId: string,
+ pitchNote: string,
+ proposedRate: number,
+ availableSlots: string[],
+ ) => void;
+ acceptApplication: (postId: string, applicationId: string) => void;
+ updatePost: (postId: string, input: Partial) => void;
+ deletePost: (postId: string) => void;
+}
+
+export const useCommunityStore = create((set) => ({
+ currentUser: mockLearnerNoSession, // Default mock user
+ posts: initialMockPosts,
+ filters: {
+ searchQuery: "",
+ teachingMode: "ALL",
+ },
+
+ setCurrentUser: (user) => set({ currentUser: user }),
+
+ setFilters: (newFilters) =>
+ set((state) => ({
+ filters: { ...state.filters, ...newFilters },
+ })),
+
+ createPost: (input) =>
+ set((state) => {
+ const newPost: CommunityPost = {
+ id: `post-${Date.now()}`,
+ author: state.currentUser,
+ content: input.content,
+ imageUrls: input.imageUrls,
+ subject: input.subject,
+ gradeLevel: input.gradeLevel,
+ teachingMode: input.teachingMode,
+ city: input.city,
+ district: input.district,
+ budgetPerSession: input.budgetPerSession,
+ status: "OPEN",
+ createdAt: new Date().toISOString(),
+ applicationsCount: 0,
+ applications: [],
+ };
+ return { posts: [newPost, ...state.posts] };
+ }),
+
+ applyForPost: (postId, pitchNote, proposedRate, availableSlots) =>
+ set((state) => {
+ const newApplication: TutorApplication = {
+ id: `app-${Date.now()}`,
+ postId,
+ tutor: state.currentUser,
+ pitchNote,
+ proposedRate,
+ availableSlots,
+ createdAt: new Date().toISOString(),
+ status: "PENDING",
+ };
+
+ const updatedPosts = state.posts.map((post) => {
+ if (post.id === postId) {
+ return {
+ ...post,
+ applicationsCount: post.applicationsCount + 1,
+ applications: [newApplication, ...post.applications],
+ };
+ }
+ return post;
+ });
+
+ return { posts: updatedPosts };
+ }),
+
+ acceptApplication: (postId, applicationId) =>
+ set((state) => {
+ const updatedPosts = state.posts.map((post) => {
+ if (post.id === postId) {
+ const updatedApps = post.applications.map((app) =>
+ app.id === applicationId
+ ? { ...app, status: "ACCEPTED" as const }
+ : app,
+ );
+ return {
+ ...post,
+ status: "IN_SESSION" as const,
+ applications: updatedApps,
+ };
+ }
+ return post;
+ });
+
+ return { posts: updatedPosts };
+ }),
+
+ updatePost: (postId, input) =>
+ set((state) => ({
+ posts: state.posts.map((post) =>
+ post.id === postId ? { ...post, ...input } : post,
+ ),
+ })),
+
+ deletePost: (postId) =>
+ set((state) => ({
+ posts: state.posts.filter((post) => post.id !== postId),
+ })),
+}));
diff --git a/apps/sale/features/community/types/community.ts b/apps/sale/features/community/types/community.ts
new file mode 100644
index 0000000..5cc9d0a
--- /dev/null
+++ b/apps/sale/features/community/types/community.ts
@@ -0,0 +1,58 @@
+import { MeType } from "@workspace/core/types/auth.type";
+
+export type UserRole = "GUEST" | "LEARNER" | "TUTOR";
+export type TeachingMode = "ONLINE" | "OFFLINE" | "HYBRID" | "ALL";
+export type PostStatus = "OPEN" | "IN_SESSION" | "FULFILLED" | "CLOSED";
+export type ApplicationStatus = "PENDING" | "ACCEPTED" | "DECLINED";
+
+// Re-using MeType for user profile in the community context
+export type UserProfile = MeType;
+
+export interface TutorApplication {
+ id: string;
+ postId: string;
+ tutor: UserProfile;
+ pitchNote: string;
+ proposedRate: number;
+ availableSlots: string[];
+ createdAt: string;
+ status: ApplicationStatus;
+}
+
+export interface CommunityPost {
+ id: string;
+ author: UserProfile;
+ content: string;
+ imageUrls: string[];
+ subject: string;
+ gradeLevel: string;
+ teachingMode: TeachingMode;
+ city?: string;
+ district?: string;
+ budgetPerSession: number;
+ status: PostStatus;
+ createdAt: string;
+ applicationsCount: number;
+ applications: TutorApplication[];
+}
+
+export interface CommunityFilters {
+ searchQuery: string;
+ subject?: string;
+ gradeLevel?: string;
+ teachingMode?: TeachingMode;
+ district?: string;
+ minBudget?: number;
+ maxBudget?: number;
+}
+
+export interface CreatePostInput {
+ content: string;
+ imageUrls: string[];
+ subject: string;
+ gradeLevel: string;
+ teachingMode: TeachingMode;
+ city?: string;
+ district?: string;
+ budgetPerSession: number;
+}
diff --git a/apps/sale/features/tutor-profile/components/TutorProfilePage.tsx b/apps/sale/features/tutor-profile/components/TutorProfilePage.tsx
index 229a18e..e648501 100644
--- a/apps/sale/features/tutor-profile/components/TutorProfilePage.tsx
+++ b/apps/sale/features/tutor-profile/components/TutorProfilePage.tsx
@@ -15,7 +15,6 @@ import { TutorTeachingMethods } from "./TutorTeachingMethods";
import { useTutorDetailQuery } from "../hooks/useTutorDetailQuery";
import { useState, useEffect } from "react";
import { EmptyState } from "@workspace/ui/components/ui/empty-state";
-import Link from "next/link";
import LoadingGradient from "@workspace/ui/components/LoadingGradient";
export function TutorProfilePage() {
@@ -102,12 +101,13 @@ export function TutorProfilePage() {
title="Không tìm thấy hồ sơ gia sư"
description="Hồ sơ gia sư này không tồn tại hoặc đã bị gỡ. Vui lòng kiểm tra lại đường dẫn."
action={
-
- Trở về danh sách gia sư
-
+ Trở về trang trước đó
+
}
/>
diff --git a/package-lock.json b/package-lock.json
index 1d75174..b96de76 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -36,6 +36,7 @@
"react-icons": "^5.7.0",
"tailwind-merge": "^3.6.0",
"tailwindcss-animate": "^1.0.7",
+ "tinhthanhvn": "^3.0.2",
"zod": "^4.4.3",
"zustand": "^5.0.14"
},
@@ -8802,6 +8803,12 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tinhthanhvn": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinhthanhvn/-/tinhthanhvn-3.0.2.tgz",
+ "integrity": "sha512-7jU4ubBc9PprcpuI9WwsaCDsw2XeHZNr/dssA0igdqi2wkviYJ5DROzAwS5KmAznMi2qxotg3ODWhoySuf0YXg==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
diff --git a/package.json b/package.json
index 82dc272..b5e5f78 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"react-icons": "^5.7.0",
"tailwind-merge": "^3.6.0",
"tailwindcss-animate": "^1.0.7",
+ "tinhthanhvn": "^3.0.2",
"zod": "^4.4.3",
"zustand": "^5.0.14"
},
diff --git a/packages/core/configs/navbar.ts b/packages/core/configs/navbar.ts
index 90e9dfd..957ff58 100644
--- a/packages/core/configs/navbar.ts
+++ b/packages/core/configs/navbar.ts
@@ -103,12 +103,14 @@ export function resolveNavbarState({
const guestMenu: NavbarItem[] = [
{ label: "Gia sư", href: "/tutors" },
+ { 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: "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" },
@@ -120,6 +122,7 @@ const tutorOnboardingMenu: NavbarItem[] = [
const tutorApprovedMenu: NavbarItem[] = [
{ label: "Trang chủ", href: "/tutor/home" },
+ { 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" },