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
133 changes: 133 additions & 0 deletions apps/sale/app/community/applications/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="max-w-3xl mx-auto px-4 py-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-[#280f91] flex items-center gap-2">
{isLearner ? (
<>
<Inbox className="w-6 h-6 text-accent" />
Gia sư yêu cầu kết nối
</>
) : (
<>
<Briefcase className="w-6 h-6 text-accent" />
Hồ sơ đã gửi
</>
)}
</h1>
<p className="text-slate-500 text-sm mt-1">
{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."}
</p>
</div>

<div className="space-y-4">
{displayApplications.length === 0 ? (
<EmptyState
title={
isLearner
? "Chưa có gia sư nào ứng tuyển"
: "Bạn chưa ứng tuyển lớp nào"
}
description={
isLearner
? "Các gia sư đang xem xét bài đăng của bạn. Vui lòng chờ thêm một chút nhé!"
: "Hãy quay lại Trang chủ để tìm kiếm các lớp học phù hợp với chuyên môn của bạn."
}
action={
<Link href="/community">
<Button>Quay lại Trang chủ</Button>
</Link>
}
/>
) : (
displayApplications.map(({ application, post }) => (
<ApplicationCard
key={application.id}
application={application}
post={post}
showPostInfo={true}
onAccept={
isLearner ? (appId) => handleAccept(appId, post.id) : undefined
}
/>
))
)}
</div>
</main>
);
}
15 changes: 15 additions & 0 deletions apps/sale/app/community/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-[#F8FAFC]">
<RoleSwitcherMockBar />
<div className="pb-24">
{children}
</div>
<CommunityBottomBar />
</div>
);
}
62 changes: 62 additions & 0 deletions apps/sale/app/community/my-posts/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="max-w-3xl mx-auto px-4 py-6">
<div className="mb-6 flex items-center justify-between">
<h1 className="text-2xl font-bold text-[#280f91]">Bài đăng của tôi</h1>
<Link href="/community">
<Button variant="outline" size="sm">
<PlusCircle className="w-4 h-4 mr-2" />
Đăng bài mới
</Button>
</Link>
</div>

<div className="space-y-4">
{myPosts.length === 0 ? (
<EmptyState
title="Bạn chưa có bài đăng nào"
description="Hãy quay lại Trang chủ để tạo bài đăng tìm gia sư đầu tiên của bạn nhé!"
action={
<Link href="/community">
<Button>
Quay lại Trang chủ
</Button>
</Link>
}
/>
) : (
myPosts.map(post => (
<PostCard key={post.id} post={post} />
))
)}
</div>
</main>
);
}
135 changes: 135 additions & 0 deletions apps/sale/app/community/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="max-w-7xl mx-auto px-4 py-6">
{/* Tiêu đề trang */}
<div className="mb-6">
<h1 className="text-2xl font-google-sans font-bold text-[#280f91]">
Cộng đồng tìm gia sư
</h1>
<p className="text-slate-500 mt-1">
Kết nối nhanh chóng với hàng ngàn gia sư chất lượng cao
</p>
</div>

<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 relative">
{/* Cột trái: Bộ lọc (Desktop) */}
<div className="hidden lg:block lg:col-span-3">
<div className="sticky top-20">
<FilterSidebar />
</div>
</div>

{/* Cột giữa: Feed chính */}
<div className="lg:col-span-6">
<PostCreateBox />

{/* Bộ lọc trên Mobile (Có thể mở rộng thành Drawer sau, tạm thời hiện inline) */}
<div className="lg:hidden mb-6">
<FilterSidebar />
</div>

<InfiniteFeedList />
</div>

{/* Cột phải: Widget phụ trợ */}
<div className="hidden lg:block lg:col-span-3">
<div className="sticky top-20 space-y-6">
{/* Widget: Bài đăng của tôi (Chỉ Learner mới thấy có ý nghĩa) */}
{currentUser.role === "LEARNER" && (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<h3 className="font-semibold text-slate-800 mb-3 flex items-center gap-2">
<BookOpen className="w-4 h-4 text-primary" />
Quản lý bài đăng
</h3>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Bài đăng của tôi:</span>
<span className="font-semibold text-primary">
{myPostsCount} bài
</span>
</div>
{myPostsCount > 0 && (
<button className="w-full mt-3 text-xs text-primary font-medium hover:underline text-left">
Xem tất cả bài đăng
</button>
)}
</div>
)}

{/* Widget: Gia sư tiêu biểu */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
<h3 className="font-semibold text-slate-800 mb-4 flex items-center gap-2">
<Award className="w-4 h-4 text-amber-500" />
Gia sư tiêu biểu tuần
</h3>

<div className="space-y-4">
<div className="flex items-center gap-3">
<img
src="https://i.pravatar.cc/150?u=tutor-top-1"
alt="Tutor"
className="w-10 h-10 rounded-full border border-slate-100"
/>
<div>
<div className="text-sm font-semibold text-slate-800">
Trần Lê Huy
</div>
<div className="text-xs text-slate-500">
IELTS 8.0 • 50+ giờ dạy
</div>
</div>
</div>
<div className="flex items-center gap-3">
<img
src="https://i.pravatar.cc/150?u=tutor-top-2"
alt="Tutor"
className="w-10 h-10 rounded-full border border-slate-100"
/>
<div>
<div className="text-sm font-semibold text-slate-800">
Nguyễn Mai Hoa
</div>
<div className="text-xs text-slate-500">
Toán LTĐH • Đánh giá 5.0
</div>
</div>
</div>
<div className="flex items-center gap-3">
<img
src="https://i.pravatar.cc/150?u=tutor-top-3"
alt="Tutor"
className="w-10 h-10 rounded-full border border-slate-100"
/>
<div>
<div className="text-sm font-semibold text-slate-800">
Phạm Văn A
</div>
<div className="text-xs text-slate-500">
Lập trình cơ bản • 30+ hs
</div>
</div>
</div>
</div>

<button className="w-full mt-4 text-sm text-center text-primary hover:text-primary/80 font-medium">
Xem thêm
</button>
</div>
</div>
</div>
</div>
</main>
);
}
Loading
Loading