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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# **.mjs
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The comment in .prettierignore is ineffective. In .prettierignore files, comments should start on their own line with a hash (#), not inline. The pattern "# **.mjs" will be treated as a literal pattern to ignore files named "# .mjs" rather than as a comment. If you want to ignore .mjs files, remove the "# " prefix to use ".mjs" as the pattern, or if you want a comment, move it to a separate line.

Suggested change
# **.mjs
**.mjs

Copilot uses AI. Check for mistakes.
48 changes: 48 additions & 0 deletions __tests__/csvAlgorithm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
matchCanonicalTrack,
sortTracks,
} from '@utils/csv-ingestion/csvAlgorithm';

describe('csvAlgorithm track matching', () => {
it('matches tracks case-insensitively to canonical names', () => {
expect(matchCanonicalTrack('best hardware hack')).toBe(
'Best Hardware Hack'
);
expect(matchCanonicalTrack('Best hardware hack')).toBe(
'Best Hardware Hack'
);
});

it('does not attempt to correct spelling', () => {
expect(matchCanonicalTrack('Best Hardwre Hack')).toBeNull();
expect(matchCanonicalTrack('Best Assistive Technlogy')).toBeNull();
});

it('ingests all opt-in tracks and does not cap length', () => {
const tracks = sortTracks(
'best hardware hack',
'',
'',
'Best Use of Gemini API; Best Use of MongoDB Atlas, Best Use of Vectara | Best Use of Auth0'
);

expect(tracks).toEqual([
'Best Hardware Hack',
'Best Use of Gemini API',
'Best Use of MongoDB Atlas',
'Best Use of Vectara',
'Best Use of Auth0',
]);
});

it('filters out excluded tracks', () => {
const tracks = sortTracks(
'Best Hack for Social Good',
"Hacker's Choice Award",
'',
'Best Hack for Social Good, Best Hardware Hack'
);

expect(tracks).toEqual(['Best Hardware Hack']);
});
});
32 changes: 32 additions & 0 deletions __tests__/csvValidation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { validateCsvBlob } from '@utils/csv-ingestion/csvAlgorithm';

describe('csvAlgorithm validation', () => {
it("silently ignores 'N/A' without warnings", async () => {
const csv =
'Table Number,Project Status,Project Title,Track #1 (Primary Track),Track #2,Track #3,Opt-In Prizes\n' +
'12,Submitted (Gallery/Visible),Test Project,Best Beginner Hack,N/A,,\n';

const blob = new Blob([csv], { type: 'text/csv' });
const res = await validateCsvBlob(blob);

expect(res.ok).toBe(true);
expect(res.report.errorRows).toBe(0);
expect(res.report.warningRows).toBe(0);
expect(res.report.issues).toEqual([]);
});

it('treats duplicate tracks as warnings (non-blocking)', async () => {
const csv =
'Table Number,Project Status,Project Title,Track #1 (Primary Track),Track #2,Track #3,Opt-In Prizes\n' +
'87,Submitted (Gallery/Visible),PartyPal,Best UI/UX Design,Best UI/UX Design,,\n';

const blob = new Blob([csv], { type: 'text/csv' });
const res = await validateCsvBlob(blob);

expect(res.ok).toBe(true);
expect(res.report.errorRows).toBe(0);
expect(res.report.warningRows).toBe(1);
expect(res.report.issues[0].severity).toBe('warning');
expect(res.report.issues[0].duplicateTracks).toEqual(['Best UI/UX Design']);
});
});
14 changes: 14 additions & 0 deletions app/(api)/_actions/logic/checkTeamsPopulated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use server';

import { getDatabase } from '@utils/mongodb/mongoClient.mjs';

export default async function checkTeamsPopulated() {
try {
const db = await getDatabase();
const count = await db.collection('teams').countDocuments({});
return { ok: true, populated: count > 0, count, error: null };
} catch (e) {
const error = e as Error;
return { ok: false, populated: false, count: 0, error: error.message };
}
}
7 changes: 7 additions & 0 deletions app/(api)/_actions/logic/ingestTeams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use server';

import { CreateManyTeams } from '@datalib/teams/createTeams';

export default async function ingestTeams(body: object) {
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

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

The parameter type 'object' is too generic. Consider using a more specific type such as ParsedRecord[] to ensure type safety and prevent invalid data from being passed to CreateManyTeams.

Suggested change
export default async function ingestTeams(body: object) {
type IngestTeamsBody = Parameters<typeof CreateManyTeams>[0];
export default async function ingestTeams(body: IngestTeamsBody) {

Copilot uses AI. Check for mistakes.
return CreateManyTeams(body);
}
28 changes: 28 additions & 0 deletions app/(api)/_actions/logic/validateCSV.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use server';

import { validateCsvBlob } from '@utils/csv-ingestion/csvAlgorithm';

export default async function validateCSV(formData: FormData) {
const file = formData.get('file') as File | null;
if (!file) {
return {
ok: false,
body: null,
validBody: null,
report: null,
error: 'Missing file',
};
}

const data = await file.arrayBuffer();
const blob = new Blob([data], { type: file.type });

const res = await validateCsvBlob(blob);
return {
ok: res.ok,
body: res.body,
validBody: res.validBody,
report: res.report,
error: res.error,
};
}
Loading