Skip to content

Commit

Permalink
Added a maximum limit of users who can RSVP
Browse files Browse the repository at this point in the history
* Added default limit for the number of allowed RSVPs in the config.
* Added check to prevent RSVP users from going over the allowed limit in
  the RSVP page.
* Added the ability to edit RSVPLimit from the admin dashboard.
  • Loading branch information
mjanderson1227 committed Jul 25, 2024
1 parent 2911823 commit 2750867
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 15 deletions.
13 changes: 13 additions & 0 deletions apps/web/src/actions/admin/registration-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const defaultRegistrationToggleSchema = z.object({
enabled: z.boolean(),
});

const defaultRSVPLimitSchema = z.object({
rsvpLimit: z.number()
});

export const toggleRegistrationEnabled = adminAction(
defaultRegistrationToggleSchema,
async ({ enabled }, { user, userId }) => {
Expand Down Expand Up @@ -44,3 +48,12 @@ export const toggleRSVPs = adminAction(
return { success: true, statusSet: enabled };
},
);

export const setRSVPLimit = adminAction(
defaultRSVPLimitSchema,
async ({ rsvpLimit }) => {
await kv.set("config:registration:maxRSVPs", rsvpLimit);
revalidatePath("/admin/toggles/registration");
return { success: true, statusSet: rsvpLimit };
}
);
17 changes: 11 additions & 6 deletions apps/web/src/app/admin/toggles/registration/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { RegistrationToggles } from "@/components/admin/toggles/RegistrationSettings";
import { kv } from "@vercel/kv";
import { parseRedisBoolean } from "@/lib/utils/server/redis";
import { parseRedisBoolean, parseRedisNumber } from "@/lib/utils/server/redis";
import c from "config";

export default async function Page() {
const pipe = kv.pipeline();
pipe.get("config:registration:registrationEnabled");
pipe.get("config:registration:secretRegistrationEnabled");
// const result = await pipe.exec();

const [
defaultRegistrationEnabled,
defaultSecretRegistrationEnabled,
defaultRSVPsEnabled,
]: (string | null)[] = await kv.mget(
const [defaultRegistrationEnabled, defaultSecretRegistrationEnabled, defaultRSVPsEnabled, defaultRSVPLimit]: (
| string
| null
)[] = await kv.mget(
"config:registration:registrationEnabled",
"config:registration:secretRegistrationEnabled",
"config:registration:allowRSVPs",
"config:registration:maxRSVPs"
);

return (
Expand All @@ -38,6 +39,10 @@ export default async function Page() {
defaultRSVPsEnabled,
true,
)}
defaultRSVPLimit={parseRedisNumber(
defaultRSVPLimit,
c.rsvpDefaultLimit
)}
/>
</div>
);
Expand Down
22 changes: 13 additions & 9 deletions apps/web/src/app/rsvp/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import ConfirmDialogue from "@/components/rsvp/ConfirmDialogue";
import c from "config";
import { auth } from "@clerk/nextjs";
import { redirect } from "next/navigation";
import { db } from "db";
import { count, db } from "db";
import { eq } from "db/drizzle";
import { users } from "db/schema";
import ClientToast from "@/components/shared/ClientToast";
import { SignedOut, RedirectToSignIn } from "@clerk/nextjs";
import { kv } from "@vercel/kv";
import { parseRedisBoolean } from "@/lib/utils/server/redis";
import { parseRedisBoolean, parseRedisNumber } from "@/lib/utils/server/redis";
import Link from "next/link";
import { Button } from "@/components/shadcn/ui/button";
import { CheckCircleIcon } from "lucide-react";
Expand Down Expand Up @@ -38,15 +38,19 @@ export default async function RsvpPage({
}

const rsvpEnabled = await kv.get("config:registration:allowRSVPs");
const rsvpLimit = parseRedisNumber(await kv.get("config:registration:maxRSVPs"), c.rsvpDefaultLimit);
const rsvpUserCount = await db
.select({ count: count() })
.from(users)
.where(eq(users.rsvp, true))
.limit(rsvpLimit)
.then((result) => result[0].count);

// TODO: fix type jank here
if (
parseRedisBoolean(
rsvpEnabled as string | boolean | null | undefined,
true,
) === true ||
user.rsvp === true
) {
const isRsvpPossible = parseRedisBoolean(rsvpEnabled as string | boolean | null | undefined, true) === true &&
rsvpUserCount < rsvpLimit;

if (isRsvpPossible || user.rsvp === true) {
return (
<>
<ClientToast />
Expand Down
27 changes: 27 additions & 0 deletions apps/web/src/components/admin/toggles/RegistrationSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ import {
toggleRegistrationMessageEnabled,
toggleSecretRegistrationEnabled,
toggleRSVPs,
setRSVPLimit,
} from "@/actions/admin/registration-actions";
import { UpdateItemWithConfirmation } from "./UpdateItemWithConfirmation";

interface RegistrationTogglesProps {
defaultRegistrationEnabled: boolean;
defaultSecretRegistrationEnabled: boolean;
defaultRSVPsEnabled: boolean;
defaultRSVPLimit: number
}

export function RegistrationToggles({
defaultSecretRegistrationEnabled,
defaultRegistrationEnabled,
defaultRSVPsEnabled,
defaultRSVPLimit
}: RegistrationTogglesProps) {
const {
execute: executeToggleSecretRegistrationEnabled,
Expand Down Expand Up @@ -57,6 +61,17 @@ export function RegistrationToggles({
},
);

const {
execute: executeSetRSVPLimit,
optimisticData: SetRSVPLimitOptimisticData
} = useOptimisticAction(
setRSVPLimit,
{ success: true, statusSet: defaultRSVPLimit },
(_, { rsvpLimit }) => {
return { statusSet: rsvpLimit, success: true }
}
);

return (
<>
<div className="rounded-lg border-2 border-muted px-5 py-10">
Expand Down Expand Up @@ -116,6 +131,18 @@ export function RegistrationToggles({
}}
/>
</div>
<div className="flex items-center py-4 border-t-muted border-t border-b">
<p className="text-sm font-bold mr-auto">RSVP Limit</p>
<UpdateItemWithConfirmation
defaultValue={SetRSVPLimitOptimisticData.statusSet}
enabled={toggleRSVPsOptimisticData.statusSet}
type="number"
onSubmit={(newLimit) => {
toast.success(`Limit on the number of users who can RSVP changed to ${newLimit}`)
executeSetRSVPLimit({ rsvpLimit: newLimit })
}}
/>
</div>
</div>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useState } from "react";
import { Input } from "@/components/shadcn/ui/input";
import { Button } from "@/components/shadcn/ui/button";

interface UpdateItemWithConfirmationBaseProps<T extends number | string> {
defaultValue: T;
enabled: boolean;
onSubmit: (value: T) => void;
}

type UpdateItemWithConfirmationProps =
| ({ type: "string" } & UpdateItemWithConfirmationBaseProps<string>)
| ({ type: "number" } & UpdateItemWithConfirmationBaseProps<number>);

export function UpdateItemWithConfirmation({
type,
defaultValue,
onSubmit,
enabled,
}: UpdateItemWithConfirmationProps) {
const [valueUpdated, setValueUpdated] = useState(false);
const [value, setValue] = useState(defaultValue.toString());

return (
<div className="flex items-center gap-2 max-h-8">
<Input
className="sm:w-40 w-24 text-center text-md font-bold"
value={value}
disabled={!enabled}
onChange={({ target: { value: updated } }) => {
// Ignore the change if the value is a non numeric character.
if (type === "number" && /[^0-9]/.test(updated)) {
setValue(value);
return;
}

setValue(updated);

/* Avoid allowing the user to update the default value to itself.
* Also disallow the user from sending a zero length input. */
setValueUpdated(
updated !== defaultValue.toString() && updated.length !== 0
);
}}
/>
<Button
className="text-sm font-bold"
type="button"
variant="default"
title="Apply Changes"
disabled={!valueUpdated || !enabled}
onClick={() => {
if (type === "number") {
onSubmit(parseInt(value));
} else {
onSubmit(value);
}

setValueUpdated(false);
}}
>
Apply
</Button>
</div>
);
}
8 changes: 8 additions & 0 deletions apps/web/src/lib/utils/server/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ export function parseRedisBoolean(
if (typeof value === "boolean") return value;
return defaultValue !== undefined ? defaultValue : false;
}

export function parseRedisNumber(value: string | null, defaultValue: number) {
if (value && !isNaN(parseInt(value))) {
return parseInt(value);
} else {
return defaultValue;
}
}
1 change: 1 addition & 0 deletions packages/config/hackkit.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
botName: "HackKit",
botParticipantRole: "Participant",
hackathonTimezone: "America/Chicago",
rsvpDefaultLimit: 500,
localUniversityName: "The University of Texas at San Antonio",
localUniversityShortIDName: "ABC123",
localUniversityShortIDMaxLength: 6,
Expand Down

0 comments on commit 2750867

Please sign in to comment.