diff --git a/apps/web/src/actions/checkin.ts b/apps/web/src/actions/checkin.ts index 5e4edf1..c6df9c5 100644 --- a/apps/web/src/actions/checkin.ts +++ b/apps/web/src/actions/checkin.ts @@ -6,14 +6,43 @@ import { UNIQUE_KEY_CONSTRAINT_VIOLATION_CODE } from "@/lib/constants/"; import { checkInUserClient, checkInUserList } from "@/lib/queries/checkins"; import { adminCheckinSchema, universityIDSplitter } from "db/zod"; import { CheckinResult } from "@/lib/types/events"; -import { revalidatePath } from "next/cache"; import { headers } from "next/headers"; +import { getEventById } from "@/lib/queries/events"; +import { returnValidationErrors } from "next-safe-action"; +import z from "zod"; +import { isWithinInterval } from "date-fns"; -const { ALREADY_CHECKED_IN, SUCCESS, FAILED, SOME_FAILED } = CheckinResult; +const { + ALREADY_CHECKED_IN, + SUCCESS, + FAILED, + SOME_FAILED, + EVENT_NOT_FOUND, + CHECKIN_NOT_AVAILABLE, +} = CheckinResult; export const checkInUserAction = userAction .schema(userCheckinSchemaFormified) .action(async ({ parsedInput }) => { + const { eventID } = parsedInput; + const event = await getEventById(eventID); + if (!event) { + returnValidationErrors(z.null(), { + _errors: [EVENT_NOT_FOUND], + }); + } + + const currentDateUTC = new Date(); + const isCheckinAvailable = isWithinInterval(currentDateUTC, { + start: event.checkinStart, + end: event.checkinEnd, + }); + if (!isCheckinAvailable) { + returnValidationErrors(z.null(), { + _errors: [CHECKIN_NOT_AVAILABLE], + }); + } + try { await checkInUserClient(parsedInput); } catch (e) { diff --git a/apps/web/src/components/dash/admin/events/EditEventForm.tsx b/apps/web/src/components/dash/admin/events/EditEventForm.tsx index 2264e5b..7a8c81d 100644 --- a/apps/web/src/components/dash/admin/events/EditEventForm.tsx +++ b/apps/web/src/components/dash/admin/events/EditEventForm.tsx @@ -29,8 +29,7 @@ import { } from "@/components/ui/alert-dialog"; import { Switch } from "@/components/ui/switch"; import { useState, useEffect } from "react"; -import { cn } from "@/lib/utils"; -import { format } from "date-fns"; +import { isAfter, addHours, isBefore } from "date-fns"; import { getLocalTimeZone, parseAbsolute } from "@internationalized/date"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -126,6 +125,30 @@ export default function EditEventForm({ return true; } + const eventStartTime = form.watch("start"); + const eventEndTime = form.watch("end"); + const checkinStartTime = form.watch("checkinStart"); + const checkinEndTime = form.watch("checkinEnd"); + + useEffect(() => { + if (isAfter(eventStartTime, eventEndTime)) { + form.setValue("end", addHours(eventStartTime, 1)); + } + }, [eventStartTime]); + + useEffect(() => { + if (isAfter(checkinStartTime, checkinEndTime)) { + form.setValue("checkinEnd", addHours(checkinStartTime, 1)); + } + }, [checkinStartTime]); + + useEffect(() => { + if (isBefore(checkinEndTime, eventEndTime)) { + form.setValue("checkinStart", eventStartTime); + form.setValue("checkinEnd", eventEndTime); + } + }, [eventEndTime]); + useEffect(() => { if (Object.keys(form.formState.errors).length > 0) { console.log("Errors: ", form.formState.errors); diff --git a/apps/web/src/components/dash/admin/events/NewEventForm.tsx b/apps/web/src/components/dash/admin/events/NewEventForm.tsx index b30a31d..56f6d38 100644 --- a/apps/web/src/components/dash/admin/events/NewEventForm.tsx +++ b/apps/web/src/components/dash/admin/events/NewEventForm.tsx @@ -45,7 +45,6 @@ import { useAction } from "next-safe-action/hooks"; import { put } from "@/lib/client/file-upload"; import { createEvent } from "@/actions/events/createNewEvent"; import { ONE_HOUR_IN_MILLISECONDS } from "@/lib/constants"; -import { bucketEventThumbnailBaseUrl } from "config"; import type { NewEventFormProps } from "@/lib/types/events"; import { Select, @@ -54,6 +53,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { isAfter, isBefore, addHours } from "date-fns"; const formSchema = insertEventSchemaFormified; @@ -116,6 +116,33 @@ export default function NewEventForm({ return true; } + const eventStartTime = form.watch("start"); + const eventEndTime = form.watch("end"); + const checkinStartTime = form.watch("checkinStart"); + const checkinEndTime = form.watch("checkinEnd"); + + useEffect(() => { + if (isAfter(eventStartTime, eventEndTime)) { + form.setValue("end", addHours(eventStartTime, 1)); + } + }, [eventStartTime]); + + useEffect(() => { + if ( + isAfter(checkinStartTime, checkinEndTime) && + hasDifferentCheckinTime + ) { + form.setValue("checkinEnd", addHours(checkinStartTime, 1)); + } + }, [checkinStartTime]); + + useEffect(() => { + if (isBefore(checkinEndTime, eventEndTime) && hasDifferentCheckinTime) { + form.setValue("checkinStart", eventStartTime); + form.setValue("checkinEnd", eventEndTime); + } + }, [eventEndTime]); + const { execute: runCreateEvent, status: actionStatus, diff --git a/apps/web/src/components/dash/admin/overview/KeyMetrics.tsx b/apps/web/src/components/dash/admin/overview/KeyMetrics.tsx index 9b8aec6..360e557 100644 --- a/apps/web/src/components/dash/admin/overview/KeyMetrics.tsx +++ b/apps/web/src/components/dash/admin/overview/KeyMetrics.tsx @@ -10,6 +10,7 @@ import { CheckCircleIcon, BarChart, ArrowUpIcon, + ArrowDownIcon, UsersIcon, TrendingUpIcon, } from "lucide-react"; @@ -117,7 +118,11 @@ export default async function KeyMetrics() {
{growthRate}%
- + {growthRate > 0 ? ( + + ) : growthRate < 0 ? ( + + ) : null}

vs last month diff --git a/apps/web/src/components/events/EventsCardView.tsx b/apps/web/src/components/events/EventsCardView.tsx index fe0922b..3316d2d 100644 --- a/apps/web/src/components/events/EventsCardView.tsx +++ b/apps/web/src/components/events/EventsCardView.tsx @@ -1,8 +1,7 @@ import type { EventAndCategoriesType } from "@/lib/types/events"; import EventCardComponent from "./EventCardComponent"; import { ScrollArea } from "../ui/scroll-area"; -import { isAfter } from "date-fns"; -import { isEventCurrentlyHappening, isEventCheckinAllowed } from "@/lib/utils"; +import { isAfter, isWithinInterval } from "date-fns"; export default function EventsCardView({ events, clientTimeZone, @@ -21,15 +20,19 @@ export default function EventsCardView({ key={event.id} event={event} isPast={isAfter(currentDateUTC, event.end)} - isEventCurrentlyHappening={isEventCurrentlyHappening( + isEventCurrentlyHappening={isWithinInterval( currentDateUTC, - event.start, - event.end, + { + start: event.start, + end: event.end, + }, )} - isEventCheckinAllowed={isEventCheckinAllowed( + isEventCheckinAllowed={isWithinInterval( currentDateUTC, - event.checkinStart, - event.checkinEnd, + { + start: event.checkinStart, + end: event.checkinEnd, + }, )} clientTimezone={clientTimeZone} /> diff --git a/apps/web/src/components/events/EventsView.tsx b/apps/web/src/components/events/EventsView.tsx index 35dcf72..dd94d92 100644 --- a/apps/web/src/components/events/EventsView.tsx +++ b/apps/web/src/components/events/EventsView.tsx @@ -1,12 +1,11 @@ import EventsCardView from "./EventsCardView"; import EventsCalendarView from "./EventsCalendarView"; -import { db, like, gte, and, lt } from "db"; +import { db, like, gte, and, lt, eq } from "db"; import { events } from "db/schema"; import type { SearchParams } from "@/lib/types/shared"; import { EVENT_FILTERS } from "@/lib/constants/events"; import { unstable_noStore as noStore } from "next/cache"; import PageError from "../shared/PageError"; -import { headers } from "next/headers"; import { getClientTimeZone, getUTCDate } from "@/lib/utils"; import { getRequestContext } from "@cloudflare/next-on-pages"; @@ -48,7 +47,11 @@ export default async function EventsView({ params }: { params: SearchParams }) { }, }, }, - where: and(eventSearchQuery, dateComparison), + where: and( + eventSearchQuery, + dateComparison, + eq(events.isHidden, false), + ), orderBy: events.start, }) .then((events) => { diff --git a/apps/web/src/components/events/id/EventDetails.tsx b/apps/web/src/components/events/id/EventDetails.tsx index 09df76e..b7462c9 100644 --- a/apps/web/src/components/events/id/EventDetails.tsx +++ b/apps/web/src/components/events/id/EventDetails.tsx @@ -1,21 +1,15 @@ import { getEventDetails } from "@/lib/queries/events"; import PageError from "../../shared/PageError"; import EventImage from "../shared/EventImage"; -import { headers } from "next/headers"; import { TWENTY_FOUR_HOURS, ONE_HOUR_IN_MILLISECONDS } from "@/lib/constants"; import c from "config"; -import { - getClientTimeZone, - getUTCDate, - isEventCurrentlyHappening, -} from "@/lib/utils"; -import { differenceInHours, isAfter } from "date-fns"; +import { getClientTimeZone, getUTCDate } from "@/lib/utils"; +import { isAfter, isWithinInterval } from "date-fns"; import { formatInTimeZone } from "date-fns-tz"; import { EVENT_DATE_FORMAT_STRING, EVENT_TIME_FORMAT_STRING, } from "@/lib/constants/events"; -import EventDetailsLiveIndicator from "../shared/EventDetailsLiveIndicator"; import EventCategories from "../EventCategories"; import { BellRing, @@ -66,14 +60,13 @@ export default async function EventDetails({ if (!event) { return ; } - const { start, end, checkinStart, checkinEnd } = event; + const { start, end } = event; const currentDateUTC = getUTCDate(); const isEventPassed = isAfter(currentDateUTC, end); - const isEventHappening = isEventCurrentlyHappening( - currentDateUTC, - start, - end, - ); + const isEventHappening = isWithinInterval(currentDateUTC, { + start: start, + end: end, + }); const startTime = formatInTimeZone( start, @@ -97,15 +90,6 @@ export default async function EventDetails({ const checkInUrl = `/events/${event.id}/checkin`; - const isCheckinAvailable = - checkinStart <= currentDateUTC && currentDateUTC <= checkinEnd; - - const checkInMessage = isCheckinAvailable - ? "Ready to check-in? Click here!" - : isEventPassed - ? "Check-in is closed" - : `Check-in starts on ${formatInTimeZone(start, clientTimeZone, `${EVENT_TIME_FORMAT_STRING} @${EVENT_DATE_FORMAT_STRING}`)}`; - const eventCalendarLink = { title: event.name, description: event.description, @@ -114,19 +98,6 @@ export default async function EventDetails({ location: event.location, }; - const detailsProps = { - event, - startTime, - startDate: startDateFormatted, - formattedEventDuration, - checkInUrl, - checkInMessage, - eventCalendarLink, - isEventPassed, - isCheckinAvailable, - isEventHappening, - }; - const { thumbnailUrl, location, description, points } = event; const width = 500; const height = 500; @@ -183,16 +154,16 @@ export default async function EventDetails({

-

{event.location}

+

{location}

- {event.points} + {points} {" "} - pt{event.points != 1 ? "s" : ""} + pt{points != 1 ? "s" : ""}

@@ -263,7 +234,7 @@ export default async function EventDetails({ href={`/api/ics-calendar?event_id=${id}`} target="_blank" className="flex w-auto justify-between gap-3 rounded-md px-3 py-2 text-primary-foreground md:max-w-[7.5rem] lg:max-w-none" - download={`event_${event.id}.ics`} + download={`event_${id}.ics`} > ; - } const userEventData = await getUserDataAndCheckin(eventID, clerkId); if (!userEventData) { @@ -52,13 +47,22 @@ export default async function EventCheckin({ return ; } - const isCheckinAvailable = - event.checkinStart <= currentDateUTC && - currentDateUTC <= event.checkinEnd; + const isCheckinAvailable = isWithinInterval(currentDateUTC, { + start: event.checkinStart, + end: event.checkinEnd, + }); + if (!isCheckinAvailable) { + const isDateBeforeCheckinStart = isBefore( + currentDateUTC, + event.checkinStart, + ); + const errorMessage = isDateBeforeCheckinStart + ? `Check-in does not start until ${formatInTimeZone(event.checkinStart, clientTimeZone, `${EVENT_TIME_FORMAT_STRING} @ ${EVENT_DATE_FORMAT_STRING}`)}` + : `Check-in for this event ended on ${formatInTimeZone(event.checkinEnd, clientTimeZone, `${EVENT_TIME_FORMAT_STRING} @ ${EVENT_DATE_FORMAT_STRING}`)}`; return ( diff --git a/apps/web/src/components/events/id/checkin/EventCheckinForm.tsx b/apps/web/src/components/events/id/checkin/EventCheckinForm.tsx index 9f7f7d8..993864b 100644 --- a/apps/web/src/components/events/id/checkin/EventCheckinForm.tsx +++ b/apps/web/src/components/events/id/checkin/EventCheckinForm.tsx @@ -50,7 +50,8 @@ export default function EventCheckinForm({ }, ); - const { ALREADY_CHECKED_IN } = CheckinResult; + const { ALREADY_CHECKED_IN, EVENT_NOT_FOUND, CHECKIN_NOT_AVAILABLE } = + CheckinResult; const { push } = useRouter(); @@ -61,6 +62,7 @@ export default function EventCheckinForm({ reset: resetCheckInUser, } = useAction(checkInUserAction, { onSuccess: async ({ data }) => { + console.log("Checkin success", data); toast.dismiss(); const success = data?.success; const code = data?.code; @@ -95,7 +97,33 @@ export default function EventCheckinForm({ }, onError: async ({ error: e }) => { toast.dismiss(); - if (e.validationErrors) { + if (e.validationErrors != null) { + if (e.validationErrors?._errors?.[0] === EVENT_NOT_FOUND) { + return toast.error( + `Event not found. Please check the event ID.`, + { + duration: Infinity, + cancel: { + label: "Close", + onClick: () => {}, + }, + }, + ); + } + if ( + e.validationErrors?._errors?.[0] === CHECKIN_NOT_AVAILABLE + ) { + return toast.error( + `Check-in is not available at this time.`, + { + duration: Infinity, + cancel: { + label: "Close", + onClick: () => {}, + }, + }, + ); + } toast.error(`Please check your input. ${e.validationErrors}`, { duration: Infinity, cancel: { diff --git a/apps/web/src/components/events/shared/EventDetailsLiveIndicator.tsx b/apps/web/src/components/events/shared/EventDetailsLiveIndicator.tsx deleted file mode 100644 index 3e4347e..0000000 --- a/apps/web/src/components/events/shared/EventDetailsLiveIndicator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export default function EventDetailsLiveIndicator({ - className, -}: { - className?: string; -}) { - return ( -
- ); -} diff --git a/apps/web/src/lib/queries/charts.ts b/apps/web/src/lib/queries/charts.ts index a1f59c0..b7b8a42 100644 --- a/apps/web/src/lib/queries/charts.ts +++ b/apps/web/src/lib/queries/charts.ts @@ -199,15 +199,14 @@ export async function getGrowthRate() { sql`${users.joinDate} >= date(datetime('now', '-1 month', 'start of month')) AND ${users.joinDate} < date(datetime('now', 'start of month'))`, ); - const currentMonthCount = currentMonthResult[0]?.count || 0; + const currentMonthCount = currentMonthResult[0]?.count || 1; const previousMonthCount = previousMonthResult[0]?.count || 1; // Avoid division by zero - return parseFloat( - ( - ((currentMonthCount - previousMonthCount) / previousMonthCount) * - 100 - ).toFixed(1), - ); + const growthRate = ( + ((currentMonthCount - previousMonthCount) / previousMonthCount) * + 100 + ).toFixed(1); + return parseFloat(growthRate); } // Get activity by day of week diff --git a/apps/web/src/lib/types/events.ts b/apps/web/src/lib/types/events.ts index 14f43ec..1d55b12 100644 --- a/apps/web/src/lib/types/events.ts +++ b/apps/web/src/lib/types/events.ts @@ -67,6 +67,8 @@ export enum CheckinResult { ALREADY_CHECKED_IN = "already_checked_in", SOME_FAILED = "some_failed", FAILED = "failed", + EVENT_NOT_FOUND = "event_not_found", + CHECKIN_NOT_AVAILABLE = "checkin_not_available", } export type iEvent = z.infer; diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index c7a6d4a..eafc1d6 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -43,22 +43,6 @@ export function getUTCDate() { return new Date(currentDate.toUTCString()); } -export function isEventCurrentlyHappening( - currentDateUTC: Date, - eventStart: Date, - eventEnd: Date, -) { - return currentDateUTC >= eventStart && currentDateUTC <= eventEnd; -} - -export function isEventCheckinAllowed( - currentDateUTC: Date, - checkinStart: Date, - checkinEnd: Date, -) { - return currentDateUTC >= checkinStart && currentDateUTC <= checkinEnd; -} - export function formatBlobUrl(blobUrl: string) { const end = blobUrl.split("/").at(-1); if (!end) return blobUrl;