Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds extra validation for rsvps #144

Merged
merged 4 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 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
.schema(defaultRegistrationToggleSchema)
.action(async ({ parsedInput: { enabled }, ctx: { user, userId } }) => {
Expand Down Expand Up @@ -40,3 +44,11 @@ export const toggleRSVPs = adminAction
revalidatePath("/admin/toggles/registration");
return { success: true, statusSet: enabled };
});

export const setRSVPLimit = adminAction
.schema(defaultRSVPLimitSchema)
.action(async ({ parsedInput: { rsvpLimit }, ctx: { user, userId } }) => {
await kv.set("config:registration:maxRSVPs", rsvpLimit);
revalidatePath("/admin/toggles/registration");
return { success: true, statusSet: rsvpLimit };
});
5 changes: 1 addition & 4 deletions apps/web/src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ export default async function AdminLayout({ children }: AdminLayoutProps) {
<ClientToast />
<div className="fixed z-20 grid h-16 w-full grid-cols-2 bg-nav px-5">
<div className="flex items-center gap-x-4">
<Link
href={"/"}
className="mr-5 flex items-center gap-x-2"
>
<Link href={"/"} className="mr-5 flex items-center gap-x-2">
<Image
src={c.icon.svg}
alt={c.hackathonName + " Logo"}
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/app/admin/toggles/registration/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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();
Expand All @@ -12,10 +13,12 @@ export default async function Page() {
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 +41,10 @@ export default async function Page() {
defaultRSVPsEnabled,
true,
)}
defaultRSVPLimit={parseRedisNumber(
defaultRSVPLimit,
c.rsvpDefaultLimit,
)}
/>
</div>
);
Expand Down
40 changes: 29 additions & 11 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 { userCommonData } 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 { getUser } from "db/functions";
Expand Down Expand Up @@ -40,16 +40,34 @@ export default async function RsvpPage({
return redirect("/i/approval");
}

const rsvpEnabled = await kv.get("config:registration:allowRSVPs");
const rsvpEnabled = parseRedisBoolean(
(await kv.get("config:registration:allowRSVPs")) as
| string
| boolean
| null
| undefined,
true,
);

// TODO: fix type jank here
if (
parseRedisBoolean(
rsvpEnabled as string | boolean | null | undefined,
true,
) === true ||
user.isRSVPed === true
) {
let isRsvpPossible = false;

if (rsvpEnabled === true) {
const rsvpLimit = parseRedisNumber(
await kv.get("config:registration:maxRSVPs"),
c.rsvpDefaultLimit,
);

const rsvpUserCount = await db
.select({ count: count() })
.from(userCommonData)
.where(eq(userCommonData.isRSVPed, true))
.limit(rsvpLimit)
.then((result) => result[0].count);

isRsvpPossible = rsvpUserCount < rsvpLimit;
}

if (isRsvpPossible || user.isRSVPed === true) {
return (
<>
<ClientToast />
Expand Down
28 changes: 28 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,16 @@ export function RegistrationToggles({
},
});

const {
execute: executeSetRSVPLimit,
optimisticState: SetRSVPLimitOptimisticData,
} = useOptimisticAction(setRSVPLimit, {
currentState: { success: true, statusSet: defaultRSVPLimit },
updateFn: (state, { 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 +130,20 @@ export function RegistrationToggles({
}}
/>
</div>
<div className="flex items-center border-b border-t border-t-muted py-4">
<p className="mr-auto text-sm font-bold">RSVP Limit</p>
<UpdateItemWithConfirmation
defaultValue={SetRSVPLimitOptimisticData.statusSet}
enabled={toggleRSVPsOptimisticData.statusSet}
type="number"
onSubmit={(newLimit) => {
toast.success(
`Hacker RSVP limit successfully changed to ${newLimit}!`,
);
executeSetRSVPLimit({ rsvpLimit: newLimit });
}}
/>
</div>
</div>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 max-h-8 items-center gap-2">
<Input
className="text-md w-24 text-center font-bold sm:w-40"
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: 4 additions & 4 deletions apps/web/src/lib/utils/client/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { redirect } from "next/navigation";
export function getClientTimeZone(vercelIPTimeZone: string | null) {
return vercelIPTimeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
}
export async function clientLogOut(){
"use server";
redirect("/");
};
export async function clientLogOut() {
"use server";
redirect("/");
}
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 @@ -778,6 +778,7 @@ const c = {
itteration: "I",
siteUrl: "https://rowdyhacks.org", // Do not have a trailing slash
defaultMetaDataDescription: "Your Metadata Description Here",
rsvpDefaultLimit: 500,
botName: "HackKit",
botParticipantRole: "Participant",
hackathonTimezone: "America/Chicago",
Expand Down
Loading