Skip to content
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
15 changes: 11 additions & 4 deletions app/(api)/_actions/auth/verifyCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@

import { updateUser } from '@actions/users/updateUser';

export default async function verifyCode(id: string, code: string) {
export default async function verifyCode(
id: string,
code: string,
optedIntoPanels?: boolean
) {
try {
const validCode = code === (process.env.CHECK_IN_CODE as string);
if (!validCode) {
throw new Error('Invalid code.');
}

const update: any = { has_checked_in: true };
if (typeof optedIntoPanels === 'boolean') {
update.opted_into_panels = optedIntoPanels;
}

const res = await updateUser(id, {
$set: {
has_checked_in: true,
},
$set: update,
});

if (!res.ok) {
Expand Down
8 changes: 7 additions & 1 deletion app/(api)/_actions/logic/assignJudgesToPanels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ export default async function assignJudgesToPanels(panelSize: number = 5) {
};
}

const judgesRes = await GetManyUsers({ role: 'judge', has_checked_in: true });
// only judges who have checked in and explicitly opted into panels
const judgesRes = await GetManyUsers({
role: 'judge',
has_checked_in: true,
opted_into_panels: true,
});

if (!judgesRes.ok || judgesRes.body.length === 0) {
return {
ok: false,
Expand Down
4 changes: 2 additions & 2 deletions app/(api)/_utils/matching/judgeToPanelAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export default async function judgeToPanelAlgorithm(

judges = judges.sort(
(a, b) =>
(a.specialties?.indexOf(panel.domain) ?? 0) -
(b.specialties?.indexOf(panel.domain) ?? 0)
(a.specialties?.indexOf(panel.domain ?? '') ?? 0) -
(b.specialties?.indexOf(panel.domain ?? '') ?? 0)
);

panel.user_ids = judges
Expand Down
39 changes: 24 additions & 15 deletions app/(pages)/_components/AuthForm/AuthForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import hackerStyles from './HackerAuthForm.module.scss';
import judgeStyles from './JudgeAuthForm.module.scss';

type Role = 'hacker' | 'judge';
type FieldName = 'email' | 'password' | 'passwordDupe' | 'code';

interface FormField {
name: FieldName;
name: string;
type: string;
label: string;
placeholder?: string;
Expand All @@ -27,8 +26,8 @@ interface AuthFormProps {
buttonText: string;
linkText?: string;
linkHref?: string;
initialValues: Record<string, string>;
onSubmit: (values: Record<string, string>) => Promise<any>;
initialValues: Record<string, any>;
onSubmit: (values: Record<string, any>) => Promise<any>;
onSuccess: () => void;
}

Expand Down Expand Up @@ -68,17 +67,27 @@ export default function AuthForm({
<p className={styles.error_msg}>{errors[field.name]}</p>
<div className={styles.input_container}>
<label htmlFor={field.name}>{field.label}</label>
<input
name={field.name}
type={field.type}
placeholder={field.placeholder}
value={formValues[field.name] || ''}
onInput={handleChange}
readOnly={field.readOnly}
style={{
cursor: field.readOnly ? 'not-allowed' : 'auto',
}}
/>
{field.type === 'checkbox' ? (
<input
name={field.name}
type="checkbox"
checked={!!formValues[field.name]}
onChange={handleChange}
readOnly={field.readOnly}
/>
) : (
<input
name={field.name}
type={field.type}
placeholder={field.placeholder}
value={formValues[field.name] || ''}
onInput={handleChange}
readOnly={field.readOnly}
style={{
cursor: field.readOnly ? 'not-allowed' : 'auto',
}}
/>
)}
</div>
</div>
))}
Expand Down
24 changes: 24 additions & 0 deletions app/(pages)/_components/AuthForm/JudgeAuthForm.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@
position: relative;
}

.input_container:has(input[type='checkbox']) {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
align-items: center;
gap: var(--small-spacer);

label {
position: static;
margin-top: 0;
font-size: 1rem;
pointer-events: auto;
cursor: pointer;
}

input[type='checkbox'] {
min-width: 30px;
min-height: 30px;
width: auto;
padding: 0;
cursor: pointer;
}
}

input {
padding-top: var(--small-medium-spacer);
padding-left: var(--small-spacer);
Expand Down
10 changes: 7 additions & 3 deletions app/(pages)/_hooks/useAuthForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useEffect, useState, ChangeEvent, FormEvent } from 'react';

interface FieldValues {
[key: string]: string;
[key: string]: any;
}

interface FieldErrors {
Expand Down Expand Up @@ -77,8 +77,12 @@ export default function useAuthForm(
};

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFieldValue(name, value);
const { name, value, type, checked } = e.target as any;
if (type === 'checkbox') {
setFieldValue(name, checked);
} else {
setFieldValue(name, value);
}
};

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
Expand Down
3 changes: 3 additions & 0 deletions app/(pages)/admin/_components/Judges/JudgeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export default function JudgeCard({
)}
</span>
<p className={styles.email}>{judge.email}</p>
<p className={styles.panel_status}>
Panel Opt-in: {judge.opted_into_panels ? 'Yes' : 'No'}
</p>
</div>
<div className={styles.header_details}>
{editable && (
Expand Down
41 changes: 36 additions & 5 deletions app/(pages)/admin/_components/Judges/JudgeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,16 @@ export default function JudgeForm({
teams.body.map((team: any) => [team._id, team])
);

if (data?._id) {
data.teams = data.teams.map((team: any) => {
return teamMap[team._id];
});
if (data?._id && data.teams) {
data.teams = data.teams
.map((team: any) => {
// Handle case where team might be just an ID string or already be a full team object
if (typeof team === 'string') {
return teamMap[team] || team;
}
return team._id ? teamMap[team._id] || team : team;
})
.filter(Boolean); // Remove any undefined teams
}
Comment on lines +52 to 62
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The team mapping logic has been updated to handle both team ID strings and full team objects. However, the condition data?._id && data.teams means this mapping only occurs when editing an existing judge (one with an _id). For new judges being created, if data.teams exists but data._id doesn't, teams won't be mapped. Consider whether this is intentional or if the mapping should also apply when creating new judges if they somehow have teams assigned.

Copilot uses AI. Check for mistakes.

const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
Expand Down Expand Up @@ -91,6 +97,11 @@ export default function JudgeForm({
validation: (has_checked_in: any) =>
has_checked_in === true || has_checked_in === false,
},
{
field: 'opted_into_panels',
validation: (opted_into_panels: any) =>
opted_into_panels === true || opted_into_panels === false,
},
];

verificationList.forEach(({ field, validation }) => {
Expand All @@ -104,13 +115,23 @@ export default function JudgeForm({
return;
}

const { _id, name, email, role, specialties, teams, has_checked_in } = data;
const {
_id,
name,
email,
role,
specialties,
teams,
has_checked_in,
opted_into_panels,
} = data;
const body = {
name,
email,
role,
specialties,
has_checked_in,
opted_into_panels,
};

const res = await updateJudgeWithTeams(_id, { $set: body }, teams);
Expand Down Expand Up @@ -232,6 +253,16 @@ export default function JudgeForm({
{ option: 'false', value: false },
]}
/>
<DropdownInput
label="opted into panels"
value={data.opted_into_panels}
updateValue={(value: any) => updateField('opted_into_panels', value)}
width="400px"
options={[
{ option: 'true', value: true },
{ option: 'false', value: false },
]}
/>
<div className={styles.action_buttons}>
<button className={styles.submit_button} type="submit">
Submit
Expand Down
13 changes: 10 additions & 3 deletions app/(pages)/judges/_components/AuthForms/CheckInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import AuthForm from '@components/AuthForm/AuthForm';
export default function CheckInForm({ id }: any) {
const router = useRouter();

const onSubmit = async (fields: any) => {
return verifyCode(id, fields.code);
};
const onSubmit = async (fields: any) =>
// include opt-in boolean when verifying
verifyCode(id, fields.code, !!fields.opted_into_panels);

const onSuccess = () => {
router.push('/judges');
Expand All @@ -24,6 +24,12 @@ export default function CheckInForm({ id }: any) {
placeholder: '',
readOnly: false,
},
{
name: 'opted_into_panels',
type: 'checkbox',
label: 'I would like to be on a judging panel',
readOnly: false,
},
];

return (
Expand All @@ -33,6 +39,7 @@ export default function CheckInForm({ id }: any) {
buttonText="Check in →"
initialValues={{
code: '',
opted_into_panels: false,
}}
onSubmit={onSubmit}
onSuccess={onSuccess}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
width: 100%;
height: 60vh;
max-height: 100vh;
background-color: var(--background-light);
background-color: #F2F2F7;
border-radius: var(--b-radius);
display: flex;
flex-direction: column;
Expand Down
2 changes: 1 addition & 1 deletion app/_types/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import User from './user';
interface Panel {
_id?: string;
track: string;
domain: string;
domain?: string;
user_ids: string[];
users?: User[]; // populated by aggregation
}
Expand Down
1 change: 1 addition & 0 deletions app/_types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface User {
position?: string; // for hackers only
is_beginner?: boolean; // for hackers only
has_checked_in: boolean;
opted_into_panels?: boolean; // for judges only
}

export default User;
Loading