Quay lại lịch học
-
-
-
-
-
- {slotData.slotNumber}
+ {slotSkills.map((slot) => (
+
+
+
+
+ {slotData.slotNumber}
+
+
{slot.title}
-
{slotData.title}
-
-
{slotData.description}
+
{slot.description}
-
-
-
- {slotData.date}
-
-
-
-
- {slotData.time} ({slotData.duration})
-
-
-
-
-
{slotData.location}
+
+
+
+ {slot.date}
+
+
+
+
+ {slot.time} ({slot.duration})
+
+
+
+
+ {slot.location}
+
-
- {/* Progress Summary */}
+ {/* Progress Summary
{totalProgress}%
@@ -227,64 +157,62 @@ export default function SlotSkills() {
{totalPoints}
Điểm
+
*/}
-
-
- {/* Overall Progress */}
+ ))}
+ {/* Overall Progress
Tiến độ tổng thể
{totalProgress}%
-
+
*/}
{/* Skills Grid */}
- {skills.map((skill, index) => {
- const IconComponent = skill.icon
+ {slotData.materials.map((material: Material) => {
+ const IconComponent = iconMap[material.type] ?? BookOpen;
return (
handleSkillClick(skill)}
+ key={material.id}
+ className={`overflow-hidden transition-all duration-300 cursor-pointer hover:shadow-lg ${material.isLocked
+ ? "opacity-60 cursor-not-allowed"
+ : "hover:shadow-xl border-red-100 hover:border-red-200"
+ } ${material.isCompleted ? "ring-2 ring-green-500" : ""}`}
>
-
- {skill.isLocked ? (
+
+ {material.isLocked ? (
- ) : skill.isCompleted ? (
+ ) : material.isCompleted ? (
) : (
-
+
)}
- {skill.isCompleted && }
+ {material.isCompleted && }
- {skill.difficulty}
+ {material.difficulty}
-
{skill.name}
-
{skill.description}
+
{material.name}
+
{material.description}
@@ -292,9 +220,9 @@ export default function SlotSkills() {
Tiến độ
- {skill.progress}%
+ {material.progress}%
-
+
{/* Stats */}
@@ -303,60 +231,47 @@ export default function SlotSkills() {
- {skill.duration}
+ {material.duration}
Thời gian
-
{skill.exercises}
+
{material.exercises}
Bài tập
-
{skill.points}
+
{material.points}
Điểm
- {/* Action Button */}
+
)
@@ -372,20 +287,19 @@ export default function SlotSkills() {
Lộ trình học tập
-
+ {/*
Hoàn thành các kỹ năng theo thứ tự để mở khóa kỹ năng tiếp theo:
- {skills.map((skill, index) => (
+ {setSlotData.materials.map((skill, index) => (
{index + 1}
@@ -397,9 +311,10 @@ export default function SlotSkills() {
))}
-
+ */}
+
{/* Achievement Banner */}
diff --git a/src/components/sections/vocabulary.tsx b/src/components/sections/common/vocabulary.tsx
similarity index 100%
rename from src/components/sections/vocabulary.tsx
rename to src/components/sections/common/vocabulary.tsx
diff --git a/src/components/sections/cta.tsx b/src/components/sections/cta.tsx
deleted file mode 100644
index 2a8b169..0000000
--- a/src/components/sections/cta.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { BookOpen, ArrowRight } from "lucide-react"
-
-export function CTA() {
- return (
-
-
-
-
-
-
Ready to Start Your Japanese Journey?
-
- Join over 100,000 learners who are already mastering Japanese. Start with our free lessons today and see
- the difference.
-
-
-
-
-
-
-
-
-
No credit card required • 7-day free trial • Cancel anytime
-
-
-
- )
-}
diff --git a/src/components/sections/entity.tsx b/src/components/sections/entity.tsx
new file mode 100644
index 0000000..ab855f5
--- /dev/null
+++ b/src/components/sections/entity.tsx
@@ -0,0 +1,70 @@
+import type {Status} from "@/components/sections/enum"
+
+export interface Topic {
+ id: string
+ name: string
+ description?: string
+ // Add other topic properties as needed
+}
+
+export interface ApprovalRequest {
+ id: string
+ status: Status
+ requestDate: string
+ approvalDate?: string
+ comments?: string
+ // Add other approval request properties as needed
+}
+
+export interface Material {
+ id: string
+ description?: string
+ fileUrl: string
+ type: string
+ unitId: string
+ unit?: Unit
+ approvalRequests?: ApprovalRequest[]
+}
+
+export interface Unit {
+ id: string
+ title: string
+ description?: string
+ status: Status
+ prerequisiteUnitId?: string
+ prerequisiteUnit?: Unit
+ chapterId: string
+ chapter?: Chapter
+ materials?: Material[]
+ approvalRequests?: ApprovalRequest[]
+}
+
+export interface Chapter {
+ id: string
+ title: string
+ description?: string
+ status: Status
+ prerequisiteChapterId?: string
+ prerequisiteChapter?: Chapter
+ courseId: string
+ course?: Course
+ units?: Unit[]
+ approvalRequests?: ApprovalRequest[]
+}
+
+export interface Course {
+ id: string
+ title: string
+ description?: string
+ duration: number
+ level: string
+ image?: string
+ requirement?: string
+ status: Status
+ prerequisiteCourseId?: string
+ prerequisiteCourse?: Course
+ chapters?: Chapter[]
+ topics?: Topic[]
+ approvalRequests?: ApprovalRequest[]
+}
+
diff --git a/src/components/sections/enum.ts b/src/components/sections/enum.ts
new file mode 100644
index 0000000..2381a1c
--- /dev/null
+++ b/src/components/sections/enum.ts
@@ -0,0 +1,35 @@
+// src/enums/enum.ts
+
+export enum Level {
+ N5 = 'N5',
+ N4 = 'N4',
+ N3 = 'N3',
+ N2 = 'N2',
+ N1 = 'N1',
+}
+
+export enum Status {
+ DRAFT = 'DRAFT', // đang biên soạn
+ PENDING = 'PENDING', // staff gửi duyệt
+ PUBLISHED = 'PUBLISHED', // manager duyệt
+ REJECTED = 'REJECTED', // manager từ chối
+ ARCHIVED = 'ARCHIVED', // ngưng sử dụng
+}
+
+export enum QuestionType {
+ MULTIPLE_CHOICE = 'MULTIPLE_CHOICE',
+ TRUE_FALSE = 'TRUE_FALSE',
+ FILL_BLANK = 'FILL_BLANK',
+ SHORT_ANSWER = 'SHORT_ANSWER',
+}
+
+export enum ExamScopeType {
+ GLOBAL = 'GLOBAL',
+ SCHOOL = 'SCHOOL',
+ CLASS = 'CLASS',
+}
+
+export enum ExamStatus {
+ PASSED = 'PASSED',
+ FAILED = 'FAILED',
+}
diff --git a/src/components/sections/staff/add-chapter-page.tsx b/src/components/sections/staff/add-chapter-page.tsx
new file mode 100644
index 0000000..1cd1021
--- /dev/null
+++ b/src/components/sections/staff/add-chapter-page.tsx
@@ -0,0 +1,250 @@
+"use client"
+
+import type React from "react"
+
+import { useState } from "react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import { Card, CardContent } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { ArrowLeft, BookOpen, Info, Sparkles, Plus, Hash } from "lucide-react"
+import type { Chapter, CreateChapterDTO, Subject } from "../entity"
+
+interface AddChapterPageProps {
+ course: Subject
+ onBack: () => void
+ onCreateChapter: (chapterData: CreateChapterDTO) => void
+}
+
+export function AddChapterPage({ course, onBack, onCreateChapter }: AddChapterPageProps) {
+ const [formData, setFormData] = useState({
+ title: "",
+ description: "",
+ orderNumber: (course.chapters?.length || 0) + 1,
+ })
+
+ const handleInputChange = (field: string, value: string | number) => {
+ setFormData((prev) => ({ ...prev, [field]: value }))
+ }
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ onCreateChapter({
+ ...formData,
+ subject: course,
+ })
+ }
+
+ const isFormValid = formData.title.trim() && formData.description.trim()
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
Thêm chương mới
+
Tạo chương học mới cho khóa học tiếng Nhật
+
+
+
+
+
+
+
+ {/* Left Sidebar - Course Info */}
+
+
+
+ {/* Course Info Header */}
+
+
+
+
+
+
Thông tin khóa học
+
+
+
+
+ {/* Course Image */}
+
+
+ {/* Course Details */}
+
+
+
+ ID Khóa học
+ {course.id}
+
+
+
+
+
Tên khóa học
+
{course.title}
+
+
+
+
+
Số chương hiện tại
+
+
+ {course.chapters?.length || 0}
+ chương
+
+
+
+
+ {/* Info Note */}
+
+
+
+
+
+
+
Lưu ý về thứ tự
+
+ Chương mới sẽ được thêm vào cuối danh sách. Bạn có thể thay đổi thứ tự sau khi tạo.
+
+
+
+
+
+
+
+
+
+ {/* Main Content - Form */}
+
+
+
+ {/* Form Header */}
+
+
+
+
+
+
Thông tin chương mới
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/sections/staff/add-unit-page.tsx b/src/components/sections/staff/add-unit-page.tsx
new file mode 100644
index 0000000..19e4f52
--- /dev/null
+++ b/src/components/sections/staff/add-unit-page.tsx
@@ -0,0 +1,522 @@
+"use client"
+
+import type React from "react"
+
+import { useState } from "react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import { Card, CardContent } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
+import {
+ ArrowLeft,
+ BookOpen,
+ Info,
+ Sparkles,
+ Plus,
+ Hash,
+ Upload,
+ FileText,
+ Video,
+ Headphones,
+ ImageIcon,
+ ChevronDown,
+ ChevronRight,
+ X,
+ Clock,
+ Target,
+ Award,
+} from "lucide-react"
+import type { Subject,Chapter } from "../entity"
+
+interface AddLessonPageProps {
+ course: Subject
+ chapter: Chapter
+ onBack: () => void
+ onCreateLesson: (lessonData: {
+ title: string
+ orderNumber: number
+ chapterId: number
+ materials: Array<{
+ type: string
+ name: string
+ description: string
+ file?: File
+ }>
+ }) => void
+}
+
+export function AddLessonPage({ course, chapter, onBack, onCreateLesson }: AddLessonPageProps) {
+ const [formData, setFormData] = useState({
+ title: "",
+ orderNumber: 9, // Next lesson number
+ })
+
+ const [materials, setMaterials] = useState([
+ {
+ id: 1,
+ type: "",
+ name: "",
+ description: "",
+ file: null as File | null,
+ expanded: true,
+ },
+ ])
+
+ const materialTypes = [
+ { value: "pdf", label: "Tài liệu PDF", icon: FileText },
+ { value: "video", label: "Video bài giảng", icon: Video },
+ { value: "audio", label: "File âm thanh", icon: Headphones },
+ { value: "image", label: "Hình ảnh", icon: ImageIcon },
+ { value: "presentation", label: "Bài thuyết trình", icon: BookOpen },
+ ]
+
+ const handleInputChange = (field: string, value: string | number) => {
+ setFormData((prev) => ({ ...prev, [field]: value }))
+ }
+
+ const handleMaterialChange = (id: number, field: string, value: string | File | null) => {
+ setMaterials((prev) => prev.map((material) => (material.id === id ? { ...material, [field]: value } : material)))
+ }
+
+ const addMaterial = () => {
+ const newId = Math.max(...materials.map((m) => m.id)) + 1
+ setMaterials((prev) => [
+ ...prev,
+ {
+ id: newId,
+ type: "",
+ name: "",
+ description: "",
+ file: null,
+ expanded: true,
+ },
+ ])
+ }
+
+ const removeMaterial = (id: number) => {
+ if (materials.length > 1) {
+ setMaterials((prev) => prev.filter((material) => material.id !== id))
+ }
+ }
+
+ const toggleMaterial = (id: number) => {
+ setMaterials((prev) =>
+ prev.map((material) => (material.id === id ? { ...material, expanded: !material.expanded } : material)),
+ )
+ }
+
+ const handleFileSelect = (id: number, file: File | null) => {
+ handleMaterialChange(id, "file", file)
+ }
+
+// const handleSubmit = (e: React.FormEvent) => {
+// e.preventDefault()
+// onCreateLesson({
+// ...formData,
+// chapterId: chapter.id,
+// materials: materials.map(({ id, expanded, ...material }) => material),
+// })
+// }
+
+ const isFormValid = formData.title.trim() && materials.every((m) => m.type && m.name.trim())
+
+ const getTypeIcon = (type: string) => {
+ const materialType = materialTypes.find((t) => t.value === type)
+ return materialType ? materialType.icon : FileText
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
Thêm bài học mới
+
Tạo bài học mới cho chương học
+
+
+
+
+
+
+
+ {/* Left Sidebar - Course & Chapter Info */}
+
+
+
+ {/* Course Info Header */}
+
+
+ {/* Course Image */}
+
+
+ {/* Course Details */}
+
+
+
+ ID KHÓA HỌC
+ {course.topic}
+
+
+
+
+
TÊN KHÓA HỌC
+
{course.title}
+
+
+
+
+ MỨC ĐỘ
+ {course.level}
+
+
+
+
+ {/* Chapter Info */}
+
+
+
+
+ CHƯƠNG
+
+
Chapter - {chapter.orderNumber}
+
+
+
+
TÊN CHƯƠNG
+
{chapter.title}
+
+
+
+
THỨ TỰ CHƯƠNG
+
Chương {chapter.orderNumber}
+
+
+
+
+
SỐ BÀI HỌC HIỆN TẠI
+
+
+ 8
+ bài học
+
+
+
+
+
+ {/* Progress Stats */}
+
+
+
+
+
+ {/* Main Content - Form */}
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/sections/staff/course-detail-layout-page.tsx b/src/components/sections/staff/course-detail-layout-page.tsx
new file mode 100644
index 0000000..58a280d
--- /dev/null
+++ b/src/components/sections/staff/course-detail-layout-page.tsx
@@ -0,0 +1,282 @@
+"use client"
+
+import { useState } from "react"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import {
+ ArrowLeft,
+ Edit,
+ Trash2,
+ Users,
+ BookOpen,
+ Star,
+ ChevronDown,
+ ChevronRight,
+ Plus,
+ Clock,
+ Play,
+ FileText,
+ Award,
+ Target,
+} from "lucide-react"
+import type { Subject } from "../entity"
+import { useNavigate } from "react-router-dom"
+
+interface CourseDetailLayoutPageProps {
+ course: Subject
+ onBack: () => void
+}
+
+export function CourseDetailLayoutPage({ course, onBack }: CourseDetailLayoutPageProps) {
+ const [expandedChapters, setExpandedChapters] = useState
>(new Set([1]))
+ const navigate = useNavigate();
+
+ const toggleChapter = (chapterId: number) => {
+ const newExpanded = new Set(expandedChapters)
+ if (newExpanded.has(chapterId)) {
+ newExpanded.delete(chapterId)
+ } else {
+ newExpanded.add(chapterId)
+ }
+ setExpandedChapters(newExpanded)
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+ Quay lại
+
+ Quản lý khóa học tiếng Nhật
+
+ Chi tiết khóa học
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Left Sidebar - Course Info */}
+
+
+
+ {/* Course Logo and Title */}
+
+
+
{course.title}
+
Tiếng Nhật cơ bản
+
+
+ {/* Stats Section */}
+
+
+
+
+ Thống kê
+
+
+
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Course Description */}
+
+
+
+
+
+ Mô tả khóa học
+
+
+
+
+ {course.description}
+
+
+
+
+ {/* Course Content */}
+
+
+
+
+
+
+
+ Nội dung khóa học
+
+
+
+
+
+ {course.chapters?.length} chương • {course.estimatedDuration}
+
+ {/*
+ {course.chapters.reduce((total, chapter) => total + chapter.lessonCount, 0)} bài học
+
*/}
+
+
+
+
+
+ {course.chapters?.map((chapter, index) => (
+
+
+
+ {expandedChapters.has(chapter.id) && (
+
+
+ {chapter.slots.length > 0 ? (
+ chapter.slots.map((lesson, lessonIndex) => (
+
+
+ {lessonIndex + 1}
+
+
{lesson.title}
+
+
+ ))
+ ) : (
+
+
+
+
+
Chưa có bài học nào
+
Thêm bài học đầu tiên cho chương này
+
+ )}
+
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/sections/staff/course-detail-modal.tsx b/src/components/sections/staff/course-detail-modal.tsx
new file mode 100644
index 0000000..bfedadb
--- /dev/null
+++ b/src/components/sections/staff/course-detail-modal.tsx
@@ -0,0 +1,324 @@
+"use client"
+
+import { useState } from "react"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import {
+ X,
+ Calendar,
+ User,
+ Clock,
+ BookOpen,
+ FileText,
+ ImageIcon,
+ Users,
+ Target,
+ Award,
+ ChevronRight,
+ Edit,
+} from "lucide-react"
+import { Subject } from "../entity"
+interface CourseDetailModalProps {
+ course: Subject
+ onClose: () => void
+}
+
+export function CourseDetailModal({ course, onClose }: CourseDetailModalProps) {
+ const [imageError, setImageError] = useState(false)
+ const [imageLoading, setImageLoading] = useState(true)
+
+ const getLevelColor = (level: string) => {
+ switch (level) {
+ case "N1":
+ return "bg-red-100 text-red-800 border-red-200"
+ case "N2":
+ return "bg-blue-100 text-blue-800 border-blue-200"
+ case "N3":
+ return "bg-orange-100 text-orange-800 border-orange-200"
+ case "N4":
+ return "bg-gray-100 text-gray-800 border-gray-200"
+ case "Cơ bản":
+ return "bg-green-100 text-green-800 border-green-200"
+ default:
+ return "bg-gray-100 text-gray-800 border-gray-200"
+ }
+ }
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case "Hoạt động":
+ return "bg-green-100 text-green-800 border-green-200"
+ case "Tạm dừng":
+ return "bg-yellow-100 text-yellow-800 border-yellow-200"
+ case "Nhập":
+ return "bg-blue-100 text-blue-800 border-blue-200"
+ default:
+ return "bg-gray-100 text-gray-800 border-gray-200"
+ }
+ }
+
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString("vi-VN", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ })
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
+ {course.topic}
+ {course.level}
+ {course.status}
+
+
{course.title}
+
+
+
+ {course.creatorId}
+
+
+
+ {course.estimatedDuration}
+
+
+
+ {course.chapters.length} chương
+
+
+
+
+
+
+
+
+
+ {/* Content */}
+
+
+ {/* Left Column - Image and Quick Info */}
+
+ {/* Course Image */}
+
+
+ {!imageError ? (
+ <>
+ {imageLoading && (
+
+
+
+ Đang tải hình ảnh...
+
+
+ )}
+

setImageLoading(false)}
+ onError={() => {
+ setImageError(true)
+ setImageLoading(false)
+ }}
+ />
+ >
+ ) : (
+
+
+
+ Hình ảnh khóa học
+
+
+ )}
+
+
+
+ {/* Quick Info */}
+
+
+
+
+ Thông tin nhanh
+
+
+
+
+ ID khóa học:
+ #{course.id}
+
+
+ Mã khóa học:
+ {course.topic}
+
+
+ Thứ tự:
+ #{course.orderNumber}
+
+
+ Số chương:
+ {course.chapters.length} chương
+
+
+ Thời lượng:
+ {course.estimatedDuration}
+
+
+
+
+ {/* Dates */}
+
+
+
+
+ Thời gian
+
+
+
+
+
Ngày tạo:
+
{formatDate(course.createdAt)}
+
+
+
Cập nhật lần cuối:
+
{formatDate(course.updatedAt)}
+
+
+
+
+
+ {/* Right Column - Detailed Information */}
+
+ {/* Description */}
+
+
+
+
+ Mô tả khóa học
+
+
+
+ {course.description}
+
+
+
+ {/* Instructor Info */}
+
+
+
+
+ Thông tin giảng viên
+
+
+
+
+
+
+
+
+
{course.creatorId}
+
Giảng viên tiếng Nhật
+
Chuyên gia giảng dạy tiếng Nhật
+
+
+
+
+
+ {/* Course Stats */}
+
+
+
+
+ Thống kê khóa học
+
+
+
+
+
+
{course.chapters.length}
+
Chương học
+
+
+
{course.estimatedDuration}
+
Thời lượng
+
+
+
{course.level}
+
Cấp độ
+
+
+
#{course.orderNumber}
+
Thứ tự
+
+
+
+
+
+ {/* Chapters */}
+
+
+
+
+ Danh sách chương ({course.chapters.length})
+
+
+
+
+ {course.chapters.map((chapter) => (
+
+
+
+
+
+
{chapter.title}
+
{chapter.description}
+
+
+ #{chapter.orderNumber}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+ Đóng
+
+
+
+ Chỉnh sửa khóa học
+
+
+
+
+
+ )
+}
diff --git a/src/components/sections/staff/course-list.tsx b/src/components/sections/staff/course-list.tsx
new file mode 100644
index 0000000..ec7239e
--- /dev/null
+++ b/src/components/sections/staff/course-list.tsx
@@ -0,0 +1,291 @@
+"use client"
+
+import { useState, useMemo } from "react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Badge } from "@/components/ui/badge"
+import { Edit, Eye, Plus, Search, Trash2 } from "lucide-react"
+import type { Subject } from "../entity"
+import { useNavigate } from "react-router-dom"
+
+interface CourseListPageProps {
+ courses: Subject[]
+ onViewDetails: (course: Subject) => void
+ onAddCourse: () => void
+}
+
+export function CourseListPage({ courses, onViewDetails, onAddCourse }: CourseListPageProps) {
+ const [searchTerm, setSearchTerm] = useState("")
+ const [selectedLevel, setSelectedLevel] = useState("all")
+ const [selectedStatus, setSelectedStatus] = useState("all")
+ const [currentPage, setCurrentPage] = useState(1)
+ const itemsPerPage = 5
+ const navigate= useNavigate();
+
+ // Get unique levels and statuses for filters
+ const uniqueLevels = Array.from(new Set(courses.map((course) => course.level)))
+ const uniqueStatuses = Array.from(new Set(courses.map((course) => course.status)))
+
+ // Filter courses based on search and filters
+ const filteredCourses = useMemo(() => {
+ return courses.filter((course) => {
+ const matchesSearch =
+ course.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ course.topic.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ course.creatorId.toLowerCase().includes(searchTerm.toLowerCase())
+
+ const matchesLevel = selectedLevel === "all" || course.level === selectedLevel
+ const matchesStatus = selectedStatus === "all" || course.status === selectedStatus
+
+ return matchesSearch && matchesLevel && matchesStatus
+ })
+ }, [courses, searchTerm, selectedLevel, selectedStatus])
+
+ // Pagination
+ const totalPages = Math.ceil(filteredCourses.length / itemsPerPage)
+ const startIndex = (currentPage - 1) * itemsPerPage
+ const endIndex = startIndex + itemsPerPage
+ const currentCourses = filteredCourses.slice(startIndex, endIndex)
+
+ const getLevelColor = (level: string) => {
+ switch (level) {
+ case "N1":
+ return "bg-red-600 text-white"
+ case "N2":
+ return "bg-blue-600 text-white"
+ case "N3":
+ return "bg-orange-600 text-white"
+ case "N4":
+ return "bg-purple-600 text-white"
+ case "N5":
+ return "bg-green-600 text-white"
+ case "Cơ bản":
+ return "bg-emerald-600 text-white"
+ default:
+ return "bg-gray-500 text-white"
+ }
+ }
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case "Hoạt động":
+ return "bg-blue-600 text-white"
+ case "Tạm dừng":
+ return "bg-yellow-100 text-yellow-800 border border-yellow-300"
+ case "Nhập":
+ return "bg-blue-100 text-blue-800 border border-blue-300"
+ default:
+ return "bg-gray-100 text-gray-800"
+ }
+ }
+
+ function handleEditCourse(course: Subject): void {
+ throw new Error("Function not implemented.")
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
Danh sách khóa học
+
Quản lý tất cả các khóa học trong hệ thống
+
+
+
+ Thêm môn học
+
+
+
+
+
+
+ {/* Filters */}
+
+
+ {/* Search */}
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10 border-blue-300 focus:border-blue-500 focus:ring-blue-500"
+ />
+
+
+
+ {/* Level Filter */}
+
+
+
+
+ {/* Status Filter */}
+
+
+
+
+ {/* Results Count */}
+
+
+ Tìm thấy {filteredCourses.length} khóa học
+
+
+
+
+
+ {/* Course Table */}
+
+ {/* Table Header */}
+
+
+
STT
+
ID
+
Tên khóa học
+
Mức độ
+
Trạng thái
+
Xem chi tiết
+
+
+
+ {/* Table Body */}
+
+ {currentCourses.length === 0 ? (
+
+
+
+
Không tìm thấy khóa học
+
Thử thay đổi bộ lọc hoặc từ khóa tìm kiếm
+
+
+ ) : (
+ currentCourses.map((course, index) => (
+
+
{startIndex + index + 1}
+
{course.topic}
+
+
+ {course.level}
+
+ {/*
{course.creatorId}
*/}
+
+ {course.status}
+
+
+ navigate(`/detail/${course.id}`, { state: { course } })}
+ className="h-8 w-8 p-0 hover:bg-blue-600 hover:text-white transition-colors"
+ >
+
+
+ handleEditCourse(course)}
+ className="h-8 w-8 p-0 hover:bg-green-600 hover:text-white transition-colors"
+ title="Chỉnh sửa"
+ >
+
+
+ handleDeleteCourse(course.id)}
+ className="h-8 w-8 p-0 hover:bg-red-600 hover:text-white transition-colors"
+ title="Xóa"
+ >
+
+
+
+ ))
+ )}
+
+
+ {/* Pagination */}
+ {filteredCourses.length > 0 && (
+
+
+
+ Hiển thị {startIndex + 1} - {Math.min(endIndex, filteredCourses.length)} trong tổng số{" "}
+ {filteredCourses.length} khóa học
+
+
+
+
setCurrentPage((prev) => Math.max(prev - 1, 1))}
+ disabled={currentPage === 1}
+ className="text-blue-600 border-blue-300 hover:bg-blue-100"
+ >
+ Trước
+
+
+
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
+ setCurrentPage(page)}
+ className={
+ currentPage === page
+ ? "bg-blue-600 text-white hover:bg-blue-700"
+ : "text-blue-600 border-blue-300 hover:bg-blue-100"
+ }
+ >
+ {page}
+
+ ))}
+
+
+
setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
+ disabled={currentPage === totalPages}
+ className="text-blue-600 border-blue-300 hover:bg-blue-100"
+ >
+ Sau
+
+
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/sections/staff/create-course-page.tsx b/src/components/sections/staff/create-course-page.tsx
new file mode 100644
index 0000000..3d3d008
--- /dev/null
+++ b/src/components/sections/staff/create-course-page.tsx
@@ -0,0 +1,308 @@
+"use client"
+
+import type React from "react"
+
+import { useState } from "react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { ArrowLeft, Upload, Plus } from "lucide-react"
+import type { Subject } from "../entity"
+interface CreateCoursePageProps {
+ onBack: () => void
+ onCreateCourse: (courseData: Omit) => void
+}
+
+export function CreateCoursePage({ onBack, onCreateCourse }: CreateCoursePageProps) {
+ const [formData, setFormData] = useState({
+ title: "",
+ topic: "",
+ description: "",
+ level: "",
+ estimatedDuration: "",
+ creatorId: "",
+ image: "",
+ status: "Pending",
+ orderNumber: 1,
+ })
+
+ const [selectedFile, setSelectedFile] = useState(null)
+ const [dragActive, setDragActive] = useState(false)
+
+ const levels = [
+ { value: "Cơ bản", label: "Cơ bản" },
+ { value: "N5", label: "N5" },
+ { value: "N4", label: "N4" },
+ { value: "N3", label: "N3" },
+ { value: "N2", label: "N2" },
+ { value: "N1", label: "N1" },
+ ]
+
+ const topics = [
+ { value: "japanese-basic", label: "Tiếng Nhật cơ bản" },
+ { value: "japanese-grammar", label: "Ngữ pháp tiếng Nhật" },
+ { value: "japanese-conversation", label: "Giao tiếp tiếng Nhật" },
+ { value: "japanese-business", label: "Tiếng Nhật thương mại" },
+ { value: "japanese-culture", label: "Văn hóa Nhật Bản" },
+ { value: "jlpt-prep", label: "Luyện thi JLPT" },
+ ]
+
+ const handleInputChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }))
+ }
+
+ const handleFileSelect = (file: File) => {
+ if (file && (file.type === "image/jpeg" || file.type === "image/png" || file.type === "image/gif")) {
+ setSelectedFile(file)
+ // In a real app, you would upload the file and get a URL
+ const imageUrl = URL.createObjectURL(file)
+ setFormData((prev) => ({ ...prev, image: imageUrl }))
+ }
+ }
+
+ const handleDrag = (e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ if (e.type === "dragenter" || e.type === "dragover") {
+ setDragActive(true)
+ } else if (e.type === "dragleave") {
+ setDragActive(false)
+ }
+ }
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setDragActive(false)
+
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
+ handleFileSelect(e.dataTransfer.files[0])
+ }
+ }
+
+ const handleFileInputChange = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files[0]) {
+ handleFileSelect(e.target.files[0])
+ }
+ }
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+
+ // Generate a unique topic code if not provided
+ const topicCode = formData.topic || `JP${String(Date.now()).slice(-3)}`
+
+ const courseData = {
+ ...formData,
+ topic: topicCode,
+ creatorId: formData.creatorId || "Admin",
+ studentCount: 0,
+ lessonCount: 0,
+ rating: 0,
+ }
+
+ onCreateCourse(courseData)
+ }
+
+ const isFormValid = formData.title && formData.description && formData.level
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+
Tạo khóa học mới
+
Thêm khóa học mới vào hệ thống
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/sections/staff/feedback-manager-page.tsx b/src/components/sections/staff/feedback-manager-page.tsx
new file mode 100644
index 0000000..7fcb2d6
--- /dev/null
+++ b/src/components/sections/staff/feedback-manager-page.tsx
@@ -0,0 +1,448 @@
+"use client"
+
+import { useState, useMemo } from "react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Card, CardContent } from "@/components/ui/card"
+import { Badge } from "@/components/ui/badge"
+import { Avatar, AvatarFallback } from "@/components/ui/avatar"
+import { Search, MessageSquare, Clock, CheckCircle, XCircle, Eye, Filter, TrendingUp, Calendar } from "lucide-react"
+
+interface FeedbackRecord {
+ id: string
+ requestId: string
+ course: string
+ chapter: string
+ lesson: string
+ manager: string
+ approvalTime: string
+ decision: "Chờ duyệt" | "Đã duyệt" | "Từ chối"
+ details?: string
+}
+
+const sampleFeedbackData: FeedbackRecord[] = [
+ {
+ id: "1",
+ requestId: "REQ005",
+ course: "Kanji cơ bản - 200 chữ",
+ chapter: "Kanji cơ bản",
+ lesson: "Số đếm từ 100-1000",
+ manager: "Nguyễn Văn Manager",
+ approvalTime: "",
+ decision: "Chờ duyệt",
+ },
+ {
+ id: "2",
+ requestId: "REQ001",
+ course: "Tiếng Nhật N5 - Cơ bản",
+ chapter: "Katakana - Bảng chữ cái nguyên âm",
+ lesson: "Luyện tập Hiragana nâng cao",
+ manager: "Trần Văn Manager",
+ approvalTime: "16/3/2024",
+ decision: "Đã duyệt",
+ },
+ {
+ id: "3",
+ requestId: "REQ002",
+ course: "Tiếng Nhật N5 - Cơ bản",
+ chapter: "Katakana - Bảng chữ cái nguyên âm",
+ lesson: "Katakana trong từ ngữ lại",
+ manager: "Trần Văn Manager",
+ approvalTime: "14/3/2024",
+ decision: "Đã duyệt",
+ },
+ {
+ id: "4",
+ requestId: "REQ003",
+ course: "Tiếng Nhật N4 - Sơ cấp",
+ chapter: "Ngữ pháp lưu cấp",
+ lesson: "Mẫu câu điều kiện",
+ manager: "Lê Thị Manager",
+ approvalTime: "13/3/2024",
+ decision: "Từ chối",
+ },
+ {
+ id: "5",
+ requestId: "REQ004",
+ course: "Tiếng Nhật N5 - Cơ bản",
+ chapter: "Kanji cơ bản N5",
+ lesson: "Kanji về thời tiết",
+ manager: "Trần Văn Manager",
+ approvalTime: "12/3/2024",
+ decision: "Đã duyệt",
+ },
+ {
+ id: "6",
+ requestId: "REQ006",
+ course: "Tiếng Nhật N4 - Sơ cấp",
+ chapter: "Từ vựng N4",
+ lesson: "Từ vựng về công việc",
+ manager: "Lê Thị Manager",
+ approvalTime: "10/3/2024",
+ decision: "Đã duyệt",
+ },
+]
+
+export function FeedbackManagerPage() {
+ const [searchTerm, setSearchTerm] = useState("")
+ const [statusFilter, setStatusFilter] = useState("all")
+ const [managerFilter, setManagerFilter] = useState("all")
+ const [currentPage, setCurrentPage] = useState(1)
+ const itemsPerPage = 6
+
+ // Calculate statistics
+ const stats = useMemo(() => {
+ const total = sampleFeedbackData.length
+ const pending = sampleFeedbackData.filter((item) => item.decision === "Chờ duyệt").length
+ const approved = sampleFeedbackData.filter((item) => item.decision === "Đã duyệt").length
+ const rejected = sampleFeedbackData.filter((item) => item.decision === "Từ chối").length
+
+ return { total, pending, approved, rejected }
+ }, [])
+
+ // Get unique managers for filter
+ const uniqueManagers = Array.from(new Set(sampleFeedbackData.map((item) => item.manager)))
+
+ // Filter data
+ const filteredData = useMemo(() => {
+ return sampleFeedbackData.filter((item) => {
+ const matchesSearch =
+ item.requestId.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ item.course.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ item.lesson.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ item.manager.toLowerCase().includes(searchTerm.toLowerCase())
+
+ const matchesStatus = statusFilter === "all" || item.decision === statusFilter
+ const matchesManager = managerFilter === "all" || item.manager === managerFilter
+
+ return matchesSearch && matchesStatus && matchesManager
+ })
+ }, [searchTerm, statusFilter, managerFilter])
+
+ // Pagination
+ const totalPages = Math.ceil(filteredData.length / itemsPerPage)
+ const startIndex = (currentPage - 1) * itemsPerPage
+ const endIndex = startIndex + itemsPerPage
+ const currentData = filteredData.slice(startIndex, endIndex)
+
+ const getDecisionColor = (decision: string) => {
+ switch (decision) {
+ case "Đã duyệt":
+ return "bg-emerald-100 text-emerald-800 border-emerald-200"
+ case "Từ chối":
+ return "bg-red-100 text-red-800 border-red-200"
+ case "Chờ duyệt":
+ return "bg-amber-100 text-amber-800 border-amber-200"
+ default:
+ return "bg-gray-100 text-gray-800 border-gray-200"
+ }
+ }
+
+ const getDecisionIcon = (decision: string) => {
+ switch (decision) {
+ case "Đã duyệt":
+ return
+ case "Từ chối":
+ return
+ case "Chờ duyệt":
+ return
+ default:
+ return null
+ }
+ }
+
+ const getManagerInitials = (name: string) => {
+ return name
+ .split(" ")
+ .map((word) => word[0])
+ .join("")
+ .toUpperCase()
+ .slice(0, 2)
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+
Feedback từ Manager
+
+ Xem phản hồi và quyết định của manager về các yêu cầu bài học
+
+
+
+
+
+
+
+ {/* Statistics Cards */}
+
+
+
+
+
+
+
+
+
Tổng yêu cầu
+
{stats.total}
+
+
+
+
+
+
+
+
+
+
+
+
+
Chờ duyệt
+
{stats.pending}
+
+
+
+
+
+
+
+
+
+
+
+
+
Đã duyệt
+
{stats.approved}
+
+
+
+
+
+
+
+
+
+
+
+
+
Từ chối
+
{stats.rejected}
+
+
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
Bộ lọc và tìm kiếm
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10 border-blue-300 focus:border-blue-500 focus:ring-blue-500 bg-white/80 backdrop-blur-sm"
+ />
+
+
+
+
+
+
+
+
+ Tìm thấy {filteredData.length} feedback
+
+
+
+
+
+
+ {/* Feedback Table */}
+
+
+ {/* Table Header */}
+
+
+
STT
+
ID Yêu cầu
+
Khóa học / Chương
+
Bài học
+
Manager
+
Thời gian duyệt
+
Quyết định
+
Chi tiết
+
+
+
+ {/* Table Body */}
+
+ {currentData.length === 0 ? (
+
+
+
+
Không tìm thấy feedback
+
Thử thay đổi bộ lọc hoặc từ khóa tìm kiếm
+
+
+ ) : (
+ currentData.map((item, index) => (
+
+
{startIndex + index + 1}
+
+
+ {item.requestId}
+
+
+
+
+
{item.course}
+
{item.chapter}
+
+
+
+
+
+
+
+ {getManagerInitials(item.manager)}
+
+
+
+
+
+
+ {item.approvalTime ? (
+
+
+ {item.approvalTime}
+
+ ) : (
+
-
+ )}
+
+
+
+ {getDecisionIcon(item.decision)}
+
+ {item.decision}
+
+
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+ {/* Pagination */}
+ {filteredData.length > 0 && (
+
+
+
+ Hiển thị {startIndex + 1} - {Math.min(endIndex, filteredData.length)} trong tổng số{" "}
+ {filteredData.length} feedback
+
+
+
+
setCurrentPage((prev) => Math.max(prev - 1, 1))}
+ disabled={currentPage === 1}
+ className="text-blue-600 border-blue-300 hover:bg-blue-100"
+ >
+ Trước
+
+
+
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
+ setCurrentPage(page)}
+ className={
+ currentPage === page
+ ? "bg-blue-600 text-white hover:bg-blue-700"
+ : "text-blue-600 border-blue-300 hover:bg-blue-100"
+ }
+ >
+ {page}
+
+ ))}
+
+
+
setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
+ disabled={currentPage === totalPages}
+ className="text-blue-600 border-blue-300 hover:bg-blue-100"
+ >
+ Sau
+
+
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/src/components/sections/stats.tsx b/src/components/sections/stats.tsx
deleted file mode 100644
index 765f78a..0000000
--- a/src/components/sections/stats.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-export function Stats() {
- const stats = [
- { number: "100K+", label: "Active Learners" },
- { number: "50+", label: "Expert Teachers" },
- { number: "1000+", label: "Lessons Available" },
- { number: "95%", label: "Success Rate" },
- ]
-
- return (
-
-
-
- {stats.map((stat, index) => (
-
-
{stat.number}
-
{stat.label}
-
- ))}
-
-
-
- )
-}
diff --git a/src/components/sections/testimonials.tsx b/src/components/sections/testimonials.tsx
deleted file mode 100644
index a1021eb..0000000
--- a/src/components/sections/testimonials.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { Card, CardContent } from "@/components/ui/card"
-import { AvatarFallback,Avatar, AvatarImage } from "../ui/avatar"
-import { Star, Quote } from "lucide-react"
-
-interface Testimonial {
- id: string
- name: string
- role: string
- avatar: string
- rating: number
- content: string
- achievement: string
-}
-
-export function Testimonials() {
- const testimonials: Testimonial[] = [
- {
- id: "1",
- name: "Sarah Johnson",
- role: "Software Engineer",
- avatar: "/placeholder.svg?height=60&width=60",
- rating: 5,
- content:
- "This platform made learning Japanese so much easier! The interactive lessons and spaced repetition system helped me pass JLPT N4 in just 6 months.",
- achievement: "Passed JLPT N4",
- },
- {
- id: "2",
- name: "Mike Chen",
- role: "Business Analyst",
- avatar: "/placeholder.svg?height=60&width=60",
- rating: 5,
- content:
- "The conversation practice with native speakers was a game-changer. I went from being afraid to speak to having confident conversations in Japanese.",
- achievement: "Fluent Speaker",
- },
- {
- id: "3",
- name: "Emma Rodriguez",
- role: "Student",
- avatar: "/placeholder.svg?height=60&width=60",
- rating: 5,
- content:
- "I love how the app adapts to my learning pace. The kanji learning system with mnemonics made memorizing characters so much more enjoyable.",
- achievement: "1000+ Kanji Learned",
- },
- ]
-
- return (
-
-
-
-
What Our Students Say
-
- Join thousands of successful Japanese learners who achieved their goals with our platform
-
-
-
-
- {testimonials.map((testimonial) => (
-
-
-
- {[...Array(testimonial.rating)].map((_, i) => (
-
- ))}
-
-
-
-
-
{testimonial.content}
-
-
-
-
-
-
- {testimonial.name
- .split(" ")
- .map((n) => n[0])
- .join("")}
-
-
-
-
{testimonial.name}
-
{testimonial.role}
-
{testimonial.achievement}
-
-
-
-
- ))}
-
-
-
- )
-}
diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx
new file mode 100644
index 0000000..74ddeee
--- /dev/null
+++ b/src/components/ui/collapsible.tsx
@@ -0,0 +1,24 @@
+"use client"
+
+import * as React from "react"
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+CollapsibleContent.displayName = CollapsiblePrimitive.CollapsibleContent.displayName
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
index 54ece17..3123942 100644
--- a/src/components/ui/dropdown-menu.tsx
+++ b/src/components/ui/dropdown-menu.tsx
@@ -3,72 +3,126 @@
import * as React from "react"
import { cn } from "@/lib/utils"
-const DropdownMenu = ({ children }: { children: React.ReactNode }) => {
- return {children}
+interface DropdownMenuContextType {
+ isOpen: boolean
+ toggle: () => void
+ close: () => void
}
-const DropdownMenuTrigger = React.forwardRef<
+const DropdownMenuContext = React.createContext(null)
+
+export const DropdownMenu = ({ children }: { children: React.ReactNode }) => {
+ const [isOpen, setIsOpen] = React.useState(false)
+ const menuRef = React.useRef(null)
+
+ const toggle = () => setIsOpen((prev) => !prev)
+ const close = () => setIsOpen(false)
+
+ // Auto close when clicking outside
+ React.useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
+ close()
+ }
+ }
+ document.addEventListener("mousedown", handleClickOutside)
+ return () => document.removeEventListener("mousedown", handleClickOutside)
+ }, [])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export const DropdownMenuTrigger = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes & { asChild?: boolean }
>(({ className, children, asChild, ...props }, ref) => {
- if (asChild) {
- return React.cloneElement(children as React.ReactElement, {
- ref,
- ...props,
- className: cn("inline-flex justify-center", (children as React.ReactElement).props.className, className),
- })
+ const context = React.useContext(DropdownMenuContext)
+ if (!context) throw new Error("DropdownMenuTrigger must be used within DropdownMenu")
+
+ const triggerProps = {
+ ref,
+ ...props,
+ onClick: (e: React.MouseEvent) => {
+ context.toggle()
+ props.onClick?.(e)
+ },
+ className: cn("inline-flex justify-center", className),
+ }
+
+ if (asChild && React.isValidElement(children)) {
+ return React.cloneElement(children as React.ReactElement, triggerProps)
}
return (
-
+
{children}
)
})
-
DropdownMenuTrigger.displayName = "DropdownMenuTrigger"
-const DropdownMenuContent = React.forwardRef<
+export const DropdownMenuContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes & {
align?: "start" | "center" | "end"
}
->(({ className, align = "center", children, ...props }, ref) => (
-
- {children}
-
-))
-DropdownMenuContent.displayName = "DropdownMenuContent"
+>(({ className, align = "center", children, ...props }, ref) => {
+ const context = React.useContext(DropdownMenuContext)
+ if (!context) throw new Error("DropdownMenuContent must be used within DropdownMenu")
+
+ if (!context.isOpen) return null
-const DropdownMenuItem = React.forwardRef>(
- ({ className, ...props }, ref) => (
+ return (
- ),
+ >
+ {children}
+
+ )
+})
+DropdownMenuContent.displayName = "DropdownMenuContent"
+
+export const DropdownMenuItem = React.forwardRef