diff --git a/.gitignore b/.gitignore index de44e4f..5cbdccf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ /.pnp .pnp.js +# debugging images +/images + # testing /coverage diff --git a/docs/documentation.md b/docs/documentation.md index 33b401f..cff9114 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -184,7 +184,7 @@ Event region and city options live in `src/lib/eventOptions.ts`. - Supports filters for `employmentType`, `compensationType`, `organizationIndustry`, and `jobStatus`. - Non-admin requests are paginated with `page` and `limit`. - Admin requests use `admin=true` and return all matching records. -- Approved non-admin results exclude old expired approved jobs by approved date. +- Approved non-admin results include all approved jobs, regardless of approval date. - Sorting prioritizes `memberJob` first, then approved date or post date. `POST /api/jobs` @@ -205,8 +205,8 @@ Event region and city options live in `src/lib/eventOptions.ts`. - Requires `nonprofit` or `spokes_admin`. - Nonprofits can update only their own jobs. - Nonprofit edits send the job back to `pending`. -- Supports renewal and unpublish actions. -- Admin status changes can approve, reject, or expire jobs. +- Supports renewal and resolve actions. +- Admin status changes can approve, reject, or resolve jobs. `DELETE /api/jobs/[jobId]` @@ -362,18 +362,18 @@ Shows the authenticated user's jobs. Organization users can: - Edit jobs - View rejection feedback -- Renew expired jobs -- Unpublish approved jobs +- Reopen resolved jobs +- Resolve approved jobs ### Job Admin Dashboard File: `src/components/jobs/pages/JobsAdminPage.tsx` -Shows pending, approved, rejected, and expired jobs. Admins can: +Shows pending, approved, rejected, and resolved jobs. Admins can: - Approve jobs - Reject jobs with a reason -- Renew or expire jobs through status actions +- Reopen or resolve jobs through status actions - Refresh dashboard data - Navigate to user management diff --git a/docs/owner-user-guide.md b/docs/owner-user-guide.md index aef8e82..afedc47 100644 --- a/docs/owner-user-guide.md +++ b/docs/owner-user-guide.md @@ -62,17 +62,17 @@ Approved jobs appear on the public job board. Rejected jobs stay available to th - Pending: waiting for Spokes admin review. - Approved: visible on the public job board. - Rejected: not public; the organization can view feedback. -- Expired: no longer active. +- Resolved: no longer active or visible on the public job board. -Approved jobs expire after their active period. The admin dashboard checks for expired jobs when it loads and periodically while open. +Jobs stay active until the posting organization resolves or deletes them. -### Edit, Renew, or Unpublish Jobs +### Edit, Reopen, or Resolve Jobs Organizations can use `/jobs/manage` to manage their own jobs. - Edit: updates the job and sends it back to pending review. -- Renew: restarts an expired job. -- Unpublish: marks an approved job as expired so it is no longer active. +- Reopen: sends a resolved job back for review. +- Resolve Job: marks an approved job as resolved so it is no longer active. - View Feedback: shows the admin's rejection reason. ## Managing Events @@ -193,7 +193,7 @@ Check its status: - Pending items are waiting for approval. - Rejected items are not public. -- Expired jobs are not active. +- Resolved jobs are not active. - Approved items should appear publicly. Also try refreshing the admin page with the refresh button. @@ -236,7 +236,7 @@ Owners can usually handle: - Updating user organizations - Deleting users - Deleting organizations -- Renewing or unpublishing jobs from the organization dashboard +- Reopening or resolving jobs from the organization dashboard Owners should ask a technical maintainer for: diff --git a/package.json b/package.json index 87610ca..05decb5 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "framer-motion": "^11.15.0", "lucide-react": "^0.475.0", "mongoose": "^8.14.1", - "next": ">=14.2.25", + "next": "16.2.6", "next-themes": "^0.4.4", "react": "^18", "react-dom": "^18", diff --git a/src/app/api/__test__/AdminPage.test.tsx b/src/app/api/__test__/AdminPage.test.tsx index 5d585de..17303cf 100644 --- a/src/app/api/__test__/AdminPage.test.tsx +++ b/src/app/api/__test__/AdminPage.test.tsx @@ -190,8 +190,8 @@ describe("Admin Jobs Page", () => { }); expect(screen.getByText("Org2")).toBeInTheDocument(); - // Click on Expired Jobs tab - fireEvent.click(screen.getByText(/Expired Jobs/i)); + // Click on Resolved Jobs tab + fireEvent.click(screen.getByText(/Resolved Jobs/i)); // Check for expired jobs await waitFor(() => { diff --git a/src/app/api/__test__/EventsApi.test.ts b/src/app/api/__test__/EventsApi.test.ts index 159a77c..e655db2 100644 --- a/src/app/api/__test__/EventsApi.test.ts +++ b/src/app/api/__test__/EventsApi.test.ts @@ -2,7 +2,7 @@ import Event from "@/database/eventSchema"; import User from "@/database/userSchema"; import { resolveOrganizationName } from "@/lib/organizations"; import { GET, POST } from "@/app/api/events/route"; -import { GET as GET_EVENT, PUT } from "@/app/api/events/[eventId]/route"; +import { DELETE, GET as GET_EVENT, PUT } from "@/app/api/events/[eventId]/route"; const mockAuth = { userId: "user-1", @@ -26,6 +26,7 @@ jest.mock("@/database/eventSchema", () => ({ find: jest.fn(), findById: jest.fn(), findByIdAndUpdate: jest.fn(), + findByIdAndDelete: jest.fn(), findOneAndUpdate: jest.fn(), }, })); @@ -241,11 +242,59 @@ describe("Events API", () => { locationLink: "https://maps.example.com/updated-event", eventLocationGeneral: "North Coast", eventLocationCity: "Morro Bay", + eventStatus: "pending", + rejectionMessage: "", }, { new: true, strict: false }, ); }); + test("resubmits an owned approved event for review after editing", async () => { + (Event.findById as jest.Mock).mockResolvedValue({ + _id: "event-1", + createdByUserId: "user-1", + eventStatus: "approved", + }); + (Event.findByIdAndUpdate as jest.Mock).mockResolvedValue({ _id: "event-1", eventStatus: "pending" }); + + const response = await PUT(jsonRequest("/api/events/event-1", { eventName: "Updated Event" }), {}); + + expect(response.status).toBe(200); + expect(Event.findByIdAndUpdate).toHaveBeenCalledWith( + "event-1", + { eventName: "Updated Event", eventStatus: "pending", rejectionMessage: "" }, + { new: true, strict: false }, + ); + }); + + test("allows an admin to reject an event with feedback", async () => { + mockAuth.role = "spokes_admin"; + (Event.findById as jest.Mock).mockResolvedValue({ _id: "event-1", createdByUserId: "user-1" }); + (Event.findByIdAndUpdate as jest.Mock).mockResolvedValue({ _id: "event-1", eventStatus: "rejected" }); + + const response = await PUT( + jsonRequest("/api/events/event-1", { eventStatus: "rejected", rejectionMessage: "Missing event details" }), + {}, + ); + + expect(response.status).toBe(200); + expect(Event.findByIdAndUpdate).toHaveBeenCalledWith( + "event-1", + { eventStatus: "rejected", rejectionMessage: "Missing event details" }, + { new: true }, + ); + }); + + test("allows an owner to delete an event", async () => { + (Event.findById as jest.Mock).mockResolvedValue({ _id: "event-1", createdByUserId: "user-1" }); + (Event.findByIdAndDelete as jest.Mock).mockResolvedValue({ _id: "event-1" }); + + const response = await DELETE({ nextUrl: { pathname: "/api/events/event-1" } } as any, {}); + + expect(response.status).toBe(200); + expect(Event.findByIdAndDelete).toHaveBeenCalledWith("event-1"); + }); + test("GET blocks non-admin requests for private event statuses", async () => { mockAuth.userId = null as any; mockAuth.role = "job_seeker"; diff --git a/src/app/api/__test__/EventsValidation.test.ts b/src/app/api/__test__/EventsValidation.test.ts index cb0e709..139ba7c 100644 --- a/src/app/api/__test__/EventsValidation.test.ts +++ b/src/app/api/__test__/EventsValidation.test.ts @@ -49,4 +49,16 @@ describe("event validation service", () => { expect(validateEventPayload({ majorFundraisingEvent: false }, { partial: true })).toBeNull(); }); + + test("allows a new event without a time or venue", () => { + const payload = { ...validPayload, time: "", location: "" }; + + expect(validateEventPayload(payload)).toBeNull(); + expect(sanitizeEventPayload(payload)).toEqual( + expect.objectContaining({ + time: "", + location: "", + }), + ); + }); }); diff --git a/src/app/api/__test__/JobsApi.test.ts b/src/app/api/__test__/JobsApi.test.ts index ba1800f..0722f7e 100644 --- a/src/app/api/__test__/JobsApi.test.ts +++ b/src/app/api/__test__/JobsApi.test.ts @@ -186,7 +186,7 @@ describe("Jobs API", () => { expect(Job.find).not.toHaveBeenCalled(); }); - test("GET defaults public job listings to approved, unexpired jobs", async () => { + test("GET defaults public job listings to all approved jobs", async () => { mockAuth.userId = null as any; mockAuth.role = "job_seeker"; const limit = jest.fn().mockResolvedValue([]); @@ -202,10 +202,7 @@ describe("Jobs API", () => { ); expect(response.status).toBe(200); - expect(Job.find).toHaveBeenCalledWith({ - jobStatus: "approved", - approvedDate: { $gte: expect.any(Date) }, - }); + expect(Job.find).toHaveBeenCalledWith({ jobStatus: "approved" }); }); test("GET job detail hides private jobs from anonymous users", async () => { @@ -224,6 +221,34 @@ describe("Jobs API", () => { expect(result.message).toBe("Job not found"); }); + test("GET job detail keeps an approved legacy job public without an approval date", async () => { + mockAuth.userId = null as any; + mockAuth.role = "job_seeker"; + (Job.findById as jest.Mock).mockResolvedValue({ + _id: "job-1", + userId: "user-1", + jobStatus: "approved", + }); + + const response = await GET_JOB({ nextUrl: { pathname: "/api/jobs/job-1" } } as any, {}); + + expect(response.status).toBe(200); + }); + + test("allows an organization to resolve its job", async () => { + (Job.findById as jest.Mock).mockResolvedValue({ _id: "job-1", userId: "user-1", jobStatus: "approved" }); + (Job.findByIdAndUpdate as jest.Mock).mockResolvedValue({ _id: "job-1", jobStatus: "expired" }); + + const response = await PUT(jsonRequest("/api/jobs/job-1", { isResolve: true }), {}); + + expect(response.status).toBe(200); + expect(Job.findByIdAndUpdate).toHaveBeenCalledWith( + "job-1", + { jobStatus: "expired", modifiedDate: expect.any(Date) }, + { new: true }, + ); + }); + test("PUT only writes whitelisted mutable job fields", async () => { (Job.findById as jest.Mock).mockResolvedValue({ _id: "job-1", diff --git a/src/app/api/events/[eventId]/route.ts b/src/app/api/events/[eventId]/route.ts index bb83ecd..7a2b439 100644 --- a/src/app/api/events/[eventId]/route.ts +++ b/src/app/api/events/[eventId]/route.ts @@ -80,11 +80,6 @@ export const PUT = withApiAuth( return NextResponse.json({ message: "Event status updated successfully", event: updatedEvent }); } - // Handle regular updates (nonprofit can update their own pending events) - if (auth.role === "nonprofit" && existingEvent.eventStatus !== "pending") { - return NextResponse.json({ message: "Can only edit pending events" }, { status: 403 }); - } - const validationError = validateEventPayload(eventData, { partial: true }); if (validationError) { return NextResponse.json({ message: validationError }, { status: 400 }); @@ -95,7 +90,13 @@ export const PUT = withApiAuth( return NextResponse.json({ message: "No valid event fields provided" }, { status: 400 }); } - const updatedEvent = await Event.findByIdAndUpdate(eventId, sanitizedEventData, { new: true, strict: false }); + // Organization changes must be reviewed again. Admin edits remain live because + // they are already made by the reviewing role. + const updateData = + auth.role === "nonprofit" + ? { ...sanitizedEventData, eventStatus: EventStatus.pending, rejectionMessage: "" } + : sanitizedEventData; + const updatedEvent = await Event.findByIdAndUpdate(eventId, updateData, { new: true, strict: false }); return NextResponse.json({ message: "Event updated successfully", event: updatedEvent }); } catch (error) { return NextResponse.json({ message: "Error updating event", error }, { status: 500 }); diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts index 07a573b..8b86633 100644 --- a/src/app/api/events/route.ts +++ b/src/app/api/events/route.ts @@ -94,8 +94,8 @@ export const POST = withApiAuth( organization, date: new Date(sanitizedEventData.date as string), eventName: sanitizedEventData.eventName as string, - time: sanitizedEventData.time as string, - location: sanitizedEventData.location as string, + time: (sanitizedEventData.time as string) || "", + location: (sanitizedEventData.location as string) || "", }; const duplicateUpdateFields: Record = {}; const insertEventData = { ...sanitizedEventData }; diff --git a/src/app/api/jobs/[jobId]/route.ts b/src/app/api/jobs/[jobId]/route.ts index e7ae625..6cf64d0 100644 --- a/src/app/api/jobs/[jobId]/route.ts +++ b/src/app/api/jobs/[jobId]/route.ts @@ -3,7 +3,6 @@ import { NextRequest, NextResponse } from "next/server"; import Job from "@/database/jobSchema"; import { withApiAuth } from "@/lib/auth"; import { JobStatus } from "@/database/jobSchema"; -import { isExpired } from "@/lib/utils"; const mutableJobFields = [ "organizationIndustry", @@ -75,7 +74,7 @@ export const PUT = withApiAuth( const jobId = req.nextUrl.pathname.split("/").pop(); const requestData = await req.json(); - const { previousStatus, newStatus, isRenewal, isUnpublish, ...jobData } = requestData; + const { previousStatus, newStatus, isRenewal, isResolve, isUnpublish, ...jobData } = requestData; // Handle both cases - with and without status transition data const hasStatusTransition = previousStatus !== undefined && newStatus !== undefined; @@ -110,8 +109,9 @@ export const PUT = withApiAuth( return NextResponse.json({ message: "Job renewed successfully", job: updatedJob }); } - // Handle job unpublishing - if (isUnpublish) { + // Resolve removes a listing from the public board while retaining it for + // the organization and admins. Keep isUnpublish for older clients. + if (isResolve || isUnpublish) { const updatedJob = await Job.findByIdAndUpdate( jobId, { @@ -120,7 +120,7 @@ export const PUT = withApiAuth( }, { new: true }, ); - return NextResponse.json({ message: "Job unpublished successfully", job: updatedJob }); + return NextResponse.json({ message: "Job resolved successfully", job: updatedJob }); } const nextJobStatus = hasStatusTransition @@ -171,7 +171,7 @@ export const GET = withApiAuth( return NextResponse.json({ message: "Job not found" }, { status: 404 }); } - const isPubliclyVisible = job.jobStatus === JobStatus.approved && !isExpired(job.jobStatus, job.approvedDate); + const isPubliclyVisible = job.jobStatus === JobStatus.approved; const canViewPrivateJob = auth.role === "spokes_admin" || (auth.userId && job.userId === auth.userId); if (!isPubliclyVisible && !canViewPrivateJob) { diff --git a/src/app/api/jobs/recent/route.ts b/src/app/api/jobs/recent/route.ts index eef11ee..137afc3 100644 --- a/src/app/api/jobs/recent/route.ts +++ b/src/app/api/jobs/recent/route.ts @@ -3,7 +3,6 @@ import { NextRequest, NextResponse } from "next/server"; import Job from "@/database/jobSchema"; import { ObjectId } from "mongodb"; import { withApiAuth } from "@/lib/auth"; -import { getThirtyDaysAgo } from "@/lib/utils"; export const POST = withApiAuth( async (req: NextRequest) => { @@ -22,7 +21,6 @@ export const POST = withApiAuth( const recentJobs = await Job.find({ _id: { $in: jobIdArray }, jobStatus: "approved", - approvedDate: { $gte: getThirtyDaysAgo() }, }); return NextResponse.json(recentJobs, { status: 200 }); } catch (error) { diff --git a/src/app/api/jobs/route.ts b/src/app/api/jobs/route.ts index 0ffd047..7a6cde1 100644 --- a/src/app/api/jobs/route.ts +++ b/src/app/api/jobs/route.ts @@ -4,7 +4,6 @@ import Job from "@/database/jobSchema"; import User from "@/database/userSchema"; import { withApiAuth } from "@/lib/auth"; import { resolveOrganizationName } from "@/lib/organizations"; -import { getThirtyDaysAgo } from "@/lib/utils"; // no filters: GET /api/jobs?page=1&limit=10 // filter by employment type: GET /api/jobs?employmentType=full-time&employment=part-time @@ -69,18 +68,8 @@ export const GET = withApiAuth( filter.jobStatus = { $in: statusFilter, }; - - // Filter out expired jobs that are already approved - if (statusFilter == "approved" && !isAdminRequest) { - filter.approvedDate = { - $gte: getThirtyDaysAgo(), - }; - } } else if (!isAdminRequest) { filter.jobStatus = "approved"; - filter.approvedDate = { - $gte: getThirtyDaysAgo(), - }; } if (statusFilter === "approved") { diff --git a/src/app/api/send/event-new/route.ts b/src/app/api/send/event-new/route.ts index 93acae5..e91f7e0 100644 --- a/src/app/api/send/event-new/route.ts +++ b/src/app/api/send/event-new/route.ts @@ -30,8 +30,8 @@ export const POST = withApiAuth( description: eventData.description, organization: eventData.organization, date: new Date(eventData.date).toLocaleDateString(), - time: eventData.time, - location: eventData.location, + time: eventData.time || "TBD", + location: eventData.location || "TBD", contactName: eventData.contactName, contactEmail: eventData.contactEmail, adminURL: getEventsAdminUrl(), diff --git a/src/app/dashboard/DashboardPage.tsx b/src/app/dashboard/DashboardPage.tsx index c8bbdc1..4b851cc 100644 --- a/src/app/dashboard/DashboardPage.tsx +++ b/src/app/dashboard/DashboardPage.tsx @@ -139,16 +139,13 @@ export default function DashboardPage({ organizationName, membershipStatus }: Da ); } - const liveJobs = userJobs.filter( - (job) => job.jobStatus.toLowerCase() === "approved" && !isExpired(job.jobStatus, job.approvedDate), - ); + const liveJobs = userJobs.filter((job) => job.jobStatus.toLowerCase() === "approved" && !isExpired(job.jobStatus)); const pendingJobs = userJobs.filter( (job) => job.jobStatus.toLowerCase() === "pending" || job.jobStatus.toLowerCase() === "rejected", ); - // Filter expired jobs by if the approval date is greater than 30 days ago or if the job status is "expired" - const expiredJobs = userJobs.filter((job) => isExpired(job.jobStatus, job.approvedDate)); + const resolvedJobs = userJobs.filter((job) => isExpired(job.jobStatus)); return (
@@ -173,7 +170,7 @@ export default function DashboardPage({ organizationName, membershipStatus }: Da )) @@ -195,14 +192,14 @@ export default function DashboardPage({ organizationName, membershipStatus }: Da
-
Expired Jobs
+
Resolved Jobs
- {expiredJobs.length > 0 ? ( - expiredJobs.map((job, index) => ( - + {resolvedJobs.length > 0 ? ( + resolvedJobs.map((job, index) => ( + )) ) : ( -
No expired jobs available
+
No resolved jobs available
)}
diff --git a/src/app/events/[eventId]/page.tsx b/src/app/events/[eventId]/page.tsx index 1f20661..0c82765 100644 --- a/src/app/events/[eventId]/page.tsx +++ b/src/app/events/[eventId]/page.tsx @@ -49,10 +49,10 @@ export default async function EventPage({ params }: EventPageProps) { Date: {formatDate(event.date)}

- Time: {event.time} + Time: {event.time || "TBD"}

- Location: {event.location} + Location: {event.location || "TBD"}

{(eventCity || eventRegion) && (

diff --git a/src/components/events/EventCard.tsx b/src/components/events/EventCard.tsx index 6b06c08..b0b8062 100644 --- a/src/components/events/EventCard.tsx +++ b/src/components/events/EventCard.tsx @@ -59,40 +59,36 @@ export default function EventCard({ event, onEventView }: EventCardProps) { {formatDate(event.date)}

- {event.time && ( -

- - - - - {event.time} -

- )} - {event.location && ( -

- - - - - {event.location} -

- )} +

+ + + + + {event.time || "Time TBD"} +

+

+ + + + + {event.location || "Location TBD"} +

{(eventCity || eventRegion) && (

{[eventCity, eventRegion].filter(Boolean).join(", ")}

)} diff --git a/src/components/events/EventCard/AdminEventCard.tsx b/src/components/events/EventCard/AdminEventCard.tsx index 111c4ff..fc27006 100644 --- a/src/components/events/EventCard/AdminEventCard.tsx +++ b/src/components/events/EventCard/AdminEventCard.tsx @@ -6,7 +6,9 @@ import JobCardModal from "@/components/jobs/JobCard/JobCardModal"; import ActionButton from "@/components/jobs/JobCard/ActionButton"; import JobPostedDate from "@/components/jobs/JobCard/JobPostedDate"; import { getEventInfoLink, getEventLocationLink } from "@/lib/eventLinks"; -import { useToast } from "@chakra-ui/react"; +import { IconButton } from "@chakra-ui/react"; +import { FiEdit } from "react-icons/fi"; +import { useRouter } from "next/navigation"; interface AdminEventCardProps { event: IEvent; @@ -25,7 +27,7 @@ export default function AdminEventCard({ event, onUpdateEvent, innerRef }: Admin const [rejectionReason, setRejectionReason] = useState(""); const [isNewIndicatorDismissed, setIsNewIndicatorDismissed] = useState(false); const [isLoading, setIsLoading] = useState<"approve" | "reject" | null>(null); - const toast = useToast(); + const router = useRouter(); useEffect(() => { const dismissedState = localStorage.getItem(`new-event-indicator-${event._id}`); @@ -101,6 +103,15 @@ export default function AdminEventCard({ event, onUpdateEvent, innerRef }: Admin return (
+ } + size="sm" + borderColor="black" + position="absolute" + className="absolute top-4 right-[1rem]" + onClick={() => router.push(`/events/list?eventId=${event._id}&returnURL=/events/admin`)} + /> {/* New indicator dot */} {new Date(event.createdAt ?? event.date).getTime() > Date.now() - 24 * 60 * 60 * 1000 && !isNewIndicatorDismissed && ( @@ -126,10 +137,10 @@ export default function AdminEventCard({ event, onUpdateEvent, innerRef }: Admin Date: {formattedDate}
- Time: {event.time} + Time: {event.time || "TBD"}
- Location: {event.location} + Location: {event.location || "TBD"}
{(eventCity || eventRegion) && (
diff --git a/src/components/events/EventCard/OrgEventCard.tsx b/src/components/events/EventCard/OrgEventCard.tsx index 44de70b..59c5be7 100644 --- a/src/components/events/EventCard/OrgEventCard.tsx +++ b/src/components/events/EventCard/OrgEventCard.tsx @@ -14,7 +14,6 @@ import { ModalCloseButton, useDisclosure, Text, - useToast, } from "@chakra-ui/react"; import { FiEdit, FiMessageSquare } from "react-icons/fi"; import { useRouter } from "next/navigation"; @@ -22,14 +21,12 @@ import { useRouter } from "next/navigation"; export interface OrgEventCardProps extends ComponentProps<"div"> { className?: string; event: IEvent; - onEventStatusUpdate?: (event: IEvent) => void; } export const OrgEventCard = forwardRef( - ({ children, className, event, onEventStatusUpdate, ...props }, ref) => { + ({ children, className, event, ...props }, ref) => { const { isOpen, onOpen, onClose } = useDisclosure(); const router = useRouter(); - const toast = useToast(); const eventDate = new Date(event.date).toLocaleDateString("en-US", { month: "short", @@ -40,9 +37,9 @@ export const OrgEventCard = forwardRef( event.eventLocationGeneral === "Other" ? event.eventLocationGeneralOther : event.eventLocationGeneral; const eventCity = event.eventLocationCity === "Other" ? event.eventLocationCityOther : event.eventLocationCity; - function handleEditButton(e: React.ChangeEvent) { + function handleEditButton(e: React.MouseEvent) { e.preventDefault(); - router.push(`/events/list?eventId=${event._id}`); + router.push(`/events/list?eventId=${event._id}&returnURL=/events/manage`); } return ( @@ -61,7 +58,7 @@ export const OrgEventCard = forwardRef(
- {eventDate} · {event.time} · {event.location} + {[eventDate, event.time || "Time TBD", event.location || "Location TBD"].join(" · ")}
{(eventCity || eventRegion) && (
{[eventCity, eventRegion].filter(Boolean).join(", ")}
@@ -84,18 +81,16 @@ export const OrgEventCard = forwardRef( Feedback )} - {(event.eventStatus === "pending" || event.eventStatus === "rejected") && ( - - )} +
diff --git a/src/components/events/EventModals/EventConfirmationModal.tsx b/src/components/events/EventModals/EventConfirmationModal.tsx index 503127a..35f2ac2 100644 --- a/src/components/events/EventModals/EventConfirmationModal.tsx +++ b/src/components/events/EventModals/EventConfirmationModal.tsx @@ -17,9 +17,16 @@ import { useRouter } from "next/navigation"; interface EventConfirmationModalProps { isOpen: boolean; onClose: () => void; + onCreateAnother: () => void; + submittedForReview: boolean; } -export default function EventConfirmationModal({ isOpen, onClose }: EventConfirmationModalProps) { +export default function EventConfirmationModal({ + isOpen, + onClose, + onCreateAnother, + submittedForReview, +}: EventConfirmationModalProps) { const router = useRouter(); return ( @@ -37,13 +44,14 @@ export default function EventConfirmationModal({ isOpen, onClose }: EventConfirm

What's Next?

- Your event has been submitted for review. Upon approval, your event will be published to the Spokes event - board and made available to the public. + {submittedForReview + ? "Your event has been submitted for review. Upon approval, your event will be published to the Spokes event board and made available to the public." + : "Your event has been updated."} - - + {eventId ? ( + <> +
+
+ + {isSpokesAdmin && ( + + )} + +
+
+ + ← Return to {isSpokesAdmin ? "Admin Dashboard" : "Dashboard"} + + + ) : ( +
+ +
+ )} - setIsConfirmationModalOpen(false)} /> + setIsConfirmationModalOpen(false)} + onCreateAnother={createAnotherEvent} + submittedForReview={!eventId || !isSpokesAdmin} + /> setIsFailModalOpen(false)} /> + setIsDeleteConfirmationOpen(false)} isCentered> + + + Confirm Delete + + Are you sure you would like to delete this event? + + + + + + + setIsRejectConfirmationOpen(false)} isCentered> + + + Confirm Rejection + + + Are you sure you would like to reject this event? +