Skip to content
Open
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,
});
console.log(judgesRes);
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.

Debug console.log statement should be removed before merging to production. This appears to be leftover debugging code.

Copilot uses AI. Check for mistakes.
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';
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why FieldName is no longer needed?


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' ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have a frontend example of this? just curious how it looks. Maybe during tmr (10/27) meeting you can briefly share.

<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
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,
},
Comment on lines +100 to +104
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 validation requires opted_into_panels to be explicitly true or false, but if a judge record doesn't have this field set (e.g., due to migration failure or data inconsistency), the validation will fail without a clear error message. Consider providing a default value (e.g., data.opted_into_panels ?? false) to handle edge cases where the field might be undefined.

Copilot uses AI. Check for mistakes.
];

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}
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 dropdown uses data.opted_into_panels directly without a default value. If this field is undefined for a judge (e.g., due to migration failure or data inconsistency), the dropdown will show an empty/undefined state. Consider providing a default value (e.g., data.opted_into_panels ?? false) to ensure the dropdown always has a valid selection, consistent with how other boolean fields like has_checked_in should be handled.

Suggested change
value={data.opted_into_panels}
value={data.opted_into_panels ?? false}

Copilot uses AI. Check for mistakes.
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'd 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
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

In response to your pr description, yeah I'm not too sure either why "Best Hack for Social Good" doesn't have a domain. It was also not in the admin panel judging view and we had to judge it ourselves last year, not sure if that was intentional - may need to check with Jay or someone last year about that.

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