diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..93d301b1 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +VITE_MODE=DEVELOPMENT +VITE_BASEURL=http://localhost:3000 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ddbbbe60..f152a18e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,9 @@ dist-ssr *.njsproj *.sln *.sw? + + +# Environment files +.env +.env.development +.env.production \ No newline at end of file diff --git a/index.html b/index.html index 844e5941..d1ddc108 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,7 @@ - Material Tailwind Dashboard React | By Creative Tim + Mentor Academy - } /> - } /> - } /> + {account ? } /> : + } />} + } /> + } /> ); } diff --git a/src/api/Admin.api.js b/src/api/Admin.api.js new file mode 100644 index 00000000..185b6493 --- /dev/null +++ b/src/api/Admin.api.js @@ -0,0 +1,30 @@ +import axiosClient from "@/api/axiosClient"; + +export const AdminApi = { + getAllUsers: (token) => { + const url = "/users/"; + return axiosClient.get(url, { headers: { token: token } }); + }, + + getAllPromoCodes: (token) => { + const url = "/getProms"; + return axiosClient.get(url, { headers: { token: token } }); + }, + + generatePromoCode: (token, data = {}) => { + const url = "/createPromocode"; + return axiosClient.post(url, data, { headers: { token: token } }); + }, + + blockUser: (token, id, data = {}) => { + const url = `/blockUser/${id}`; + return axiosClient.patch(url, data, { headers: { token: token } }); + }, + getRecentSupportSessions: (token) => { + const url = "/activeSupportSessions"; + return axiosClient.get(url, { headers: { token: token } }); + }, + +}; + +export default AdminApi; \ No newline at end of file diff --git a/src/api/Auth.api.js b/src/api/Auth.api.js new file mode 100644 index 00000000..ff82e962 --- /dev/null +++ b/src/api/Auth.api.js @@ -0,0 +1,58 @@ +import axiosClient from "@/api/axiosClient"; + + +export const AuthApi = { + + SignIn: (data) => { + const url = "/auth/user/signIn"; + return axiosClient.post(url, data); + }, + + SignUp: (data) => { + const url = "/auth/user/signup"; + return axiosClient.post(url, data); + }, + + forGotPassword: (data) => { + const url = "/user/forgotPassword"; + return axiosClient.post(url, data); + }, + + reset: (data) => { + const url = "/user/reset/"; + return axiosClient.post(url, data) + }, + + verifyPromocode: (data) => { + const url = "/auth/verifyPromocode"; + return axiosClient.post(url, data, { headers: { 'Content-Type': "application/json" } }); + }, + + getTree: (token, id) => { + const url = "/auth/getAllChildren"; + return axiosClient.get(url, { headers: { token: token }, params: { educator: id } }); + }, + + contactUs: (data, token) => { + const url = "/contactus"; + return axiosClient.post(url, data, { headers: { token: token } }); + }, + + getContactUs: (token, limit, page) => { + const url = "/contactus"; + return axiosClient.get(url, { headers: { token: token }, params: { limit: limit, page: page } }); + }, + + getUserData: (token) => { + const url = "/user/user/profile"; + return axiosClient.get(url, { headers: { token: token } }); + }, + + EditUserData: (data, token) => { + const url = "/user/updateProfile"; + return axiosClient.patch(url, data, { headers: { token: token } }); + }, + +} + +export default AuthApi; \ No newline at end of file diff --git a/src/api/Courses.api.js b/src/api/Courses.api.js new file mode 100644 index 00000000..51934928 --- /dev/null +++ b/src/api/Courses.api.js @@ -0,0 +1,54 @@ +import axiosClient from "@/api/axiosClient"; +import MyCourses from "@/pages/dashboard/MyCourses"; + +export const CoursesApi = { + getAllCourses: (limit, page) => { + const url = "/course/"; + return axiosClient.get(url, { params: { limit: limit, page: page } }); + }, + + addCourse: (data, token) => { + const url = "/course/addCourse/"; + return axiosClient.post(url, data, { headers: { token: token } }); + }, + + getCourseLesson: (id, token) => { + const url = `/course/${id}/lessons`; + return axiosClient.get(url, { headers: { token: token } }); + }, + + getLessonById: (id, token) => { + const url = `/lessons/${id}`; + return axiosClient.get(url, { headers: { token: token } }); + }, + + deleteCourse: (id, token) => { + const url = `/course/${id}`; + return axiosClient.delete(url, { headers: { token: token } }); + }, + + editCourse: (id, data, token) => { + const url = `/course/edit/${id}`; + return axiosClient.post(url, data, { headers: { token: token } }); + }, + + addLessonToCourse: (id, data, token) => { + const url = `/course/${id}/lessons`; + return axiosClient.post(url, data, { headers: { token: token } }); + }, + + deleteLesson: (id, token) => { + const url = `/course/lessons/${id}`; + return axiosClient.delete(url, { headers: { token: token, "Content-Type": 'multipart/form-data' } }); + }, + MyCourses: (token) => { + const url = `/course/myCourses/if-you-find-this-you-are-a-hacker`; + return axiosClient.get(url, { headers: { token: token } }); + }, + AddtoMyCourses: (id, token) => { + const url = `/user/addToMyCourses/${id}`; + return axiosClient.post(url, {}, { headers: { token: token } }); + } +}; + +export default CoursesApi; \ No newline at end of file diff --git a/src/api/Notification.api.js b/src/api/Notification.api.js new file mode 100644 index 00000000..51917b27 --- /dev/null +++ b/src/api/Notification.api.js @@ -0,0 +1,13 @@ +import axiosClient from "@/api/axiosClient"; + +const NotificationsApi = { + getNotifications: (toekn, pageParam, limit) => axiosClient.get("/notification/getNotifications", { + headers: { token: toekn }, params: { + offset: pageParam * limit, + limit, + }, + }), + markAsRead: (toekn, notificationId) => axiosClient.post(`/notification/markAsRead`, { notificationId: notificationId }, { headers: { token: toekn } }), +} + +export default NotificationsApi; \ No newline at end of file diff --git a/src/api/Packages.api.js b/src/api/Packages.api.js new file mode 100644 index 00000000..7bc9e41a --- /dev/null +++ b/src/api/Packages.api.js @@ -0,0 +1,28 @@ +import axiosClient from "@/api/axiosClient"; + + +const PackagesApi = { + getAll: () => { + return axiosClient.get('/package'); + }, + add: (data, token) => { + return axiosClient.post('/package/create', data, { headers: { token: token }, "Content-Type": 'multipart/form-data' }); + }, + getPackage: (id) => { + return axiosClient.get(`/package/${id}`); + }, + deletePackage: (id, token) => { + return axiosClient.delete(`/package/${id}`, { headers: { token: token } }); + }, + updatePackage: (id, data, token) => { + return axiosClient.put(`/package/${id}`, data, { headers: { token: token }, "Content-Type": 'multipart/form-data' }); + }, + subscribeToPackage: (id, token) => { + return axiosClient.post(`/package/subscribe/${id}`, {}, { headers: { token: token } }); + }, + myPackages: (token) => { + return axiosClient.get('/package/my-packages/myPackages', { headers: { token: token } }); + }, +} + +export default PackagesApi; \ No newline at end of file diff --git a/src/api/Signal.api.js b/src/api/Signal.api.js new file mode 100644 index 00000000..0f35728a --- /dev/null +++ b/src/api/Signal.api.js @@ -0,0 +1,9 @@ +import axiosClient from "@/api/axiosClient"; + +const SignalApi = { + getAll: (token) => { + return axiosClient.get('/signal', { headers: { token: token } }); + }, +} + +export default SignalApi; \ No newline at end of file diff --git a/src/api/User.api.js b/src/api/User.api.js new file mode 100644 index 00000000..6e3454a1 --- /dev/null +++ b/src/api/User.api.js @@ -0,0 +1,14 @@ +import axiosClient from "@/api/axiosClient"; + +const UserApi = { + getTeam: (token) => { + const url = "/user/team/2"; + return axiosClient.get(url, { headers: { token: token }}); + }, + getAdminTeam: (token) => { + const url = "/user/team/admin"; + return axiosClient.get(url, { headers: { token: token }}); + }, +} + +export default UserApi; \ No newline at end of file diff --git a/src/api/Wallet.api.js b/src/api/Wallet.api.js new file mode 100644 index 00000000..1ca486b0 --- /dev/null +++ b/src/api/Wallet.api.js @@ -0,0 +1,62 @@ +import axiosClient from "@/api/axiosClient"; +import { data } from "autoprefixer"; + +const WalletApi = { + getWithdrawalRequests: (token,rest) => { + const url = "/wallet/withdraw"; + console.log(token,rest) + return axiosClient.post(url,rest, { headers: { token: token } }); + }, + getTransactionHistory: (token) => { + const url = "/wallet/transactionHistory"; + return axiosClient.get(url, { headers: { token: token } }); + } + , + getUserWithdrawalRequests: (token) => { + const url = "/wallet/withdrawalRequests"; + return axiosClient.get(url, { headers: { token: token } }); + }, + getUserDepositRequests: (token) => { + const url = "/wallet/depositRequests"; + return axiosClient.get(url, { headers: { token: token } }); + } + , + getPendingAmount: (token) => { + const url = "/wallet/pendingAmount"; + return axiosClient.get(url, { headers: { token: token } }); + } + , + getUserWalletAnalysis: (token) => { + const url = "/wallet/analysis"; + return axiosClient.get(url, { headers: { token: token } }); + }, + getAdminWalletAnalysis: (token) => { + const url = "/wallet/adminAnalysis"; + return axiosClient.get(url, { headers: { token: token } }); + }, + getWallet: (token) => { + const url = "/wallet/"; + return axiosClient.get(url, { headers: { token: token } }); + }, + walletAnalysisCharts: (token) => { + const url = "/wallet/charts"; + return axiosClient.get(url, { headers: { token: token } }); + }, + adminWalletAnalysisCharts: (token) => { + const url = "/wallet/adminCharts"; + return axiosClient.get(url, { headers: { token: token } }); + }, + + adminWallets: (token) => { + const url = "/wallet/admin/wallets"; + return axiosClient.get(url, { headers: { token: token } }); + }, + + + forceReleaseAllPendingBalances: (token) => { + const url = "/wallet/force-release-all"; + return axiosClient.post(url, { headers: { token: token } }); + } +} + +export default WalletApi; \ No newline at end of file diff --git a/src/api/axiosClient.js b/src/api/axiosClient.js new file mode 100644 index 00000000..51846210 --- /dev/null +++ b/src/api/axiosClient.js @@ -0,0 +1,12 @@ +import axios from 'axios'; + +const baseUrl = import.meta.env.VITE_BASEURL + +export const axiosClient = axios.create({ + baseURL: baseUrl + "/api/v1/", + headers: { + 'Content-Type': 'application/json', + }, +}) + +export default axiosClient; \ No newline at end of file diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 00000000..14c7a59f --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,8 @@ +export * from "@/api/axiosClient"; +export * from "@/api/Auth.api"; +export * from "@/api/Admin.api"; +export * from "@/api/Courses.api"; +export * from "@/api/Notification.api"; +export * from "@/api/Packages.api"; +export * from "@/api/Signal.api"; +export * from "@/api/Wallet.api"; \ No newline at end of file diff --git a/src/components/CategoryFilter.jsx b/src/components/CategoryFilter.jsx new file mode 100644 index 00000000..1497bf19 --- /dev/null +++ b/src/components/CategoryFilter.jsx @@ -0,0 +1,39 @@ +import React, { useState } from 'react'; + +const CategoryFilter = ({ categories, onCategoryChange }) => { + const [selectedCategories, setSelectedCategories] = useState([]); + + const toggleCategory = (category) => { + const newCategories = selectedCategories.includes(category) + ? selectedCategories.filter(cat => cat !== category) + : [...selectedCategories, category]; + + // Update local state first + setSelectedCategories(newCategories); + + // Then call the parent component's callback + onCategoryChange(newCategories); + }; + + return ( +
+ {categories.map(category => ( + + ))} +
+ ); +}; + +export default CategoryFilter; \ No newline at end of file diff --git a/src/components/ReadMoreComonent.jsx b/src/components/ReadMoreComonent.jsx new file mode 100644 index 00000000..acc2ea69 --- /dev/null +++ b/src/components/ReadMoreComonent.jsx @@ -0,0 +1,58 @@ +import { useLang } from "@/hooks/LangContext"; +import { motion } from "framer-motion"; +import { useState } from "react"; +import { CalendarClock } from "lucide-react"; + +export default function ReadMoreComonent({ + children, + className, + date, + headingData, +}) { + const [readMore, setReadMore] = useState(false); + const { lang } = useLang(); + return ( +
+ {headingData && ( + <> + + {headingData} + +
+ +

{date}

+
+ + )} + +
+ + {children} + + { + setReadMore(!readMore); + }} + className={` ${lang === "ar" ? "justify-end text-right" : "justify-start text-left" + } flex text-[#e9e9e9] cursor-pointer`} + > + {readMore + ? lang === "ar" + ? "...اقرا اقل" + : "read less..." + : lang === "ar" + ? "...اقرأ اكثر" + : "Read More..."} + +
+
+ ); +} diff --git a/src/components/Support.jsx b/src/components/Support.jsx new file mode 100644 index 00000000..7b4f3332 --- /dev/null +++ b/src/components/Support.jsx @@ -0,0 +1,412 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useAuth } from '@/hooks/Auth'; +import { MessageSquare, Send, X } from 'lucide-react'; +import { useSnackbar } from '@/hooks/SnackBar'; +import { AdminApi } from '@/api'; + +const ChatMessage = ({ message, isOwn }) => ( +
+
+

{message.message}

+ + {new Date(message.createdAt).toLocaleTimeString()} + +
+
+); + +export const UserSupportChat = () => { + const { account, socket } = useAuth(); + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([]); + const [newMessage, setNewMessage] = useState(''); + const [issue, setIssue] = useState(''); + const [sessionId, setSessionId] = useState(null); + const [queueInfo, setQueueInfo] = useState(null); + const { openSnackbar } = useSnackbar() + const [showCloseChatMessage, setShowCloseChatMessage] = useState(false); + const messagesEndRef = useRef(null); + const [newMessageWhileClosed, setNewMessageWhileClosed] = useState(false); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + if (!socket || !account) return; + + socket.on('queueUpdate', (info) => { + setQueueInfo(info); + }); + + socket.on('chatAccepted', (data) => { + setQueueInfo(null); + setSessionId(data.sessionId); + }); + + socket.on('newMessage', (message) => { + if (isOpen === false && newMessageWhileClosed === false) { + setNewMessageWhileClosed(true); + } + + setMessages(prev => [...prev, message]); + setTimeout(() => { + scrollToBottom(); + }, 100); + }); + + socket.on('chatClosed', () => { + openSnackbar('Chat has been closed', 'info') + setShowCloseChatMessage(true); + setNewMessageWhileClosed(false); + }); + + return () => { + socket.off('queueUpdate'); + socket.off('chatAccepted'); + socket.off('newMessage'); + socket.off('chatClosed'); + }; + }, [socket, account]); + + const handleStartChat = () => { + if (!issue.trim()) return; + console.log(issue) + socket.emit('requestSupport', { + userId: account._id, + issue + }); + }; + + const handleSendMessage = (e) => { + e.preventDefault(); + if (!newMessage.trim() || !sessionId) return; + console.log("🚀 ~ handleSendMessage ~ sessionId", typeof sessionId) + const message = { + sessionId, + senderId: account._id, + message: newMessage.trim() + }; + + socket.emit('chatMessage', message); + + setNewMessage(''); + setTimeout(() => { + scrollToBottom(); + }, 100); + }; + + const closeChat = () => { + setIsOpen(false); + setSessionId(null); + setMessages([]); + setQueueInfo(null); + setShowCloseChatMessage(false); + } + + const openChat = () => { + setIsOpen(true); + setNewMessageWhileClosed(false); + } + + const closeChatModal = () => { + setIsOpen(false); + setNewMessageWhileClosed(false); + } + + + return ( + <> + {!isOpen ? ( + + ) : ( +
+
+

Support Chat

+ +
+ +
+ {!sessionId ? ( +
+