diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 369d7bf..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -MONGODB_URI="" -SECRET="" -APP_EMAIL="" -APP_PASSWORD="" -ADMIN_EMAIL="" -BACKEND_DOMAIN_DEV = "http://localhost:3000" -FRONTEND_DOMAIN_DEV = "http://localhost:5173" -CLOUDINARY_CLOUD_NAME="" -CLOUDINARY_API_KEY="" -CLOUDINARY_API_SECRET="" diff --git a/backend/controllers/Event.controller.js b/backend/controllers/Event.controller.js index 47636d5..92efe7d 100644 --- a/backend/controllers/Event.controller.js +++ b/backend/controllers/Event.controller.js @@ -25,7 +25,7 @@ export const createEvent = asyncHandler(async (req, res) => { const posterLocalPath = req?.files?.poster[0]?.path; if (!posterLocalPath) { - return res.status(400).json({ message: "poster is required" }); + return res.status(400).json({ success : false, message: "poster is required" }); } const cloudinaryPosterPath = await uploadOnCloudinary(posterLocalPath); @@ -83,16 +83,17 @@ export const updateEventById = async (req, res) => { } const eventData = req.body; + console.log("New data : ", eventData); // checking if event name alrady exist in databse (as it is unique) - if (eventData.name) { - const ifExisted = await Event.findOne({ name: eventData.name }); - if (ifExisted) { - return res.status(409).json({ - message: "Event name already exists try using differnt event name", - }); - } - } + // if (eventData.name) { + // const ifExisted = await Event.findOne({ name: eventData.name }); + // if (ifExisted) { + // return res.status(409).json({ + // message: "Event name already exists try using differnt event name", + // }); + // } + // } const updatedEvent = await Event.findByIdAndUpdate(eventId, eventData, { new: true, diff --git a/backend/middlewares/validate.middlewares.js b/backend/middlewares/validate.middlewares.js index bd3813b..d8a0c6b 100644 --- a/backend/middlewares/validate.middlewares.js +++ b/backend/middlewares/validate.middlewares.js @@ -66,9 +66,9 @@ export const validateCreateEvent =[ body("venue").escape().notEmpty(), body("description").escape().notEmpty(), body("numberOfMember").escape().isInt({min:1}), - body("poster").escape().trim().notEmpty(), (req, res, next) => { const errors = validationResult(req); + console.log(errors) if (errors.isEmpty()) { next(); return ; diff --git a/backend/models/Event.model.js b/backend/models/Event.model.js index 3d3edd7..5a96be8 100644 --- a/backend/models/Event.model.js +++ b/backend/models/Event.model.js @@ -3,6 +3,10 @@ import User from './User.model.js'; import Team from './Team.model.js'; const EventSchema = new mongoose.Schema({ + isWorkshop : { + type: Boolean, + required : true + }, name : { type: String, required : true, @@ -45,24 +49,6 @@ const EventSchema = new mongoose.Schema({ title:String, link:String, }], - leaderboard: [ - { - type:mongoose.Schema.Types.ObjectId, - ref: User, - } - ], - teams: [ - { - type:mongoose.Schema.Types.ObjectId, - ref: Team, - } - ], - participants: [ - { - type:mongoose.Schema.Types.ObjectId, - ref: User, - } - ], isLive: { type: Boolean, default: false, diff --git a/backend/public/temp/1756228729443-Screenshot 2025-08-14 231736.png b/backend/public/temp/1756228729443-Screenshot 2025-08-14 231736.png new file mode 100644 index 0000000..4e02415 Binary files /dev/null and b/backend/public/temp/1756228729443-Screenshot 2025-08-14 231736.png differ diff --git a/backend/public/temp/1756228729448-Screenshot 2025-08-14 231736.png b/backend/public/temp/1756228729448-Screenshot 2025-08-14 231736.png new file mode 100644 index 0000000..4e02415 Binary files /dev/null and b/backend/public/temp/1756228729448-Screenshot 2025-08-14 231736.png differ diff --git a/backend/public/temp/1756229708577-Screenshot 2025-08-14 231736.png b/backend/public/temp/1756229708577-Screenshot 2025-08-14 231736.png new file mode 100644 index 0000000..4e02415 Binary files /dev/null and b/backend/public/temp/1756229708577-Screenshot 2025-08-14 231736.png differ diff --git a/backend/public/temp/1756229708580-Screenshot 2025-08-14 231736.png b/backend/public/temp/1756229708580-Screenshot 2025-08-14 231736.png new file mode 100644 index 0000000..4e02415 Binary files /dev/null and b/backend/public/temp/1756229708580-Screenshot 2025-08-14 231736.png differ diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index 0dd6ab1..0000000 --- a/frontend/.env.example +++ /dev/null @@ -1 +0,0 @@ -VITE_API_BASE_URL="http://localhost:3000" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6ba8b31..2386135 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "3dfolio", + "name": "EmR frontend", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "3dfolio", + "name": "EmR frontend", "version": "0.0.0", "dependencies": { "@emailjs/browser": "^4.4.1", @@ -31,6 +31,7 @@ "react-awesome-button": "^7.0.5", "react-carousel-minimal": "^1.4.1", "react-dom": "^18.3.1", + "react-hot-toast": "^2.6.0", "react-icons": "^5.3.0", "react-responsive": "^10.0.0", "react-router-dom": "^6.27.0", @@ -5174,6 +5175,15 @@ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", "license": "MIT" }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -8095,6 +8105,23 @@ "node": ">= 6" } }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-icons": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.3.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 525fbd0..06e51c5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "react-awesome-button": "^7.0.5", "react-carousel-minimal": "^1.4.1", "react-dom": "^18.3.1", + "react-hot-toast": "^2.6.0", "react-icons": "^5.3.0", "react-responsive": "^10.0.0", "react-router-dom": "^6.27.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2431810..05dee5e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -26,7 +26,7 @@ import UserDashboardPage from "./pages/UserDashboardPage"; import EventDetailsPage from "./pages/EventDetailsPage.jsx"; import CreateEventPage from "./pages/CreateEventPage.jsx"; import ManageEventsPage from "./pages/ManageEventsPage.jsx"; -import { Toaster } from "./components/ui/toaster"; +import { Toaster } from "react-hot-toast"; const App = () => { return ( @@ -36,7 +36,23 @@ const App = () => { - +
} /> @@ -125,33 +141,23 @@ const App = () => { - - - - + + } /> - - - - + } /> - - - - + } /> +
diff --git a/frontend/src/api/apiService.js b/frontend/src/api/apiService.js index e0bd65e..6591008 100644 --- a/frontend/src/api/apiService.js +++ b/frontend/src/api/apiService.js @@ -334,25 +334,44 @@ export const leaveTeam = async (teamId) => { // Events export const createEvent = async (eventData) => { + // Check if eventData is FormData or regular object + const isFormData = eventData instanceof FormData; + + const headers = { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }; + + // Don't set Content-Type for FormData (browser will set it automatically with boundary) + if (!isFormData) { + headers["Content-Type"] = "application/json"; + } + const response = await fetch(`${API_BASE_URL}/api/events/`, { method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify(eventData), + headers, + body: isFormData ? eventData : JSON.stringify(eventData), }); return handleResponse(response); }; export const updateEvent = async (eventId, eventData) => { + // Check if eventData is FormData or regular object + const isFormData = eventData instanceof FormData; + + const headers = { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }; + + // Don't set Content-Type for FormData (browser will set it automatically with boundary) + if (!isFormData) { + headers["Content-Type"] = "application/json"; + } + const response = await fetch(`${API_BASE_URL}/api/events/${eventId}`, { method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify(eventData), + credentials: "include", + headers, + body: isFormData ? eventData : JSON.stringify(eventData), }); return handleResponse(response); }; @@ -378,3 +397,15 @@ export const getEventsById = async (eventId) => { }); return handleResponse(response); }; + +export const deleteEvent = async (eventId) => { + const response = await fetch(`${API_BASE_URL}/api/events/${eventId}`, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + return handleResponse(response); +}; diff --git a/frontend/src/components/DynamicList.jsx b/frontend/src/components/DynamicList.jsx index d8d8f4b..47b2763 100644 --- a/frontend/src/components/DynamicList.jsx +++ b/frontend/src/components/DynamicList.jsx @@ -1,12 +1,12 @@ import React, { useContext, useState } from "react"; import { EventFormContext } from "../context/EventFormContext"; import useUpdateEffect from "../hooks/useUpdateEffect"; -import { FaTrash, FaEdit, FaCheck } from "react-icons/fa"; +import { FaTrash, FaEdit, FaCheck, FaPlus } from "react-icons/fa"; import { Input } from "./Input"; export const DynamicList = ({ fields, section, isEditing }) => { const { eventData, updateField } = useContext(EventFormContext); - const [items, setItems] = useState(eventData[section]); + const [items, setItems] = useState(eventData[section] || []); const [inputValues, setInputValues] = useState(fields.map(() => "")); useUpdateEffect(() => { @@ -24,7 +24,6 @@ export const DynamicList = ({ fields, section, isEditing }) => { setInputValues(fields.map(() => "")); } }; - // console.log(inputValues); const handleDeleteItem = (index) => { setItems(items.filter((_, i) => i !== index)); @@ -32,13 +31,13 @@ export const DynamicList = ({ fields, section, isEditing }) => { const handleEditItem = (index) => { const updatedItems = [...items]; - updatedItems[index].editing = true; // Add editing state + updatedItems[index].editing = true; setItems(updatedItems); }; const handleSaveEdit = (index) => { const updatedItems = [...items]; - delete updatedItems[index].editing; // Remove editing state after saving + delete updatedItems[index].editing; setItems(updatedItems); }; @@ -47,12 +46,14 @@ export const DynamicList = ({ fields, section, isEditing }) => { updatedItems[index][field] = e.target.value; setItems(updatedItems); }; + return ( - <> - {isEditing ? ( -
- {fields.map((field, index) => { - return ( +
+ {isEditing && ( +
+

Add New Item

+
+ {fields.map((field, index) => ( { setInputValues(updatedInputValues); }} /> - ); - })} - - + ))} + +
- ) : ( - "" )} {items.length > 0 && ( -
+
+

Current Items

{items.map((item, index) => (
- {fields.map((field) => ( - handleInputChange(e, index, field.name)} - /> - ))} - - {/* Controls: Edit, Save, Delete */} - {isEditing && ( -
- {item.editing ? ( - handleSaveEdit(index)} - /> - ) : ( - handleEditItem(index)} - /> - )} - handleDeleteItem(index)} +
+ {fields.map((field) => ( + handleInputChange(e, index, field.name)} /> -
- )} + ))} + + {isEditing && ( +
+ {item.editing ? ( + + ) : ( + + )} + +
+ )} +
))}
)} - + + {items.length === 0 && ( +
+
📝
+

No items added yet

+

Start by adding your first item above

+
+ )} +
); }; diff --git a/frontend/src/components/EventCard.jsx b/frontend/src/components/EventCard.jsx index 140f190..109eacf 100644 --- a/frontend/src/components/EventCard.jsx +++ b/frontend/src/components/EventCard.jsx @@ -1,29 +1,109 @@ import React from "react"; +import { + getEventStatus, + getStatusColor, + getStatusTextColor, + formatDate, + formatTime +} from "../utils/eventUtils"; + +function EventCard({ event, onActionClick }) { + const status = getEventStatus(event.date); + const statusColor = getStatusColor(status); + const statusTextColor = getStatusTextColor(status); -function EventCard({ event, action, onActionClick, isLive }) { return ( -
onActionClick(event)} > - {event.name} -

{event.name}

-

{event.details}

- + {/* Status Badge */} +
+ + + {status} + +
+ + {/* Event Image */} +
+ {event.name} { + e.target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDQwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iMzAwIiBmaWxsPSIjMzc0MTUxIi8+CjxwYXRoIGQ9Ik0yMDAgMTUwQzE3OC45NTQgMTUwIDE2MiAxMzMuMDQ2IDE2MiAxMTJDMTYyIDkwLjk1NCAxNzguOTU0IDc0IDIwMCA3NEMyMjEuMDQ2IDc0IDIzOCA5MC45NTQgMjM4IDExMkMyMzggMTMzLjA0NiAyMjEuMDQ2IDE1MCAyMDAgMTUwWiIgZmlsbD0iIzlDQTBBNiIvPgo8cGF0aCBkPSJNMzAwIDIyNkMzMDAgMjI2IDI2MCAyMDAgMjAwIDIwMEMxNDAgMjAwIDEwMCAyMjYgMTAwIDIyNlYyNThIMzAwVjIyNloiIGZpbGw9IiM5Q0EwQTYiLz4KPC9zdmc+'; + }} + /> +
+
+ + {/* Event Content */} +
+ {/* Event Name */} +

+ {event.name} +

+ + {/* Date and Time */} +
+
+ + + + {formatDate(event.date)} +
+
+ + + + {formatTime(event.date)} +
+
+ + {/* Venue */} + {event.venue && ( +
+ + + + + {event.venue} +
+ )} + + {/* Description */} + {event.description && ( +

+ {event.description} +

+ )} + + {/* Event Type Badge */} + {event.isWorkshop !== undefined && ( +
+ + {event.isWorkshop ? 'Workshop' : 'Event'} + +
+ )} + + {/* Click Indicator */} +
+ + + + + Click to view details +
+
+ + {/* Hover Effect Overlay */} +
); } diff --git a/frontend/src/components/EventDescripitonSection.jsx b/frontend/src/components/EventDescripitonSection.jsx index e2dc22e..4a8fa67 100644 --- a/frontend/src/components/EventDescripitonSection.jsx +++ b/frontend/src/components/EventDescripitonSection.jsx @@ -1,49 +1,116 @@ import React, { useContext, useState } from "react"; import MDEditor from "@uiw/react-md-editor"; -import Accordion from "./Accordion"; -import useUpdateEffect from "../hooks/useUpdateEffect"; import { EventFormContext } from "../context/EventFormContext"; export const EventDescriptionSection = ({ disabled }) => { const [isEditing, setIsEditing] = useState(!disabled); const { eventData, updateField } = useContext(EventFormContext); - const [content, setContent] = useState(eventData["description"]); + const [content, setContent] = useState(eventData["description"] || ""); - useUpdateEffect(() => { + const handleSave = () => { updateField("description", content); - }, [isEditing]); + setIsEditing(false); + }; - const handleSave = () => { - setIsEditing(); + const handleEdit = () => { + setIsEditing(true); }; return ( - <> - - {isEditing ? ( - - ) : ( - - )} - - - +
+
+ {/* Header */} +
+

Event Description

+

+ Create a compelling description for your event using markdown formatting +

+
+ + {/* Content Area */} +
+ {isEditing ? ( +
+ {/* Editor Container */} +
+ +
+ + {/* Action Buttons */} +
+ + +
+
+ ) : ( +
+ {/* Preview Container */} +
+ {content ? ( + + ) : ( +
+
📝
+

No description added yet

+

Click edit to add your event description

+
+ )} +
+ + {/* Action Button */} +
+ +
+
+ )} +
+ + {/* Markdown Tips */} +
+

💡 Markdown Tips:

+
+

• Use # Heading for titles

+

• Use **bold** or *italic* for emphasis

+

• Use - item for bullet points

+

• Use [text](url) for links

+
+
+
+
); }; diff --git a/frontend/src/components/EventDetails.jsx b/frontend/src/components/EventDetails.jsx new file mode 100644 index 0000000..2ae1581 --- /dev/null +++ b/frontend/src/components/EventDetails.jsx @@ -0,0 +1,313 @@ +import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + getEventStatus, + getStatusColor, + formatDate, + formatTime +} from "../utils/eventUtils"; +import { showToast } from "../utils/toast"; +import { updateEvent, deleteEvent } from "../api/apiService"; +import EventEditForm from "./EventEditForm"; + +const EventDetails = ({ event, onEdit, onDelete }) => { + const navigate = useNavigate(); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const status = getEventStatus(event.date); + const statusColor = getStatusColor(status); + + const handleDelete = async () => { + setIsLoading(true); + try { + const eventId = event._id || event.id; + await deleteEvent(eventId); + showToast.largeSuccess("Event deleted successfully!"); + setShowDeleteConfirm(false); + navigate("/events/manage"); + } catch (error) { + showToast.largeError("Failed to delete event. Please try again."); + console.error("Delete error:", error); + } finally { + setIsLoading(false); + } + }; + + const handleEdit = () => { + setIsEditing(true); + }; + + const handleSave = async (formData) => { + setIsLoading(true); + try { + const eventId = event._id || event.id; + if (!eventId) { + throw new Error("Event ID not found"); + } + + console.log("Updating event with ID:", eventId); + console.log("FormData being sent:", formData); + + const response = await updateEvent(eventId, formData); + console.log("Update response:", response); + + showToast.largeSuccess("Event updated successfully!"); + setIsEditing(false); + // Refresh the page to show updated data + window.location.reload(); + } catch (error) { + console.error("Update error details:", error); + const errorMessage = error.message || error.response?.data?.message || "Failed to update event. Please try again."; + showToast.largeError(errorMessage); + } finally { + setIsLoading(false); + } + }; + + const handleCancelEdit = () => { + setIsEditing(false); + }; + + // If editing, show the dedicated edit form + if (isEditing) { + return ( + + ); + } + + return ( + <> +
+ {/* Header */} +
+
+
+
+

{event.name}

+ + {status} + +
+
+ {event.isWorkshop ? "Workshop" : "Event"} + + {formatDate(event.date)} + + {formatTime(event.date)} +
+
+ + {/* Action Buttons */} +
+ + + +
+
+
+ + {/* Content Grid */} +
+ {/* Left Column - Image & Stats */} +
+ {/* Event Poster */} + {event.poster && ( +
+ {event.name} +
+ )} + + {/* Quick Stats */} +
+

Event Details

+
+
+ Venue + {event.venue} +
+
+ Max Participants + {event.numberOfMember} +
+
+ Registration Fee + ₹{event.amount || 0} +
+
+
+
+ + {/* Right Column - Details */} +
+ {/* Description */} + {event.description && ( +
+

Description

+
+
+ )} + + {/* Rules & Links */} + {(event.ruleBook || event.qrCode) && ( +
+ {event.ruleBook && ( + + )} + + {event.qrCode && ( +
+

Payment QR Code

+
+ Payment QR Code +
+
+ )} +
+ )} + + {/* Coordinators */} + {event.coordinator && event.coordinator.length > 0 && ( +
+

Coordinators

+
+ {event.coordinator.map((coord, index) => ( +
+
+ + {coord.name?.charAt(0)?.toUpperCase() || 'C'} + +
+
+

{coord.name}

+

{coord.mobileNo}

+
+
+ ))} +
+
+ )} + + {/* Useful Links */} + {event.usefulLinks && event.usefulLinks.length > 0 && ( +
+

Useful Links

+
+ {event.usefulLinks.map((link, index) => ( + + + + + {link.title} + + ))} +
+
+ )} +
+
+
+ + {/* Delete Confirmation Modal - Rendered outside the main container */} + {showDeleteConfirm && ( +
setShowDeleteConfirm(false)} + > +
e.stopPropagation()} + > +
+
+ + + +
+

Delete Event

+

+ Are you sure you want to delete "{event.name}"? This action cannot be undone. +

+
+ + +
+
+
+
+ )} + + ); +}; + +export default EventDetails; diff --git a/frontend/src/components/EventDynamicListSection.jsx b/frontend/src/components/EventDynamicListSection.jsx index 212faea..a8143d2 100644 --- a/frontend/src/components/EventDynamicListSection.jsx +++ b/frontend/src/components/EventDynamicListSection.jsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import Accordion from "./Accordion"; import { DynamicList } from "./DynamicList"; export const EventDynamicListSection = ({ @@ -15,21 +14,32 @@ export const EventDynamicListSection = ({ }; return ( - <> - - +
+
+
+

{title}

+

+ Manage your event's {title.toLowerCase()}. Add, edit, or remove items as needed. +

+
- - - +
+ + +
+ +
+
+
+
); }; diff --git a/frontend/src/components/EventEditForm.jsx b/frontend/src/components/EventEditForm.jsx new file mode 100644 index 0000000..e083327 --- /dev/null +++ b/frontend/src/components/EventEditForm.jsx @@ -0,0 +1,581 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import FormField from "./form/FormField"; +import CustomRichTextEditor from "./form/CustomRichTextEditor"; +import { showToast } from "../utils/toast"; +import { updateEvent } from "../api/apiService"; + +const EventEditForm = ({ event, onSave, onCancel, isLoading }) => { + const navigate = useNavigate(); + const [currentStep, setCurrentStep] = useState(0); + const [formData, setFormData] = useState({ + isWorkshop: false, + name: "", + date: "", + venue: "", + numberOfMember: "", + poster: null, + description: "", + ruleBook: "", + amount: "", + qrCode: null, + coordinators: [], + usefulLinks: [] + }); + const [errors, setErrors] = useState({}); + const [loading, setLoading] = useState(false); + + // Initialize form data with existing event data + useEffect(() => { + if (event) { + setFormData({ + isWorkshop: event.isWorkshop || false, + name: event.name || "", + date: event.date ? event.date.split('T')[0] : "", // Convert ISO date to YYYY-MM-DD + venue: event.venue || "", + numberOfMember: event.numberOfMember || "", + poster: null, // Don't pre-fill files, user can choose to re-upload + description: event.description || "", + ruleBook: event.ruleBook || "", + amount: event.amount || "", + qrCode: null, // Don't pre-fill files, user can choose to re-upload + coordinators: event.coordinator || [], + usefulLinks: event.usefulLinks || [] + }); + } + }, [event]); + + // Compact 3-step design + const steps = [ + { + title: "Basic Details", + subtitle: "Event info, rules & poster" + }, + { + title: "Description", + subtitle: "Rich description of your event" + }, + { + title: "Team & Links", + subtitle: "Coordinators & resources" + } + ]; + + const validateStep = (step) => { + const newErrors = {}; + + switch (step) { + case 0: + if (formData.isWorkshop === null || formData.isWorkshop === undefined) newErrors.isWorkshop = "Please select a type"; + if (!formData.name?.trim()) newErrors.name = "Event name is required"; + if (!formData.date) newErrors.date = "Event date is required"; + if (!formData.venue?.trim()) newErrors.venue = "Venue is required"; + if (!formData.numberOfMember || formData.numberOfMember <= 0) newErrors.numberOfMember = "Number of members must be greater than 0"; + if (!formData.ruleBook?.trim()) newErrors.ruleBook = "Rulebook link is required"; + if (!formData.amount || formData.amount <= 0) newErrors.amount = "Registration fee must be greater than 0"; + break; + case 1: + if (!formData.description?.trim()) newErrors.description = "Description is required"; + break; + case 2: + if (!formData.coordinators?.length) newErrors.coordinators = "At least one coordinator is required"; + break; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const validateAllSteps = () => { + const allErrors = {}; + + if (formData.isWorkshop === null || formData.isWorkshop === undefined) allErrors.isWorkshop = "Please select a type"; + if (!formData.name?.trim()) allErrors.name = "Event name is required"; + if (!formData.date) allErrors.date = "Event date is required"; + if (!formData.venue?.trim()) allErrors.venue = "Venue is required"; + if (!formData.numberOfMember || formData.numberOfMember <= 0) allErrors.numberOfMember = "Number of members must be greater than 0"; + if (!formData.description?.trim()) allErrors.description = "Description is required"; + if (!formData.coordinators?.length) allErrors.coordinators = "At least one coordinator is required"; + + setErrors(allErrors); + return Object.keys(allErrors).length === 0; + }; + + const handleNext = () => { + if (validateStep(currentStep)) { + setCurrentStep(prev => Math.min(prev + 1, steps.length - 1)); + } else { + showToast.error("Please complete all required fields in this section."); + } + }; + + const handleBack = () => { + setCurrentStep(prev => Math.max(prev - 1, 0)); + }; + + const handleFieldChange = (field, value) => { + setFormData(prev => ({ ...prev, [field]: value })); + // Clear error when user starts typing + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: "" })); + } + }; + + const handleFileChange = (field, file) => { + setFormData(prev => ({ ...prev, [field]: file })); + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: "" })); + } + }; + + const handleCoordinatorChange = (index, field, value) => { + const updatedCoordinators = [...formData.coordinators]; + updatedCoordinators[index] = { ...updatedCoordinators[index], [field]: value }; + setFormData(prev => ({ ...prev, coordinators: updatedCoordinators })); + }; + + const addCoordinator = () => { + setFormData(prev => ({ + ...prev, + coordinators: [...prev.coordinators, { name: "", mobileNo: "" }] + })); + }; + + const removeCoordinator = (index) => { + setFormData(prev => ({ + ...prev, + coordinators: prev.coordinators.filter((_, i) => i !== index) + })); + }; + + const handleUsefulLinkChange = (index, field, value) => { + const updatedLinks = [...formData.usefulLinks]; + updatedLinks[index] = { ...updatedLinks[index], [field]: value }; + setFormData(prev => ({ ...prev, usefulLinks: updatedLinks })); + }; + + const addUsefulLink = () => { + setFormData(prev => ({ + ...prev, + usefulLinks: [...prev.usefulLinks, { title: "", link: "" }] + })); + }; + + const removeUsefulLink = (index) => { + setFormData(prev => ({ + ...prev, + usefulLinks: prev.usefulLinks.filter((_, i) => i !== index) + })); + }; + + const handleSubmit = async () => { + if (!validateAllSteps()) { + showToast.error("Please complete all required fields before submitting."); + return; + } + + setLoading(true); + try { + // Check if we have files to upload + const hasFiles = (formData.poster && formData.poster instanceof File) || + (formData.qrCode && formData.qrCode instanceof File); + + if (hasFiles) { + // Use FormData if files are present + const formDataToSend = new FormData(); + + // Basic fields + formDataToSend.append('isWorkshop', formData.isWorkshop); + formDataToSend.append('name', formData.name); + formDataToSend.append('date', formData.date); + formDataToSend.append('venue', formData.venue); + formDataToSend.append('numberOfMember', parseInt(formData.numberOfMember)); + formDataToSend.append('description', formData.description); + formDataToSend.append('ruleBook', formData.ruleBook || ""); + formDataToSend.append('amount', parseFloat(formData.amount) || 0); + + // Coordinators - send as JSON string for better backend parsing + const coordinators = formData.coordinators || []; + formDataToSend.append('coordinator', JSON.stringify(coordinators)); + + // Useful Links - send as JSON string for better backend parsing + const usefulLinks = formData.usefulLinks || []; + formDataToSend.append('usefulLinks', JSON.stringify(usefulLinks)); + + // Files + if (formData.poster && formData.poster instanceof File) { + formDataToSend.append('poster', formData.poster); + } + if (formData.qrCode && formData.qrCode instanceof File) { + formDataToSend.append('qrCode', formData.qrCode); + } + + console.log("=== FORM DATA BEING SENT ==="); + console.log("Event ID:", event._id || event.id); + console.log("FormData entries:"); + for (let [key, value] of formDataToSend.entries()) { + console.log(`${key}:`, value); + } + console.log("=== END FORM DATA ==="); + + if (onSave) { + await onSave(formDataToSend); + } + } else { + // Use JSON if no files (more reliable for backend parsing) + const jsonData = { + isWorkshop: formData.isWorkshop, + name: formData.name, + date: formData.date, + venue: formData.venue, + numberOfMember: parseInt(formData.numberOfMember), + description: formData.description, + ruleBook: formData.ruleBook || "", + amount: parseFloat(formData.amount) || 0, + coordinator: formData.coordinators || [], + usefulLinks: formData.usefulLinks || [] + }; + + console.log("=== JSON DATA BEING SENT ==="); + console.log("Event ID:", event._id || event.id); + console.log("JSON Data:", jsonData); + console.log("=== END JSON DATA ==="); + + if (onSave) { + await onSave(jsonData); + } + } + + } catch (error) { + console.error('Failed to update:', error); + showToast.error(error.message || "Failed to update event. Please try again."); + } finally { + setLoading(false); + } + }; + + const renderStepContent = () => { + switch (currentStep) { + case 0: + return ( +
+ {/* Event Type */} +
+ +
+ + +
+ {errors.isWorkshop &&

{errors.isWorkshop}

} +
+ + {/* Basic Info */} +
+ handleFieldChange('name', e.target.value)} + error={errors.name} + required + /> + handleFieldChange('date', e.target.value)} + error={errors.date} + required + /> + handleFieldChange('venue', e.target.value)} + error={errors.venue} + required + /> + handleFieldChange('numberOfMember', e.target.value)} + error={errors.numberOfMember} + required + /> +
+ + {/* Rules & Payment */} +
+ handleFieldChange('ruleBook', e.target.value)} + error={errors.ruleBook} + required + /> + handleFieldChange('amount', e.target.value)} + error={errors.amount} + required + /> +
+ + {/* Files */} +
+
+ + handleFileChange('poster', e.target.files[0])} + className="block w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-600 file:text-white hover:file:bg-blue-700 file:cursor-pointer bg-gray-800/50 border border-gray-700/50 rounded-lg p-2" + /> + {errors.poster &&

{errors.poster}

} +
+ +
+ + handleFileChange('qrCode', e.target.files[0])} + className="block w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-blue-600 file:text-white hover:file:bg-blue-700 file:cursor-pointer bg-gray-800/50 border border-gray-700/50 rounded-lg p-2" + /> + {errors.qrCode &&

{errors.qrCode}

} +
+
+
+ ); + + case 1: + return ( +
+ handleFieldChange('description', value)} + placeholder="Describe your event in detail..." + height="500px" + /> + {errors.description &&

{errors.description}

} +
+ ); + + case 2: + return ( +
+ {/* Coordinators */} +
+
+ + +
+ + {formData.coordinators.map((coordinator, index) => ( +
+ handleCoordinatorChange(index, 'name', e.target.value)} + required + /> + handleCoordinatorChange(index, 'mobileNo', e.target.value)} + required + /> +
+ +
+
+ ))} + {errors.coordinators &&

{errors.coordinators}

} +
+ + {/* Useful Links */} +
+
+ + +
+ + {formData.usefulLinks.map((link, index) => ( +
+ handleUsefulLinkChange(index, 'title', e.target.value)} + /> + handleUsefulLinkChange(index, 'link', e.target.value)} + /> +
+ +
+
+ ))} +
+
+ ); + + default: + return null; + } + }; + + return ( +
+ {/* Header */} +
+

Edit Event

+

Update your event details

+
+ + {/* Progress Steps */} +
+
+ {steps.map((step, index) => ( +
+
+ {index + 1} +
+
+

+ {step.title} +

+

{step.subtitle}

+
+ {index < steps.length - 1 && ( +
+ )} +
+ ))} +
+
+ + {/* Form Content */} +
+ {renderStepContent()} +
+ + {/* Action Buttons */} +
+ + +
+ {currentStep > 0 && ( + + )} + + {currentStep < steps.length - 1 ? ( + + ) : ( + + )} +
+
+
+ ); +}; + +export default EventEditForm; diff --git a/frontend/src/components/EventForm.jsx b/frontend/src/components/EventForm.jsx index 8915f26..f2df607 100644 --- a/frontend/src/components/EventForm.jsx +++ b/frontend/src/components/EventForm.jsx @@ -1,84 +1,147 @@ -import React from "react"; +import React, { useContext } from "react"; import { EventDynamicListSection } from "./EventDynamicListSection"; import { EventFormProvider } from "../context/EventFormContext"; import { EventFormSection } from "./EventFormSection"; import { EventDescriptionSection } from "./EventDescripitonSection"; import { EventFormControls } from "./EventFormControls"; +import { EventFormContext } from "../context/EventFormContext"; + +// Inner component that can access the context +const EventFormContent = ({ disabled, onSave, isLoading }) => { + const { eventData, updatedValues } = useContext(EventFormContext); + + const handleSave = async () => { + if (onSave) { + const formDataToSend = { ...eventData, ...updatedValues }; + await onSave(formDataToSend); + } + }; -export const EventForm = ({ initialData, disabled }) => { return ( - <> - -
-
- - - - - - +
+
+ + + + + + +
+ + {/* Enhanced Action Buttons */} +
+ {onSave ? ( +
+
+ {isLoading ? ( +
+
+ Processing... +
+ ) : ( +
+ + + + Review your changes before saving +
+ )} +
+ +
+ + +
-
+ ) : ( +
-
+ )} +
+
+ ); +}; + +export const EventForm = ({ initialData, disabled, onSave, isLoading }) => { + return ( + <> + + ); diff --git a/frontend/src/components/EventFormControls.jsx b/frontend/src/components/EventFormControls.jsx index d53fcc8..ea15479 100644 --- a/frontend/src/components/EventFormControls.jsx +++ b/frontend/src/components/EventFormControls.jsx @@ -48,32 +48,35 @@ export const EventFormControls = ({ disabled }) => { if (response) navigate("/events/manage"); }; + return ( - <> -
- {loading ? ( -
Processing...
- ) : ( - "" - )} - +
+ {loading ? ( +
+
+ Processing... +
+ ) : null} + + - -
- + +
); }; diff --git a/frontend/src/components/EventFormSection.jsx b/frontend/src/components/EventFormSection.jsx index f9e58cb..2759ff7 100644 --- a/frontend/src/components/EventFormSection.jsx +++ b/frontend/src/components/EventFormSection.jsx @@ -1,5 +1,4 @@ import React, { useState, useContext } from "react"; -import Accordion from "./Accordion"; import { EventFormContext } from "../context/EventFormContext"; import { Input } from "./Input"; import { readFile } from "../utils/readFile"; @@ -36,39 +35,54 @@ export const EventFormSection = ({ title, section, fields, disabled }) => { }; return ( - <> - - {fields.map((field, index) => { - return ( - { - field.type == "file" - ? handleFileChange(e, field.name) - : handleInputChange(e, field.name); - }} - /> - ); - })} +
+
+ {/* Header */} +
+

{title}

+

+ Fill in the details for your event's {title.toLowerCase()} +

+
- - - + {/* Form Fields */} +
+
+ {fields.map((field, index) => ( +
+ { + field.type == "file" + ? handleFileChange(e, field.name) + : handleInputChange(e, field.name); + }} + /> +
+ ))} +
+ + {/* Action Button */} +
+ +
+
+
+
); }; diff --git a/frontend/src/components/EventSection.jsx b/frontend/src/components/EventSection.jsx index 8b087a9..5da5019 100644 --- a/frontend/src/components/EventSection.jsx +++ b/frontend/src/components/EventSection.jsx @@ -1,28 +1,37 @@ import React from "react"; import EventCard from "./EventCard"; -const EventsSection = ({ title, events, isLive, navigate }) => { +const EventsSection = ({ title, events, navigate, emptyMessage = "No events found" }) => { + if (!events || events.length === 0) { + return ( +
+
+ + {title} + +
+
+
{emptyMessage}
+
+
+ ); + } + return ( -
-
- - {title} +
+
+ + {title} ({events.length})
-
- {events.map((event) => ( +
+ {events.map((event, index) => ( navigate("/events/manage/event", { state: { event } }) } - isLive={isLive} /> ))}
diff --git a/frontend/src/components/Input.jsx b/frontend/src/components/Input.jsx index 011bde9..6c70807 100644 --- a/frontend/src/components/Input.jsx +++ b/frontend/src/components/Input.jsx @@ -13,33 +13,65 @@ export const Input = ({ hidden, }) => { return ( - <> -
- - - {type == "file" && ( - +
+
+ + + {type === "file" ? ( +
+ + + {fileName && ( +

+ + + + File selected: {fileName} +

+ )} +
+ ) : ( + )}
- +
); }; diff --git a/frontend/src/components/MultiPageEventForm.jsx b/frontend/src/components/MultiPageEventForm.jsx new file mode 100644 index 0000000..5450000 --- /dev/null +++ b/frontend/src/components/MultiPageEventForm.jsx @@ -0,0 +1,528 @@ +import React, { useState, useContext } from "react"; +import { EventFormProvider } from "../context/EventFormContext"; +import { EventFormControls } from "./EventFormControls"; +import StepProgress from "./form/StepProgress"; +import FormActions from "./form/FormActions"; +import FormField from "./form/FormField"; +import CustomRichTextEditor from "./form/CustomRichTextEditor"; +import { showToast } from "../utils/toast"; +import { createEvent } from "../api/apiService"; +import { useNavigate } from "react-router-dom"; + +const MultiPageEventForm = ({ initialData, disabled, onSave, isLoading }) => { + const navigate = useNavigate(); + const [currentStep, setCurrentStep] = useState(0); + const [formData, setFormData] = useState({ + isWorkshop: false, + name: "", + date: "", + venue: "", + numberOfMember: "", + poster: null, + description: "", + ruleBook: "", + amount: "", + qrCode: null, + coordinators: [], + usefulLinks: [] + }); + const [errors, setErrors] = useState({}); + const [loading, setLoading] = useState(false); + + // Compact 3-step design + const steps = [ + { + title: "Basic Details", + subtitle: "Event info, rules & poster" + }, + { + title: "Description", + subtitle: "Rich description of your event" + }, + { + title: "Team & Links", + subtitle: "Coordinators & resources" + } + ]; + + const validateStep = (step) => { + const newErrors = {}; + + switch (step) { + case 0: + if (formData.isWorkshop === null || formData.isWorkshop === undefined) newErrors.isWorkshop = "Please select a type"; + if (!formData.name?.trim()) newErrors.name = "Event name is required"; + if (!formData.date) newErrors.date = "Event date is required"; + if (!formData.venue?.trim()) newErrors.venue = "Venue is required"; + if (!formData.numberOfMember || formData.numberOfMember <= 0) newErrors.numberOfMember = "Number of members must be greater than 0"; + if (!formData.poster) newErrors.poster = "Event poster is required"; + if (!formData.ruleBook?.trim()) newErrors.ruleBook = "Rulebook link is required"; + if (!formData.amount || formData.amount <= 0) newErrors.amount = "Registration fee must be greater than 0"; + break; + case 1: + if (!formData.description?.trim()) newErrors.description = "Description is required"; + break; + case 2: + if (!formData.coordinators?.length) newErrors.coordinators = "At least one coordinator is required"; + break; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const validateAllSteps = () => { + const allErrors = {}; + + if (formData.isWorkshop === null || formData.isWorkshop === undefined) allErrors.isWorkshop = "Please select a type"; + if (!formData.name?.trim()) allErrors.name = "Event name is required"; + if (!formData.date) allErrors.date = "Event date is required"; + if (!formData.venue?.trim()) allErrors.venue = "Venue is required"; + if (!formData.numberOfMember || formData.numberOfMember <= 0) allErrors.numberOfMember = "Number of members must be greater than 0"; + if (!formData.poster) allErrors.poster = "Event poster is required"; + if (!formData.description?.trim()) allErrors.description = "Description is required"; + if (!formData.coordinators?.length) allErrors.coordinators = "At least one coordinator is required"; + + setErrors(allErrors); + return Object.keys(allErrors).length === 0; + }; + + const handleNext = () => { + if (validateStep(currentStep)) { + setCurrentStep(prev => Math.min(prev + 1, steps.length - 1)); + } else { + showToast.error("Please complete all required fields in this section."); + } + }; + + const handleBack = () => { + setCurrentStep(prev => Math.max(prev - 1, 0)); + }; + + const handleStepClick = (stepIndex) => { + setCurrentStep(stepIndex); + }; + + const handleFieldChange = (field, value) => { + if (field === 'poster' && value) { + const file = value; + const maxSize = 16 * 1024; + + if (file.size > maxSize) { + setErrors(prev => ({ + ...prev, + poster: `File size must be less than 16KB. Current size: ${(file.size / 1024).toFixed(1)}KB` + })); + return; + } + } + + setFormData(prev => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors(prev => ({ ...prev, [field]: null })); + } + }; + + const handleSubmit = async () => { + if (!validateAllSteps()) { + showToast.largeError("Please complete all required fields before submitting the form."); + return; + } + + setLoading(true); + try { + const formDataToSend = new FormData(); + + formDataToSend.append('isWorkshop', formData.isWorkshop); + formDataToSend.append('name', formData.name); + formDataToSend.append('date', formData.date); + formDataToSend.append('venue', formData.venue); + formDataToSend.append('numberOfMember', parseInt(formData.numberOfMember)); + formDataToSend.append('description', formData.description); + formDataToSend.append('ruleBook', formData.ruleBook || ""); + formDataToSend.append('amount', parseFloat(formData.amount) || 0); + + const coordinators = formData.coordinators || []; + coordinators.forEach((coord, index) => { + formDataToSend.append(`coordinator[${index}][name]`, coord.name); + formDataToSend.append(`coordinator[${index}][mobileNo]`, coord.mobileNo); + }); + + const usefulLinks = formData.usefulLinks || []; + usefulLinks.forEach((link, index) => { + formDataToSend.append(`usefulLinks[${index}][title]`, link.title); + formDataToSend.append(`usefulLinks[${index}][link]`, link.link); + }); + + if (formData.poster) { + formDataToSend.append('poster', formData.poster); + } else { + formDataToSend.append('poster', ''); + } + if (formData.qrCode) { + formDataToSend.append('qrCode', formData.qrCode); + } + + if (onSave) { + await onSave(formDataToSend); + } else { + const response = await createEvent(formDataToSend); + showToast.largeSuccess("Event created successfully!"); + navigate("/events/manage"); + } + + } catch (error) { + console.error('Failed to submit:', error); + showToast.largeError(error.message || "Failed to create event. Please try again."); + } finally { + setLoading(false); + } + }; + + const renderStepContent = () => { + switch (currentStep) { + case 0: + return ( +
+ {/* Type Selection - Compact */} +
+ +
+ + +
+ {errors.isWorkshop && ( +

{errors.isWorkshop}

+ )} +
+ + {/* Basic Details - Compact Grid */} +
+ handleFieldChange('name', e.target.value)} + error={errors.name} + placeholder="Enter event name" + required + /> + handleFieldChange('date', e.target.value)} + error={errors.date} + required + /> + handleFieldChange('venue', e.target.value)} + error={errors.venue} + placeholder="Event location" + required + /> + handleFieldChange('numberOfMember', e.target.value)} + error={errors.numberOfMember} + placeholder="0" + required + /> +
+ + {/* Rules & Fees - Compact Grid */} +
+ handleFieldChange('ruleBook', e.target.value)} + error={errors.ruleBook} + placeholder="https://..." + required + /> + handleFieldChange('amount', e.target.value)} + error={errors.amount} + placeholder="0" + required + /> +
+ + {/* File Uploads - Compact */} +
+ { + const file = e.target.files[0]; + if (file) { + handleFieldChange('poster', file); + } + }} + error={errors.poster} + accept="image/*" + required + helperText="Max 16KB" + /> + { + const file = e.target.files[0]; + if (file) { + handleFieldChange('qrCode', file); + } + }} + accept="image/*" + /> +
+
+ ); + + case 1: + return ( +
+ {/* Custom Rich Text Editor - Full Page */} +
+ + handleFieldChange('description', value)} + placeholder="Describe your event in detail. Use the toolbar above to format your text with bold, italic, headings, lists, and links..." + height="600px" + /> + {errors.description && ( +

{errors.description}

+ )} +
+
+ ); + + case 2: + return ( +
+ {/* Coordinators - Compact */} +
+
+
+

Coordinators

+

Add event coordinators

+
+ +
+ +
+ {(formData.coordinators || []).map((coordinator, index) => ( +
+ { + const updated = [...(formData.coordinators || [])]; + updated[index] = { ...updated[index], name: e.target.value }; + handleFieldChange('coordinators', updated); + }} + className="flex-1 px-2 py-1 text-sm bg-white/5 border border-white/20 rounded text-white placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500" + /> + { + const updated = [...(formData.coordinators || [])]; + updated[index] = { ...updated[index], mobileNo: e.target.value }; + handleFieldChange('coordinators', updated); + }} + className="flex-1 px-2 py-1 text-sm bg-white/5 border border-white/20 rounded text-white placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500" + /> + +
+ ))} + {(formData.coordinators || []).length === 0 && ( +

No coordinators added

+ )} +
+ {errors.coordinators && ( +

{errors.coordinators}

+ )} +
+ + {/* Useful Links - Compact */} +
+
+
+

Useful Links

+

Add relevant resources

+
+ +
+ +
+ {(formData.usefulLinks || []).map((link, index) => ( +
+ { + const updated = [...(formData.usefulLinks || [])]; + updated[index] = { ...updated[index], title: e.target.value }; + handleFieldChange('usefulLinks', updated); + }} + className="flex-1 px-2 py-1 text-sm bg-white/5 border border-white/20 rounded text-white placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500" + /> + { + const updated = [...(formData.usefulLinks || [])]; + updated[index] = { ...updated[index], link: e.target.value }; + handleFieldChange('usefulLinks', updated); + }} + className="flex-1 px-2 py-1 text-sm bg-white/5 border border-white/20 rounded text-white placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500" + /> + +
+ ))} + {(formData.usefulLinks || []).length === 0 && ( +

No links added

+ )} +
+
+
+ ); + + default: + return null; + } + }; + + return ( + +
+
+ {/* Compact Header */} +
+

+ {onSave ? "Edit Event" : "Create Event"} +

+

+ {onSave ? "Update your event" : "Build your event in 3 simple steps"} +

+
+ + {/* Progress Bar */} +
+ +
+ + {/* Compact Form Container */} +
+ {/* Step Header - Compact */} +
+

+ {steps[currentStep].title} +

+

+ {steps[currentStep].subtitle} +

+
+ + {/* Form Content - Compact */} +
+ {renderStepContent()} +
+ + {/* Actions - Compact */} +
+ +
+
+
+
+
+ ); +}; + +export default MultiPageEventForm; diff --git a/frontend/src/components/PageLayout.jsx b/frontend/src/components/PageLayout.jsx index e7373af..801cbf2 100644 --- a/frontend/src/components/PageLayout.jsx +++ b/frontend/src/components/PageLayout.jsx @@ -6,11 +6,13 @@ const PageLayout = ({ title, children }) => { return ( <>
-
+
{title && ( -

- {title} -

+
+

+ {title} +

+
)} {children}
diff --git a/frontend/src/components/SearchBar.jsx b/frontend/src/components/SearchBar.jsx new file mode 100644 index 0000000..a2f71e9 --- /dev/null +++ b/frontend/src/components/SearchBar.jsx @@ -0,0 +1,100 @@ +import React, { useState, useEffect } from "react"; + +const SearchBar = ({ onSearch, placeholder = "Search events..." }) => { + const [searchTerm, setSearchTerm] = useState(""); + const [searchType, setSearchType] = useState("All"); + + useEffect(() => { + const timer = setTimeout(() => { + onSearch(searchTerm, searchType); + }, 300); + + return () => clearTimeout(timer); + }, [searchTerm, searchType, onSearch]); + + const getSearchIcon = () => { + if (searchType === "Date") { + return ( + + + + ); + } else if (searchType === "Status") { + return ( + + + + ); + } + return ( + + + + ); + }; + + const detectSearchType = (value) => { + const term = value.toLowerCase().trim(); + + // Date detection (YYYY-MM-DD or MM/DD/YYYY) + const dateRegex = /^\d{4}-\d{2}-\d{2}$|^\d{1,2}\/\d{1,2}\/\d{4}$/; + if (dateRegex.test(term)) { + return "Date"; + } + + // Status detection + const statusTerms = ['past', 'live', 'upcoming']; + if (statusTerms.includes(term)) { + return "Status"; + } + + return "All"; + }; + + const handleInputChange = (e) => { + const value = e.target.value; + setSearchTerm(value); + setSearchType(detectSearchType(value)); + }; + + return ( +
+
+
+
+ {getSearchIcon()} +
+
+ + {searchTerm && ( + + )} +
+ + {/* Search Type Indicator */} + {searchType !== "All" && ( +
+ Searching by: {searchType} +
+ )} +
+ ); +}; + +export default SearchBar; diff --git a/frontend/src/components/Tabs.jsx b/frontend/src/components/Tabs.jsx index 85ab050..0c1d0ef 100644 --- a/frontend/src/components/Tabs.jsx +++ b/frontend/src/components/Tabs.jsx @@ -4,23 +4,26 @@ const Tabs = ({ tabs, content }) => { const [active, setActive] = useState(0); return ( -
+
{/* Tab Headers */} -
+
{tabs.map((tab, index) => ( -
setActive(index)} + className={`px-6 py-3 text-sm font-medium transition-all duration-200 ${ + active === index + ? "text-blue-400 border-b-2 border-blue-400 bg-blue-400/5" + : "text-gray-400 hover:text-gray-300 hover:bg-gray-800/30" + }`} > {tab} -
+ ))}
+ {/* Tab Content */} -
+
{content[active]}
diff --git a/frontend/src/components/form/CustomRichTextEditor.jsx b/frontend/src/components/form/CustomRichTextEditor.jsx new file mode 100644 index 0000000..e359b20 --- /dev/null +++ b/frontend/src/components/form/CustomRichTextEditor.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import MDEditor from "@uiw/react-md-editor"; + +const CustomRichTextEditor = ({ + value = "", + onChange, + placeholder = "Describe your event...", + height = "600px" +}) => { + const handleChange = (val) => { + onChange(val || ''); + }; + + return ( +
+ {/* Editor Header */} +
+
+ = 100 && value.length <= 2000 ? 'text-green-400' : 'text-gray-400'}`}> + {value.length} characters + + + {value.split(/\s+/).filter(word => word.length > 0).length} words + +
+
+ + {/* Editor */} +
+ +
+
+ ); +}; + +export default CustomRichTextEditor; diff --git a/frontend/src/components/form/DescriptionField.jsx b/frontend/src/components/form/DescriptionField.jsx new file mode 100644 index 0000000..bcc8b26 --- /dev/null +++ b/frontend/src/components/form/DescriptionField.jsx @@ -0,0 +1,106 @@ +import React, { useState, useEffect, useCallback } from "react"; + +const DescriptionField = ({ + value = "", + onChange, + placeholder = "Describe your event...", + storageKey = "event-description-draft", + enablePreview = true +}) => { + const [localValue, setLocalValue] = useState(value); + const [showPreview, setShowPreview] = useState(false); + const [lastSaved, setLastSaved] = useState(null); + const [isSaving, setIsSaving] = useState(false); + + // Load draft from localStorage on mount + useEffect(() => { + const saved = localStorage.getItem(storageKey); + if (saved && !value) { + setLocalValue(saved); + onChange?.(saved); + } + }, [storageKey, value, onChange]); + + // Debounced autosave + const saveToStorage = useCallback( + (() => { + let timeoutId; + return (text) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + localStorage.setItem(storageKey, text); + setLastSaved(new Date()); + setIsSaving(false); + }, 1000); + setIsSaving(true); + }; + })(), + [storageKey] + ); + + // Handle text changes + const handleChange = (e) => { + const newValue = e.target.value; + setLocalValue(newValue); + onChange?.(newValue); + saveToStorage(newValue); + }; + + const characterCount = localValue.length; + const isGoodLength = characterCount >= 100 && characterCount <= 1000; + + return ( +
+ {/* Toolbar */} +
+
+ + {characterCount} characters + + {lastSaved && ( + + Draft saved {lastSaved.toLocaleTimeString()} + + )} + {isSaving && ( + Saving... + )} +
+ + {enablePreview && ( + + )} +
+ + {/* Textarea or Preview */} + {!showPreview ? ( +