From 21b2205bd341ff1aef8edafa47ea499e4551d866 Mon Sep 17 00:00:00 2001 From: adelrahmangharib Date: Wed, 26 Nov 2025 18:52:24 +0200 Subject: [PATCH] abdelrahmannnn --- backend/controllers/authController.js | 2 +- backend/controllers/eventController.js | 4 +- frontend/my-app/src/App.jsx | 4 +- .../src/components/Shared/EventCard.jsx | 18 +- .../src/components/Shared/EventsListing.jsx | 20 + .../EventsListing/hooks/EventListingHook.js | 144 +++- .../EventsListing/ui/EventsListingUI.jsx | 109 +++ .../MyWorkshops/hooks/MyWorkshopsHooks.js | 375 +++++++++ .../features/MyWorkshops/ui/MyWorkshopsUI.jsx | 748 +++++++++++++++++ .../usecases/MyWorkshopsUsecases.js | 32 + .../hooks/RegisteredEventsUsersHooks.js | 87 ++ .../ui/RegisteredEventsUsersUI.jsx | 267 ++++++ .../usecases/RegisteredEventsUsersUsecase.js | 55 ++ .../Workshops/ui/WorkshopsUsersUI.jsx | 1 - .../usecases/WorkshopsUsersUsecases.js | 24 +- .../src/features/trips/ui/TripsUserUI.jsx | 1 - frontend/my-app/src/pages/EventDetail.jsx | 19 +- .../my-app/src/pages/users/MyWorkshops.jsx | 764 +----------------- .../src/pages/users/RegisteredEvents.jsx | 273 +------ 19 files changed, 1918 insertions(+), 1029 deletions(-) create mode 100644 frontend/my-app/src/features/MyWorkshops/hooks/MyWorkshopsHooks.js create mode 100644 frontend/my-app/src/features/MyWorkshops/ui/MyWorkshopsUI.jsx create mode 100644 frontend/my-app/src/features/MyWorkshops/usecases/MyWorkshopsUsecases.js create mode 100644 frontend/my-app/src/features/RegisteredEventsUsers/hooks/RegisteredEventsUsersHooks.js create mode 100644 frontend/my-app/src/features/RegisteredEventsUsers/ui/RegisteredEventsUsersUI.jsx create mode 100644 frontend/my-app/src/features/RegisteredEventsUsers/usecases/RegisteredEventsUsersUsecase.js diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index edf6e93..969d9c5 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -78,7 +78,7 @@ export const register = async (req, res) => { // Send verification email for students if(userRole == 'student'){ try { - const backendUrl = process.env.BACKEND_URL || 'http://localhost:3000'; + const backendUrl = `http://localhost:${process.env.PORT}` || 'http://localhost:5000'; const verificationUrl = `${backendUrl}/api/users/verify-email?token=${emailVerificationToken}`; await sendViaGmailApi({ diff --git a/backend/controllers/eventController.js b/backend/controllers/eventController.js index ff37d11..81cf95b 100644 --- a/backend/controllers/eventController.js +++ b/backend/controllers/eventController.js @@ -426,6 +426,8 @@ export const createWorkshop = async (req, res) => { registrationDeadline, } = req.body; + const creatorId = req.user?.userId || req.body.createdBy || null; + const workshop = new Workshop({ name, description, @@ -441,7 +443,7 @@ export const createWorkshop = async (req, res) => { extraRequiredResources, capacity, registrationDeadline, - createdBy: req.user?.userId || null // from auth middleware + createdBy: creatorId // from auth middleware or request body }); await workshop.save(); diff --git a/frontend/my-app/src/App.jsx b/frontend/my-app/src/App.jsx index 497fe04..9d89f0f 100644 --- a/frontend/my-app/src/App.jsx +++ b/frontend/my-app/src/App.jsx @@ -93,7 +93,7 @@ function AppRoutes() { return ( {/* Auth Routes */} - } /> + } /> } /> } /> } /> @@ -144,7 +144,7 @@ function App() { {/*Auth Routes*/ } - } /> + } /> } /> } /> } /> diff --git a/frontend/my-app/src/components/Shared/EventCard.jsx b/frontend/my-app/src/components/Shared/EventCard.jsx index efe351a..89a05e1 100644 --- a/frontend/my-app/src/components/Shared/EventCard.jsx +++ b/frontend/my-app/src/components/Shared/EventCard.jsx @@ -17,7 +17,16 @@ import useArchiveEvent from "../../features/Events/hooks/useArchiveEvent"; import useExportRegistrations from "../../features/Events/hooks/useExportRegistrations"; import useRestrictEventRoles from "../../features/Events/hooks/useRestrictEventRoles"; -export default function EventCard({ event, index = 0, onUpdate, isFavorite: propIsFavorite, onFavoriteChange, onRegister }) { +export default function EventCard({ + event, + index = 0, + onUpdate, + isFavorite: propIsFavorite, + onFavoriteChange, + onRegister, + detailUrl, + detailState, +}) { const navigate = useNavigate(); const [isFavorited, setIsFavorited] = useState(propIsFavorite || false); const [favoriteLoading, setFavoriteLoading] = useState(false); @@ -46,7 +55,9 @@ export default function EventCard({ event, index = 0, onUpdate, isFavorite: prop }, [event.isArchived]); const handleCardClick = () => { - navigate(`/events/${event._id}`); + const targetId = event._id || event.id; + const targetUrl = detailUrl || (targetId ? `/events/${targetId}` : "/events"); + navigate(targetUrl, detailState ? { state: detailState } : undefined); }; const handleFavoriteClick = async (e) => { @@ -386,7 +397,8 @@ export default function EventCard({ event, index = 0, onUpdate, isFavorite: prop const canRestrictRoles = user?.role === "eventsOffice"; const isWorkshop = eventType === "workshop"; const isTrip = eventType === "trip"; - const canRegister = (isWorkshop || isTrip) && onRegister && user && ['student', 'staff', 'ta', 'professor'].includes(user.role); + // For public registration, show button whenever a handler is provided for workshops/trips + const canRegister = (isWorkshop || isTrip) && typeof onRegister === "function"; const handleArchiveClick = async (e) => { e.stopPropagation(); diff --git a/frontend/my-app/src/components/Shared/EventsListing.jsx b/frontend/my-app/src/components/Shared/EventsListing.jsx index 1c644ae..f50644e 100644 --- a/frontend/my-app/src/components/Shared/EventsListing.jsx +++ b/frontend/my-app/src/components/Shared/EventsListing.jsx @@ -22,6 +22,16 @@ export default function EventsListing() { resetFilters, showArchivedOnly, setShowArchivedOnly, + showFavoritesOnly, + setShowFavoritesOnly, + favoriteEventIds, + favoritesLoading, + refreshFavorites, + registrationForm, + setRegistrationForm, + openRegistrationForm, + closeRegistrationForm, + handleRegistration, } = useEventsListing(); return ( @@ -43,6 +53,16 @@ export default function EventsListing() { resetFilters={resetFilters} showArchivedOnly={showArchivedOnly} setShowArchivedOnly={setShowArchivedOnly} + showFavoritesOnly={showFavoritesOnly} + setShowFavoritesOnly={setShowFavoritesOnly} + favoriteEventIds={favoriteEventIds} + favoritesLoading={favoritesLoading} + refreshFavorites={refreshFavorites} + registrationForm={registrationForm} + setRegistrationForm={setRegistrationForm} + openRegistrationForm={openRegistrationForm} + closeRegistrationForm={closeRegistrationForm} + handleRegistration={handleRegistration} /> ); } diff --git a/frontend/my-app/src/features/EventsListing/hooks/EventListingHook.js b/frontend/my-app/src/features/EventsListing/hooks/EventListingHook.js index e3f22c3..6eaa31e 100644 --- a/frontend/my-app/src/features/EventsListing/hooks/EventListingHook.js +++ b/frontend/my-app/src/features/EventsListing/hooks/EventListingHook.js @@ -1,7 +1,13 @@ -import { useState, useEffect, useMemo, useContext } from "react"; +import { useState, useEffect, useMemo, useContext, useCallback } from "react"; import AuthContext from "../../../context/AuthContext"; -import { fetchUpcomingEvents, fetchArchivedEventsUseCase } from "../usecases/EventListingUseCase.js"; -import { assignEventImage } from"../../../utils/eventImages"; +import { + fetchUpcomingEvents, + fetchArchivedEventsUseCase, + fetchFavoriteEventIds, +} from "../usecases/EventListingUseCase.js"; +import { registerForEvent } from "../../Workshops/usecases/WorkshopsUsersUsecases"; +import { assignEventImage } from "../../../utils/eventImages"; +import Swal from "sweetalert2"; export default function useEventsListing() { const [searchQuery, setSearchQuery] = useState(""); @@ -19,8 +25,18 @@ export default function useEventsListing() { const [favoriteEventIds, setFavoriteEventIds] = useState([]); const [favoritesLoading, setFavoritesLoading] = useState(false); const [showArchivedOnly, setShowArchivedOnly] = useState(false); + const [registrationForm, setRegistrationForm] = useState({ + open: false, + event: null, + name: "", + email: "", + studentStaffId: "", + }); const { token, user } = useContext(AuthContext); + const favoriteRoles = ["student", "staff", "ta", "professor"]; + const normalizedRole = user?.role?.toLowerCase(); + const canFavorite = normalizedRole && favoriteRoles.includes(normalizedRole); const apiUrl = import.meta.env.VITE_API_BASE_URL || @@ -90,6 +106,29 @@ export default function useEventsListing() { loadArchived(); }, [showArchivedOnly, archivedEvents.length, archivedLoading, apiUrl, token]); + const refreshFavorites = useCallback(async () => { + if (!canFavorite || !token) { + setFavoritesLoading(false); + setFavoriteEventIds([]); + return; + } + + setFavoritesLoading(true); + const result = await fetchFavoriteEventIds(apiUrl, token); + + if (result.data && Array.isArray(result.data)) { + setFavoriteEventIds(result.data); + } else if (result.error) { + console.error("Failed to fetch favorites:", result.error); + } + + setFavoritesLoading(false); + }, [apiUrl, token, canFavorite]); + + useEffect(() => { + refreshFavorites(); + }, [refreshFavorites]); + // Event types const sourceEvents = showArchivedOnly ? archivedEvents : events; @@ -172,7 +211,16 @@ export default function useEventsListing() { }); return out; - }, [sourceEvents, searchQuery, selectedType, selectedLocation, sortBy, showArchivedOnly]); + }, [ + sourceEvents, + searchQuery, + selectedType, + selectedLocation, + sortBy, + showArchivedOnly, + showFavoritesOnly, + favoriteEventIds, + ]); const sortOptions = [ { label: "Date (Ascending)", value: "date-asc" }, @@ -187,6 +235,84 @@ export default function useEventsListing() { setSelectedLocation(null); setSortBy("date-asc"); setShowArchivedOnly(false); + setShowFavoritesOnly(false); + }; + + const openRegistrationForm = (event) => { + setRegistrationForm({ + open: true, + event, + name: "", + email: "", + studentStaffId: "", + }); + }; + + const closeRegistrationForm = () => { + setRegistrationForm({ + open: false, + event: null, + name: "", + email: "", + studentStaffId: "", + }); + }; + + const handleRegistration = async () => { + const { name, email, studentStaffId, event } = registrationForm; + + if (!name || !email || !studentStaffId) { + Swal.fire({ + title: "Missing Information", + text: "Please fill in name, email, and student/staff ID.", + icon: "warning", + }); + return; + } + + if (!email.includes("@")) { + Swal.fire({ + title: "Invalid Email", + text: "Please enter a valid email address.", + icon: "warning", + }); + return; + } + + const eventId = event?._id || event?.id; + if (!eventId) { + Swal.fire({ + title: "Error", + text: "Invalid event selection.", + icon: "error", + }); + return; + } + + const result = await registerForEvent( + eventId, + user?._id, + { name, email, studentStaffId }, + token + ); + + if (result.error) { + Swal.fire({ + title: "Registration Failed", + text: result.error, + icon: "error", + }); + } else { + const eventName = event?.title || event?.name || "this event"; + Swal.fire({ + title: "Registration Successful!", + text: `You have been registered for "${eventName}"`, + icon: "success", + timer: 2000, + showConfirmButton: false, + }); + closeRegistrationForm(); + } }; const loading = showArchivedOnly ? archivedLoading : activeLoading; @@ -210,5 +336,15 @@ export default function useEventsListing() { resetFilters, showArchivedOnly, setShowArchivedOnly, + showFavoritesOnly, + setShowFavoritesOnly, + favoriteEventIds, + favoritesLoading, + refreshFavorites, + registrationForm, + setRegistrationForm, + openRegistrationForm, + closeRegistrationForm, + handleRegistration, }; } diff --git a/frontend/my-app/src/features/EventsListing/ui/EventsListingUI.jsx b/frontend/my-app/src/features/EventsListing/ui/EventsListingUI.jsx index 580a800..65dd477 100644 --- a/frontend/my-app/src/features/EventsListing/ui/EventsListingUI.jsx +++ b/frontend/my-app/src/features/EventsListing/ui/EventsListingUI.jsx @@ -28,6 +28,16 @@ export default function EventsListingUI({ resetFilters, // Add resetFilters to props showArchivedOnly, setShowArchivedOnly, + showFavoritesOnly, + setShowFavoritesOnly, + favoriteEventIds = [], + refreshFavorites = () => {}, + favoritesLoading = false, + registrationForm, + setRegistrationForm, + openRegistrationForm, + closeRegistrationForm, + handleRegistration, }) { const { user } = useContext(AuthContext); @@ -234,6 +244,105 @@ export default function EventsListingUI({
+ + {/* Public registration dialog for workshops/trips */} + {registrationForm?.open ? ( +
+
+
+
+
+ Register for{" "} + {registrationForm.event?.type === "trip" ? "Trip" : "Workshop"} +
+
+ {registrationForm.event?.title || + registrationForm.event?.name} +
+
+
+
+ + + setRegistrationForm((prev) => ({ + ...prev, + name: e.target.value, + })) + } + className="w-full rounded-md border border-white/40 bg-white/20 backdrop-blur-sm px-3 py-2.5 text-sm font-medium text-navy placeholder:text-navy/50 focus:border-white/60 focus:ring-2 focus:ring-white/30 focus:bg-white/30 transition-all" + placeholder="Enter your full name" + /> +
+
+ + + setRegistrationForm((prev) => ({ + ...prev, + email: e.target.value, + })) + } + className="w-full rounded-md border border-white/40 bg-white/20 backdrop-blur-sm px-3 py-2.5 text-sm font-medium text-navy placeholder:text-navy/50 focus:border-white/60 focus:ring-2 focus:ring-white/30 focus:bg-white/30 transition-all" + placeholder="Enter your email" + /> +
+
+ + + setRegistrationForm((prev) => ({ + ...prev, + studentStaffId: e.target.value, + })) + } + className="w-full rounded-md border border-white/40 bg-white/20 backdrop-blur-sm px-3 py-2.5 text-sm font-medium text-navy placeholder:text-navy/50 focus:border-white/60 focus:ring-2 focus:ring-white/30 focus:bg-white/30 transition-all" + placeholder="Enter your student or staff ID" + /> +
+
+
+ + +
+
+
+ ) : null}
); } diff --git a/frontend/my-app/src/features/MyWorkshops/hooks/MyWorkshopsHooks.js b/frontend/my-app/src/features/MyWorkshops/hooks/MyWorkshopsHooks.js new file mode 100644 index 0000000..ab4669a --- /dev/null +++ b/frontend/my-app/src/features/MyWorkshops/hooks/MyWorkshopsHooks.js @@ -0,0 +1,375 @@ +import { useState, useEffect, useContext } from "react"; +import AuthContext from "../../../context/AuthContext"; +import Swal from "sweetalert2"; +import axios from "axios"; +import { fetchMyWorkshopsUsecase } from "../usecases/MyWorkshopsUsecases"; + +export default function useMyWorkshops() { + const { token, user } = useContext(AuthContext); + + const [workshops, setWorkshops] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showTimesModal, setShowTimesModal] = useState(false); + const [showAllSessionsModal, setShowAllSessionsModal] = useState(false); + const [editingWorkshop, setEditingWorkshop] = useState(null); + const [selectedTimeIndex, setSelectedTimeIndex] = useState(null); + const [sessionInEdit, setSessionInEdit] = useState(null); + const [formData, setFormData] = useState({ + name: "", + description: "", + location: "GUC Cairo", + startDate: "", + endDate: "", + times: [], + capacity: 50, + fullAgenda: "", + facultyResponsible: "MET", + professorsParticipating: [], + requiredBudget: 0, + fundingSource: "GUC", + extraRequiredResources: "", + registrationDeadline: "", + }); + + const apiUrl = + import.meta.env.VITE_API_BASE_URL || + import.meta.env.VITE_API_URL || + "http://localhost:5000"; + + useEffect(() => { + // Wait until we have both token and user id before fetching + if (!token || !user?._id) return; + fetchMyWorkshops(); + }, [token, user?._id]); + + const fetchMyWorkshops = async () => { + setLoading(true); + setError(null); + const result = await fetchMyWorkshopsUsecase(token, user?._id); + + if (result.error) { + setError(result.error); + setWorkshops([]); + } else { + setWorkshops(result.data || []); + } + + setLoading(false); + }; + + const filteredWorkshops = Array.isArray(workshops) + ? workshops.filter((workshop) => { + const matchesSearch = + workshop.title?.toLowerCase().includes(searchTerm.toLowerCase()) || + workshop.description + ?.toLowerCase() + .includes(searchTerm.toLowerCase()) || + workshop.location + ?.toLowerCase() + .includes(searchTerm.toLowerCase()); + const matchesStatus = + statusFilter === "all" || workshop.status === statusFilter; + return matchesSearch && matchesStatus; + }) + : []; + + const handleCreateWorkshop = () => { + setFormData({ + name: "", + description: "", + location: "GUC Cairo", + startDate: "", + endDate: "", + times: [], + capacity: 50, + fullAgenda: "", + facultyResponsible: "MET", + professorsParticipating: [], + requiredBudget: 0, + fundingSource: "GUC", + extraRequiredResources: "", + registrationDeadline: "", + }); + setEditingWorkshop(null); + setShowCreateModal(true); + }; + + const handleEditWorkshop = (workshop) => { + setFormData({ + name: workshop.name || workshop.title || "", + description: workshop.description || "", + location: workshop.location || "GUC Cairo", + startDate: workshop.startDate + ? new Date(workshop.startDate).toISOString().split("T")[0] + : "", + endDate: workshop.endDate + ? new Date(workshop.endDate).toISOString().split("T")[0] + : "", + times: + workshop.times && workshop.times.length > 0 + ? workshop.times + : [ + { + day: "Monday", + startTime: "", + endTime: "", + }, + ], + capacity: workshop.capacity || 50, + fullAgenda: workshop.fullAgenda || "", + facultyResponsible: workshop.facultyResponsible || "MET", + professorsParticipating: Array.isArray( + workshop.professorsParticipating + ) + ? workshop.professorsParticipating + : [], + requiredBudget: workshop.requiredBudget || 0, + fundingSource: workshop.fundingSource || "GUC", + extraRequiredResources: workshop.extraRequiredResources || "", + registrationDeadline: workshop.registrationDeadline + ? new Date(workshop.registrationDeadline) + .toISOString() + .split("T")[0] + : "", + }); + setEditingWorkshop(workshop); + setShowCreateModal(true); + }; + + const handleSaveWorkshop = async () => { + try { + // Basic validation against backend schema + if (!formData.name?.trim()) { + Swal.fire({ + title: "Missing Title", + text: "Please enter a workshop title.", + icon: "warning", + }); + return; + } + if (!formData.description?.trim()) { + Swal.fire({ + title: "Missing Description", + text: "Please enter a short description.", + icon: "warning", + }); + return; + } + if (!formData.startDate || !formData.endDate) { + Swal.fire({ + title: "Missing Dates", + text: "Please select both start and end dates.", + icon: "warning", + }); + return; + } + if (!formData.registrationDeadline) { + Swal.fire({ + title: "Missing Registration Deadline", + text: "Please select a registration deadline.", + icon: "warning", + }); + return; + } + if (!formData.fullAgenda?.trim()) { + Swal.fire({ + title: "Missing Full Agenda", + text: "Please enter a full agenda for the workshop.", + icon: "warning", + }); + return; + } + if (!formData.times[0]?.startTime) { + Swal.fire({ + title: "Missing Session Time", + text: "Please add at least one session with a start time.", + icon: "warning", + }); + return; + } + + const workshopData = { + name: formData.name, + type: "workshop", + description: formData.description, + location: formData.location, + startDate: formData.startDate, + endDate: formData.endDate, + times: [ + { + day: formData.times[0]?.day || "Monday", + startTime: formData.times[0]?.startTime || "", + endTime: formData.times[0]?.endTime || "", + }, + ], + capacity: parseInt(formData.capacity) || 50, + fullAgenda: formData.fullAgenda, + facultyResponsible: formData.facultyResponsible, + professorsParticipating: + typeof formData.professorsParticipating === "string" + ? formData.professorsParticipating + .split(",") + .map((p) => p.trim()) + .filter(Boolean) + : formData.professorsParticipating || [], + requiredBudget: parseInt(formData.requiredBudget) || 0, + fundingSource: formData.fundingSource, + extraRequiredResources: formData.extraRequiredResources, + registrationDeadline: formData.registrationDeadline, + createdBy: user?._id, + }; + + if (editingWorkshop) { + const resp = await axios.patch( + `${apiUrl}/api/events/workshops/${editingWorkshop._id}`, + workshopData, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + // Optimistically update the edited workshop in local state + const updated = resp.data?.workshop || resp.data || workshopData; + setWorkshops((prev) => + prev.map((w) => (w._id === editingWorkshop._id ? updated : w)) + ); + + Swal.fire({ + title: "Workshop Updated!", + text: "Your workshop has been updated successfully.", + icon: "success", + timer: 2000, + showConfirmButton: false, + }); + } else { + const resp = await axios.post(`${apiUrl}/api/events/workshops`, workshopData, { + headers: { Authorization: `Bearer ${token}` }, + }); + // Optimistically add the newly created workshop to local state + const created = resp.data?.workshop || resp.data || workshopData; + setWorkshops((prev) => [created, ...prev]); + + Swal.fire({ + title: "Workshop Created!", + text: "Your workshop has been submitted for review.", + icon: "success", + timer: 2000, + showConfirmButton: false, + }); + } + + setShowCreateModal(false); + setEditingWorkshop(null); + } catch (error) { + console.error("Error saving workshop:", error); + Swal.fire({ + title: "Error", + text: "Failed to save workshop. Please try again.", + icon: "error", + }); + } + }; + + const handleDeleteWorkshop = async (workshop) => { + const result = await Swal.fire({ + title: "Delete Workshop?", + text: `Are you sure you want to delete "${workshop.title}"? This action cannot be undone.`, + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#dc2626", + cancelButtonColor: "#6b7280", + confirmButtonText: "Yes, delete it!", + cancelButtonText: "Cancel", + }); + + if (result.isConfirmed) { + try { + await axios.delete(`${apiUrl}/api/events/workshops/${workshop._id}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + await fetchMyWorkshops(); + + Swal.fire({ + title: "Deleted!", + text: "Your workshop has been deleted.", + icon: "success", + timer: 1500, + showConfirmButton: false, + }); + } catch (error) { + console.error("Error deleting workshop:", error); + Swal.fire({ + title: "Error", + text: "Failed to delete workshop. Please try again.", + icon: "error", + }); + } + } + }; + + const getStatusColor = (status) => { + switch (status) { + case "approved": + return "bg-green-100 text-green-800 border-green-200"; + case "pending": + return "bg-yellow-100 text-yellow-800 border-yellow-200"; + case "rejected": + return "bg-red-100 text-red-800 border-red-200"; + case "edits_requested": + return "bg-blue-100 text-blue-800 border-blue-200"; + default: + return "bg-gray-100 text-gray-800 border-gray-200"; + } + }; + + const getStatusText = (status) => { + switch (status) { + case "approved": + return "Approved"; + case "pending": + return "Pending Review"; + case "rejected": + return "Rejected"; + case "edits_requested": + return "Edits Requested"; + default: + return "Unknown"; + } + }; + + return { + workshops, + loading, + error, + searchTerm, + setSearchTerm, + statusFilter, + setStatusFilter, + showCreateModal, + setShowCreateModal, + showTimesModal, + setShowTimesModal, + showAllSessionsModal, + setShowAllSessionsModal, + editingWorkshop, + setEditingWorkshop, + selectedTimeIndex, + setSelectedTimeIndex, + sessionInEdit, + setSessionInEdit, + formData, + setFormData, + filteredWorkshops, + fetchMyWorkshops, + handleCreateWorkshop, + handleEditWorkshop, + handleSaveWorkshop, + handleDeleteWorkshop, + getStatusColor, + getStatusText, + }; +} diff --git a/frontend/my-app/src/features/MyWorkshops/ui/MyWorkshopsUI.jsx b/frontend/my-app/src/features/MyWorkshops/ui/MyWorkshopsUI.jsx new file mode 100644 index 0000000..e4f9114 --- /dev/null +++ b/frontend/my-app/src/features/MyWorkshops/ui/MyWorkshopsUI.jsx @@ -0,0 +1,748 @@ +import React from "react"; +import { motion } from "framer-motion"; +import EventCard from "../../../components/Shared/EventCard"; +import Loader from "../../../components/Shared/Loader"; +import Footer from "../../../components/Shared/Footer"; +import SearchBar from "../../../components/Shared/SearchBar"; +import { Button } from "../../../components/ui/button"; +import { Plus, Edit3, Trash2 } from "lucide-react"; + +export default function MyWorkshopsUI(props) { + const { + loading, + error, + workshops, + searchTerm, + setSearchTerm, + statusFilter, + setStatusFilter, + filteredWorkshops, + fetchMyWorkshops, + handleCreateWorkshop, + handleEditWorkshop, + handleSaveWorkshop, + handleDeleteWorkshop, + showCreateModal, + setShowCreateModal, + showTimesModal, + setShowTimesModal, + showAllSessionsModal, + setShowAllSessionsModal, + editingWorkshop, + selectedTimeIndex, + setSelectedTimeIndex, + sessionInEdit, + setSessionInEdit, + formData, + setFormData, + getStatusColor, + getStatusText, + } = props; + + if (loading) { + return ( +
+
+
+
+
+ +
+
+ ); + } + + if (error && workshops.length === 0) { + return ( +
+
+
+
+
+
+
⚠️
+

+ Error Loading Workshops +

+

{error}

+ +
+
+
+ ); + } + + return ( +
+
+
+
+ +
+ +
+
+
+
+ +
+ {/* Header Section */} + +
+

+ My Workshops +

+

+ Manage and track your workshop submissions +

+
+ +
+ + {/* Search & Filters */} + + {/* Search Bar */} + + + {/* Status Filter Row */} +
+ {["all", "pending", "approved", "rejected", "edits_requested"].map( + (status) => ( + + ) + )} +
+
+ + {/* Workshops Grid */} + + {filteredWorkshops.map((workshop, index) => { + const eventData = { + ...workshop, + name: workshop.name || workshop.title, + type: workshop.type || "workshop", + }; + return ( +
+ +
+ + +
+
+ ); + })} +
+ + {filteredWorkshops.length === 0 && ( + +

No Workshops Yet

+

+ {searchTerm || statusFilter !== "all" + ? "No workshops match your current search criteria. Try adjusting your filters or search terms." + : "Share your knowledge by creating your first workshop. It's a great way to engage with students and colleagues."} +

+ {!searchTerm && statusFilter === "all" && ( + + )} +
+ )} +
+ + {/* Create/Edit Workshop Modal */} + {showCreateModal && ( +
+
setShowCreateModal(false)} + /> +
+
+
+ {editingWorkshop ? "Edit Workshop" : "Create New Workshop"} +
+
+
+
+
+ + + setFormData((prev) => ({ ...prev, name: e.target.value })) + } + className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-[#001F3F] focus:ring-2 focus:ring-[#001F3F]/20" + placeholder="Enter workshop title" + /> +
+
+ + +
+
+ +
+ +