From 796d511fb3cd046833124d4be1d529a6f29665c2 Mon Sep 17 00:00:00 2001 From: Corey Mostero Date: Fri, 3 Oct 2025 17:57:46 -0700 Subject: [PATCH 1/2] feat: regular decisions --- .../grade/_components/statistics-card.tsx | 105 +++--- src/app/(auth)/grade/actions.ts | 125 +++++-- src/app/(auth)/grade/page.tsx | 8 +- src/lib/utils/airtable.ts | 338 +++--------------- src/lib/utils/util.ts | 9 +- src/schema/airtable.ts | 225 ++++++++++-- 6 files changed, 415 insertions(+), 395 deletions(-) diff --git a/src/app/(auth)/grade/_components/statistics-card.tsx b/src/app/(auth)/grade/_components/statistics-card.tsx index 56c3e02..70f1cb1 100644 --- a/src/app/(auth)/grade/_components/statistics-card.tsx +++ b/src/app/(auth)/grade/_components/statistics-card.tsx @@ -14,6 +14,7 @@ export interface StatisticsCardProps { rejectedApplications: number; deferredApplications: number; applicationsBegan: number; + regularRoundApplications: number; totalApplications: number; }>; } @@ -21,10 +22,32 @@ export interface StatisticsCardProps { export function StatisticsCard(props: StatisticsCardProps) { const statistics = use(props.statistics); + const acceptedPercentage = ( + (statistics.acceptedApplications / statistics.totalApplications) * + 100 + ).toFixed(1); + const rejectedPercentage = ( + (statistics.rejectedApplications / statistics.totalApplications) * + 100 + ).toFixed(1); + const deferredPercentage = ( + (statistics.deferredApplications / statistics.totalApplications) * + 100 + ).toFixed(1); + const applicationsBeganPercentage = ( + (statistics.applicationsBegan / statistics.regularRoundApplications) * + 100 + ).toFixed(1); + const applicationsLeftPercentage = ( + ((statistics.regularRoundApplications - statistics.applicationsBegan) / + statistics.regularRoundApplications) * + 100 + ).toFixed(1); + return ( - Priority Applications Progress + Applications Progress
@@ -35,16 +58,11 @@ export function StatisticsCard(props: StatisticsCardProps) { {statistics.acceptedApplications},{" "} - {( - (statistics.acceptedApplications / - statistics.applicationsBegan) * - 100 - ).toFixed(1)} - % + {acceptedPercentage}% accepted_applications / - applications_began + total_applications } @@ -55,16 +73,11 @@ export function StatisticsCard(props: StatisticsCardProps) { {statistics.rejectedApplications},{" "} - {( - (statistics.rejectedApplications / - statistics.applicationsBegan) * - 100 - ).toFixed(1)} - % + {rejectedPercentage}% rejected_applications / - applications_began + total_applications } @@ -75,16 +88,11 @@ export function StatisticsCard(props: StatisticsCardProps) { {statistics.deferredApplications},{" "} - {( - (statistics.deferredApplications / - statistics.applicationsBegan) * - 100 - ).toFixed(1)} - % + {deferredPercentage}% deferred_applications / - applications_began + total_applications } @@ -95,15 +103,11 @@ export function StatisticsCard(props: StatisticsCardProps) { {statistics.applicationsBegan},{" "} - {( - (statistics.applicationsBegan / - statistics.totalApplications) * - 100 - ).toFixed(1)} - % + {applicationsBeganPercentage}% - applications_began / total_applications + applications_began / + regular_round_applications } @@ -113,27 +117,44 @@ export function StatisticsCard(props: StatisticsCardProps) { value={ - {statistics.totalApplications - + {statistics.regularRoundApplications - statistics.applicationsBegan} - ,{" "} - {( - ((statistics.totalApplications - - statistics.applicationsBegan) / - statistics.totalApplications) * - 100 - ).toFixed(1)} - % + , {applicationsLeftPercentage}% - (total_applications - - applications_began) / total_applications + (regular_round_applications - + applications_began) / + regular_round_applications } /> + + {statistics.regularRoundApplications} + + + # of applications submitted during + Regular Round + + + } + /> + + + {statistics.totalApplications} + + + Total # of applications submitted + + + } />
diff --git a/src/app/(auth)/grade/actions.ts b/src/app/(auth)/grade/actions.ts index 94e9c72..8408d31 100644 --- a/src/app/(auth)/grade/actions.ts +++ b/src/app/(auth)/grade/actions.ts @@ -1,9 +1,17 @@ "use server"; -import { Console, Effect, Option } from "effect"; -import { AirtableDb, fetchHackerReviews } from "@/lib/utils/airtable"; -import { calculatePriorityStatus } from "@/lib/utils/util"; -import type { ApplicationEncoded, ReviewEncoded } from "@/schema/airtable"; +import { Console, Effect, Array as EffectArray, Option, Schema } from "effect"; +import { AirtableDb } from "@/lib/utils/airtable"; +import { calculateStatus } from "@/lib/utils/util"; +import { + Application, + type ApplicationEncoded, + ApplicationReviewer1, + ApplicationReviewer2, + ApplicationReviewer3, + Review, + type ReviewEncoded, +} from "@/schema/airtable"; export async function submitApplicationDecision( applicationId: Pick, @@ -17,30 +25,78 @@ export async function submitApplicationDecision( const applicationsTable = db.table("Applications"); const reviewsTable = db.table("Reviews"); - const reviews = yield* fetchHackerReviews; + const fetchApplication = Effect.tryPromise(() => + applicationsTable + .select({ + filterByFormula: `{Email} = '${review.email}'`, + maxRecords: 1, + }) + .firstPage(), + ).pipe( + Effect.flatMap(EffectArray.head), + Effect.flatMap((record) => + Schema.decodeUnknown(Application)({ + id: record.id, + ...record.fields, + }), + ), + ); + + const fetchReviews = Effect.tryPromise(() => + reviewsTable + .select({ + filterByFormula: `{Email} = '${review.email}'`, + }) + .all(), + ).pipe( + Effect.flatMap((records) => + Effect.allSuccesses( + records.map((record) => + Schema.decodeUnknown(Review)({ + id: record.id, + ...record.fields, + }), + ), + ), + ), + ); + + const [application, reviews] = yield* Effect.all( + [fetchApplication, fetchReviews], + { concurrency: 2 }, + ); + const decisions = { - accept: review.decision === "accept" ? 1 : 0, - reject: review.decision === "reject" ? 1 : 0, + accept: 0, + reject: 0, }; - for (const { decision, email } of reviews) { - if (review.email === email) { - if (decision === "accept") { - decisions.accept += 1; - } else if (decision === "reject") { - decisions.reject += 1; - } - } - if (decisions.accept > 2 || decisions.reject > 1) { - return; + for (const { decision } of reviews) { + if (decision === "accept") { + decisions.accept += 1; + } else if (decision === "reject") { + decisions.reject += 1; } } - const status = calculatePriorityStatus(decisions); + if ( + !application.reviewNeeded || + decisions.accept >= 1 || + decisions.reject >= 2 + ) { + return; + } + + const adjustedDecisions = { + accept: decisions.accept + (review.decision === "accept" ? 1 : 0), + reject: decisions.reject + (review.decision === "reject" ? 1 : 0), + }; + + const status = calculateStatus(adjustedDecisions); const insertDecision = Option.match(status, { onNone: () => Effect.void, - onSome: (status) => - Effect.tryPromise(() => + onSome: (status) => { + return Effect.tryPromise(() => applicationsTable.update([ { id: applicationId.id, @@ -49,7 +105,25 @@ export async function submitApplicationDecision( }, }, ]), - ), + ); + }, + }); + + const insertReviewer = Effect.tryPromise(() => { + const reviewerSlot = + application.reviewer1 === undefined + ? ApplicationReviewer1.literals[0] + : application.reviewer2 === undefined + ? ApplicationReviewer2.literals[0] + : ApplicationReviewer3.literals[0]; + return applicationsTable.update([ + { + id: applicationId.id, + fields: { + [reviewerSlot]: review.reviewer_id, + }, + }, + ]); }); const insertReview = Effect.tryPromise(() => @@ -65,9 +139,12 @@ export async function submitApplicationDecision( ]), ); - return yield* Effect.all([insertDecision, insertReview], { - concurrency: 2, - }); + return yield* Effect.all( + [insertDecision, insertReviewer, insertReview], + { + concurrency: "unbounded", + }, + ); }).pipe( Effect.tapErrorCause((error) => Console.error(error)), Effect.withSpan("app/(auth)/grade/actions/submitApplicationDecision"), diff --git a/src/app/(auth)/grade/page.tsx b/src/app/(auth)/grade/page.tsx index 2b8002a..d35ba09 100644 --- a/src/app/(auth)/grade/page.tsx +++ b/src/app/(auth)/grade/page.tsx @@ -8,7 +8,7 @@ import { } from "@/app/(auth)/grade/_components/statistics-card"; import { createClient } from "@/lib/supabase/server"; import { - findNewHackerApplication, + findHackerApplication, progressStatistics, } from "@/lib/utils/airtable"; import { SupabaseUser } from "@/lib/utils/supabase"; @@ -16,13 +16,11 @@ import { SupabaseUser } from "@/lib/utils/supabase"; export default async function Grade() { const supabase = await createClient(); const user = SupabaseUser(supabase).pipe(Effect.runPromise); - const application = findNewHackerApplication({ priority: true }).pipe( + const application = findHackerApplication.pipe( Effect.map(Option.getOrElse(() => null)), Effect.runPromise, ); - const statistics = progressStatistics({ priority: true }).pipe( - Effect.runPromise, - ); + const statistics = progressStatistics.pipe(Effect.runPromise); return (
diff --git a/src/lib/utils/airtable.ts b/src/lib/utils/airtable.ts index 109b39e..89b36b2 100644 --- a/src/lib/utils/airtable.ts +++ b/src/lib/utils/airtable.ts @@ -3,6 +3,7 @@ import { Chunk, Console, Effect, + Array as EffectArray, Option, Random, Redacted, @@ -13,9 +14,17 @@ import { createClient } from "@/lib/supabase/server"; import { SupabaseUser } from "@/lib/utils/supabase"; import { Application, + type ApplicationColumns, + ApplicationEmail, + ApplicationReviewNeeded, + ApplicationStatus, type ApplicationType, Decision, Review, + Status, + StatusAccept, + StatusDeferred, + StatusRejected, } from "@/schema/airtable"; Effect.gen(function* () { @@ -35,65 +44,21 @@ export const AirtableDb = Effect.gen(function* () { Effect.withSpan("lib/utils/airtable"), ); -export const findNewHackerApplication = Effect.fn( - "lib/utils/airtable/findNewHackerApplication", -)(function* ({ priority }: { priority: boolean }) { +export const findHackerApplication = Effect.gen(function* () { const supabase = yield* Effect.tryPromise(() => createClient()); const user = yield* SupabaseUser(supabase); const db = yield* AirtableDb; const applicationsTable = db.table("Applications"); - const reviewsTable = db.table("Reviews"); - - const reviews = yield* Effect.tryPromise(() => - reviewsTable.select().all(), - ).pipe( - Effect.flatMap((records) => - Effect.allSuccesses( - records.map((record) => - Schema.decodeUnknown(Review)(record.fields), - ), - ), - ), - ); - - const previouslyReviewedEmails = new Set( - reviews - .filter((review) => review.reviewerId === user.id) - .map((review) => review.email), - ); - - const reviewDecisionCounts = reviews.reduce((count, review) => { - const reviewCount = count.get(review.email); - if (!reviewCount) { - count.set(review.email, { - accepts: review.decision === "accept" ? 1 : 0, - rejects: review.decision === "reject" ? 1 : 0, - }); - return count; - } - if (review.decision === "accept") { - reviewCount.accepts += 1; - } - if (review.decision === "reject") { - reviewCount.rejects += 1; - } - return count; - }, new Map()); - - const completedEmails = new Set( - Array.from(reviewDecisionCounts.entries()) - .filter(([_, { accepts, rejects }]) => accepts >= 2 || rejects >= 1) - .map(([email]) => email), - ); - - const excludedEmails = previouslyReviewedEmails.union(completedEmails); return yield* Effect.async>((resume) => { applicationsTable .select({ filterByFormula: `AND( {Role} = "Hacker", - {Review needed} = 1 + {Review needed} = 1, + {Reviewer 1} != "${user.id}", + {Reviewer 2} != "${user.id}", + {Reviewer 3} != "${user.id}" )`, pageSize: 100, }) @@ -107,14 +72,9 @@ export const findNewHackerApplication = Effect.fn( }), ), ).pipe( - Effect.flatMap(Random.shuffle), - Effect.map( - Chunk.findFirst((application) => - application.email - ? !excludedEmails.has(application.email) - : false, - ), - ), + // Effect.flatMap(Random.shuffle), + // Effect.map(Chunk.head), + Effect.map(EffectArray.head), Effect.tap( Option.match({ onNone: () => paginate(), @@ -132,11 +92,14 @@ export const findNewHackerApplication = Effect.fn( () => resume(Effect.succeed(Option.none())), ); }); -}); +}).pipe( + Effect.tapErrorCause(Console.error), + Effect.withLogSpan("lib/utils/airtable/findHackerApplication"), +); export const fetchHackerApplications = Effect.fn( "lib/utils/airtable/fetchHackerApplications", -)(function* (fields?: string[]) { +)(function* (fields?: ApplicationColumns[]) { const db = yield* AirtableDb; const applicationsTable = db.table("Applications"); @@ -176,14 +139,10 @@ export const fetchHackerReviews = Effect.gen(function* () { ); }).pipe( Effect.tapErrorCause((error) => Console.error(error)), - Effect.withSpan("lib/utils/airtable/fetchHackerReviews"), + Effect.withLogSpan("lib/utils/airtable/fetchHackerReviews"), ); -export const progressStatistics = Effect.fnUntraced(function* ({ - priority, -}: { - priority: boolean; -}) { +export const progressStatistics = Effect.gen(function* () { const db = yield* AirtableDb; const applicationsTable = db.table("Applications"); const reviewsTable = db.table("Reviews"); @@ -192,10 +151,13 @@ export const progressStatistics = Effect.fnUntraced(function* ({ applicationsTable .select({ filterByFormula: `AND( - {Role} = "Hacker", - {Review needed} = 1 + {Role} = "Hacker" )`, - fields: ["Email"], + fields: [ + ApplicationEmail.literals[0], + ApplicationStatus.literals[0], + ApplicationReviewNeeded.literals[0], + ], }) .all(), ).pipe( @@ -203,14 +165,12 @@ export const progressStatistics = Effect.fnUntraced(function* ({ Effect.allSuccesses( records.map((record) => Schema.decodeUnknown( - Schema.Struct({ - Email: Schema.String, - }), + Application.pick("email", "status", "reviewNeeded"), )(record.fields), ), ), ), - Effect.map((records) => new Set(records.map((record) => record.Email))), + Effect.map((records) => new Set(records)), ); const fetchReviews = Effect.tryPromise(() => @@ -219,12 +179,9 @@ export const progressStatistics = Effect.fnUntraced(function* ({ Effect.flatMap((records) => Effect.allSuccesses( records.map((record) => - Schema.decodeUnknown( - Schema.Struct({ - email: Schema.String, - decision: Decision, - }), - )(record.fields), + Schema.decodeUnknown(Review.pick("email", "decision"))( + record.fields, + ), ), ), ), @@ -235,214 +192,35 @@ export const progressStatistics = Effect.fnUntraced(function* ({ { concurrency: 2 }, ); - const acceptedApplications = new Set(); - const rejectedApplications = new Set(); - const deferredApplications = new Set(); - let applicationsBegan = 0; - - const reviewsCache = new Map< - string, - { accepted: number; rejected: number } - >(); - for (const review of reviews) { - if (!applications.has(review.email)) { - continue; - } - - const entry = reviewsCache.get(review.email); - if (!entry) { - reviewsCache.set(review.email, { - accepted: review.decision === "accept" ? 1 : 0, - rejected: review.decision === "reject" ? 1 : 0, - }); - applicationsBegan += 1; - } else { - if (review.decision === "accept") { - entry.accepted += 1; - } else if (review.decision === "reject") { - entry.rejected += 1; - } - } - } - - for (const [email, entry] of reviewsCache) { - if (entry.accepted >= 2) { - acceptedApplications.add(email); - } else if (entry.accepted >= 1 && entry.rejected >= 1) { - deferredApplications.add(email); - } else if (entry.rejected >= 1) { - rejectedApplications.add(email); - } - } - - return { - acceptedApplications: acceptedApplications.size, - rejectedApplications: rejectedApplications.size, - deferredApplications: deferredApplications.size, - applicationsBegan, - totalApplications: applications.size, - }; -}); - -export const priorityEmailResults = Effect.gen(function* () { - const db = yield* AirtableDb; - const applicationsTable = db.table("Applications"); - const reviewsTable = db.table("Reviews"); - - const fetchApplications = Effect.tryPromise(() => - applicationsTable - .select({ - filterByFormula: `AND( - {Role} = "Hacker", - {Review needed} = 1 - )`, - fields: ["Email"], - }) - .all(), - ).pipe( - Effect.flatMap((records) => - Effect.allSuccesses( - records.map((record) => - Schema.decodeUnknown( - Schema.Struct({ - Email: Schema.String, - }), - )(record.fields), - ), - ), - ), - Effect.map((records) => new Set(records.map((record) => record.Email))), + const applicationsByStatus = EffectArray.groupBy( + applications, + (application) => application.status ?? "No Status", ); - const fetchReviews = Effect.tryPromise(() => - reviewsTable.select({ fields: ["email", "decision"] }).all(), - ).pipe( - Effect.flatMap((records) => - Effect.allSuccesses( - records.map((record) => - Schema.decodeUnknown( - Schema.Struct({ - email: Schema.String, - decision: Decision, - }), - )(record.fields), - ), - ), - ), - ); + const [_applicationsReviewed, applicationsNotReviewed] = + EffectArray.partition( + applications, + (application) => application.reviewNeeded === 1, + ); - const [applications, reviews] = yield* Effect.all( - [fetchApplications, fetchReviews], - { concurrency: 2 }, + const reviewsByEmail = EffectArray.groupBy( + reviews, + (review) => review.email, ); - const reviewCache = new Map(); - for (const review of reviews) { - if (!applications.has(review.email)) { - continue; - } - - const entry = reviewCache.get(review.email); - if (!entry) { - reviewCache.set(review.email, { - accepts: review.decision === "accept" ? 1 : 0, - rejects: review.decision === "reject" ? 1 : 0, - }); - } else { - if (review.decision === "accept") { - entry.accepts += 1; - } else if (review.decision === "reject") { - entry.rejects += 1; - } - } - } - - const accepted: string[] = []; - const deferred: string[] = []; - - for (const [email, { accepts, rejects }] of reviewCache) { - if (accepts >= 2 && rejects === 0) { - accepted.push(email); - } else if (rejects >= 1) { - deferred.push(email); - } - } - return { - accepted, - deferred, - }; -}).pipe( - Effect.tapErrorCause((error) => - Console.error("priorityEmailResults", error), - ), - Effect.withSpan("lib/utils/airtable/priorityEmailResults"), -); - -export const priorityEmailResultsToCsv = Effect.gen(function* () { - const { accepted, deferred } = yield* priorityEmailResults; - - const rows: string[] = []; - rows.push("accepted_emails,deferred_emails"); - - const maxLength = Math.max(accepted.length, deferred.length); - - for (let index = 0; index < maxLength; index++) { - const acceptedEmail = accepted[index] ?? ""; - const deferredEmail = deferred[index] ?? ""; - rows.push(`${acceptedEmail},${deferredEmail}`); - } - - return rows.join("\n"); -}); - -export const priorityUpdateApplicationStatus = Effect.gen(function* () { - const db = yield* AirtableDb; - const applicationsTable = db.table("Applications"); - - const { accepted, deferred } = yield* priorityEmailResults; - - const updates: { id: string; fields: { Status: string } }[] = []; - - const priorityApps = yield* Effect.tryPromise(() => - applicationsTable - .select({ - filterByFormula: `AND( - {Role} = "Hacker", - {Created at} < DATETIME_PARSE("2025-09-24 07:00", "YYYY-MM-DD HH:mm") - )`, - fields: ["Email", "Status"], - }) - .all(), - ); - - for (const record of priorityApps) { - const email = record.fields.Email; - if (!email || typeof email !== "string") { - continue; - } - - if (accepted.includes(email)) { - updates.push({ id: record.id, fields: { Status: "Accept" } }); - } else if (deferred.includes(email)) { - updates.push({ id: record.id, fields: { Status: "Deferred" } }); - } - } - - const chunkSize = 10; - for (let index = 0; index < updates.length; index += chunkSize) { - const chunk = updates.slice(index, index + chunkSize); - yield* Effect.tryPromise(() => applicationsTable.update(chunk)); - } - - return { - updated: updates.length, - accepted: accepted.length, - deferred: deferred.length, + acceptedApplications: + applicationsByStatus[StatusAccept.literals[0]].length || 0, + rejectedApplications: + applicationsByStatus[StatusRejected.literals[0]].length || 0, + deferredApplications: + applicationsByStatus[StatusDeferred.literals[0]].length || 0, + applicationsBegan: Object.keys(reviewsByEmail).length, + regularRoundApplications: + Object.keys(reviewsByEmail).length + applicationsNotReviewed.length, + totalApplications: applications.size, }; }).pipe( - Effect.tapErrorCause((error) => - Console.error("priorityUpdateApplicationStatus", error), - ), - Effect.withSpan("lib/utils/airtable/priorityUpdateApplicationStatus"), + Effect.tapErrorCause((error) => Console.error(error)), + Effect.withLogSpan("lib/utils/airtable/progressStatistics"), ); diff --git a/src/lib/utils/util.ts b/src/lib/utils/util.ts index 09e3983..b589b13 100644 --- a/src/lib/utils/util.ts +++ b/src/lib/utils/util.ts @@ -27,20 +27,17 @@ export function isAdult(birthdate: Date, date: Date): boolean { return date >= eighteenthBirthday; } -export function calculatePriorityStatus({ +export function calculateStatus({ accept, reject, }: { accept: number; reject: number; }): Option.Option<(typeof Status.members)[number]> { - if (accept >= 1 && reject >= 1) { - return Status.pipe(Schema.pickLiteral("Deferred"), Option.some); - } - if (accept >= 2) { + if (accept >= 1) { return Status.pipe(Schema.pickLiteral("Accept"), Option.some); } - if (reject >= 1) { + if (reject >= 2) { return Status.pipe(Schema.pickLiteral("Rejected"), Option.some); } return Option.none(); diff --git a/src/schema/airtable.ts b/src/schema/airtable.ts index e88887d..d20e22a 100644 --- a/src/schema/airtable.ts +++ b/src/schema/airtable.ts @@ -13,59 +13,188 @@ export const Status = Schema.Union( ); export type Status = typeof Status.Type; +export const ApplicationEmail = Schema.Literal("Email"); +export const ApplicationFirstName = Schema.Literal("First Name"); +export const ApplicationLastName = Schema.Literal("Last Name"); +export const ApplicationPhone = Schema.Literal("Phone"); +export const ApplicationRole = Schema.Literal("Role"); +export const ApplicationBirthday = Schema.Literal("Birthday"); +export const ApplicationUniversity = Schema.Literal("University"); +export const ApplicationMajorStudying = Schema.Literal("Major / studying"); +export const ApplicationLevelOfStudy = Schema.Literal("Level of study"); +export const ApplicationGraduationClass = Schema.Literal("Graduation class"); +export const ApplicationCountry = Schema.Literal("Country"); +export const ApplicationUnderrepresented = Schema.Literal("Underrepresented?"); +export const ApplicationGender = Schema.Literal("Gender"); +export const ApplicationRaceEthnicity = Schema.Literal("Race / ethnicity"); +export const ApplicationPreviousHackathons = Schema.Literal( + "Previous hackathons", +); +export const ApplicationLgbtq = Schema.Literal("LGBTQ?"); +export const ApplicationBeenToCalHacksBefore = Schema.Literal( + "Been to Cal Hacks before?", +); +export const ApplicationPreviousBerkeleyHackathons = Schema.Literal( + "Previous Hackathons @ Berkeley events", +); +export const ApplicationReferrer = Schema.Literal("Referrer"); +export const ApplicationFavouriteProject = Schema.Literal("Favourite project"); +export const ApplicationPlannedProject = Schema.Literal("Planned project"); +export const ApplicationTakeaways = Schema.Literal("Takeaways"); +export const ApplicationGoal = Schema.Literal("Goal"); +export const ApplicationJoke = Schema.Literal("Joke"); +export const ApplicationGithub = Schema.Literal("GitHub"); +export const ApplicationDevpost = Schema.Literal("Devpost"); +export const ApplicationLinkedIn = Schema.Literal("LinkedIn"); +export const ApplicationResume = Schema.Literal("Resume"); +export const ApplicationEmployer = Schema.Literal("Employer"); +export const ApplicationJobTitle = Schema.Literal("Job title"); +export const ApplicationAreasOfExpertise = Schema.Literal("Areas of expertise"); +export const ApplicationLookingForwardJudge = Schema.Literal( + "Looking forward (judge)", +); +export const ApplicationPastProjectJudge = Schema.Literal( + "Past project (judge)", +); +export const ApplicationVolunteerIntroduction = Schema.Literal( + "Volunteer introduction", +); +export const ApplicationStatus = Schema.Literal("Status"); +export const ApplicationReviewer1 = Schema.Literal("Reviewer 1"); +export const ApplicationReviewer2 = Schema.Literal("Reviewer 2"); +export const ApplicationReviewer3 = Schema.Literal("Reviewer 3"); +export const ApplicationCreatedAt = Schema.Literal("Created at"); +export const ApplicationReviewNeeded = Schema.Literal("Review needed"); + +export const ApplicationColumns = Schema.Union( + ApplicationEmail, + ApplicationFirstName, + ApplicationLastName, + ApplicationPhone, + ApplicationRole, + ApplicationBirthday, + ApplicationUniversity, + ApplicationMajorStudying, + ApplicationLevelOfStudy, + ApplicationGraduationClass, + ApplicationCountry, + ApplicationUnderrepresented, + ApplicationGender, + ApplicationRaceEthnicity, + ApplicationPreviousHackathons, + ApplicationLgbtq, + ApplicationBeenToCalHacksBefore, + ApplicationPreviousBerkeleyHackathons, + ApplicationReferrer, + ApplicationFavouriteProject, + ApplicationPlannedProject, + ApplicationTakeaways, + ApplicationGoal, + ApplicationJoke, + ApplicationGithub, + ApplicationDevpost, + ApplicationLinkedIn, + ApplicationResume, + ApplicationEmployer, + ApplicationJobTitle, + ApplicationAreasOfExpertise, + ApplicationLookingForwardJudge, + ApplicationPastProjectJudge, + ApplicationVolunteerIntroduction, + ApplicationStatus, + ApplicationReviewer1, + ApplicationReviewer2, + ApplicationReviewer3, + ApplicationCreatedAt, + ApplicationReviewNeeded, +); + +export type ApplicationColumns = typeof ApplicationColumns.Type; + export const Application = Schema.Struct({ id: Schema.String, - email: Schema.optional(Schema.String).pipe(Schema.fromKey("Email")), + email: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationEmail.literals[0]), + ), firstName: Schema.optional(Schema.String).pipe( - Schema.fromKey("First Name"), + Schema.fromKey(ApplicationFirstName.literals[0]), + ), + lastName: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationLastName.literals[0]), + ), + phone: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationPhone.literals[0]), + ), + role: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationRole.literals[0]), + ), + birthday: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationBirthday.literals[0]), ), - lastName: Schema.optional(Schema.String).pipe(Schema.fromKey("Last Name")), - phone: Schema.optional(Schema.String).pipe(Schema.fromKey("Phone")), - role: Schema.optional(Schema.String).pipe(Schema.fromKey("Role")), - birthday: Schema.optional(Schema.String).pipe(Schema.fromKey("Birthday")), university: Schema.optional(Schema.String).pipe( - Schema.fromKey("University"), + Schema.fromKey(ApplicationUniversity.literals[0]), ), majorStudying: Schema.optional(Schema.String).pipe( - Schema.fromKey("Major / studying"), + Schema.fromKey(ApplicationMajorStudying.literals[0]), ), levelOfStudy: Schema.optional(Schema.String).pipe( - Schema.fromKey("Level of study"), + Schema.fromKey(ApplicationLevelOfStudy.literals[0]), ), graduationClass: Schema.optional(Schema.String).pipe( - Schema.fromKey("Graduation class"), + Schema.fromKey(ApplicationGraduationClass.literals[0]), + ), + country: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationCountry.literals[0]), ), - country: Schema.optional(Schema.String).pipe(Schema.fromKey("Country")), underrepresented: Schema.optional(Schema.String).pipe( - Schema.fromKey("Underrepresented?"), + Schema.fromKey(ApplicationUnderrepresented.literals[0]), + ), + gender: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationGender.literals[0]), ), - gender: Schema.optional(Schema.String).pipe(Schema.fromKey("Gender")), raceEthnicity: Schema.optional(Schema.String).pipe( - Schema.fromKey("Race / ethnicity"), + Schema.fromKey(ApplicationRaceEthnicity.literals[0]), ), previousHackathons: Schema.optional(Schema.String).pipe( - Schema.fromKey("Previous hackathons"), + Schema.fromKey(ApplicationPreviousHackathons.literals[0]), + ), + lgbtq: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationLgbtq.literals[0]), ), - lgbtq: Schema.optional(Schema.String).pipe(Schema.fromKey("LGBTQ?")), beenToCalHacks: Schema.optional(Schema.String).pipe( - Schema.fromKey("Been to Cal Hacks before?"), + Schema.fromKey(ApplicationBeenToCalHacksBefore.literals[0]), ), previousBerkeleyHackathons: Schema.optional(Schema.String).pipe( - Schema.fromKey("Previous Hackathons @ Berkeley events"), + Schema.fromKey(ApplicationPreviousBerkeleyHackathons.literals[0]), + ), + referrer: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationReferrer.literals[0]), ), - referrer: Schema.optional(Schema.String).pipe(Schema.fromKey("Referrer")), favouriteProject: Schema.optional(Schema.String).pipe( - Schema.fromKey("Favourite project"), + Schema.fromKey(ApplicationFavouriteProject.literals[0]), ), plannedProject: Schema.optional(Schema.String).pipe( - Schema.fromKey("Planned project"), - ), - takeaways: Schema.optional(Schema.String).pipe(Schema.fromKey("Takeaways")), - goal: Schema.optional(Schema.String).pipe(Schema.fromKey("Goal")), - joke: Schema.optional(Schema.String).pipe(Schema.fromKey("Joke")), - github: Schema.optional(Schema.String).pipe(Schema.fromKey("GitHub")), - devpost: Schema.optional(Schema.String).pipe(Schema.fromKey("Devpost")), - linkedin: Schema.optional(Schema.String).pipe(Schema.fromKey("LinkedIn")), + Schema.fromKey(ApplicationPlannedProject.literals[0]), + ), + takeaways: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationTakeaways.literals[0]), + ), + goal: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationGoal.literals[0]), + ), + joke: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationJoke.literals[0]), + ), + github: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationGithub.literals[0]), + ), + devpost: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationDevpost.literals[0]), + ), + linkedin: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationLinkedIn.literals[0]), + ), + resume: Schema.optional( Schema.Array( Schema.Struct({ @@ -96,24 +225,44 @@ export const Application = Schema.Struct({ ), }), ), - ).pipe(Schema.fromKey("Resume")), - employer: Schema.optional(Schema.String).pipe(Schema.fromKey("Employer")), - jobTitle: Schema.optional(Schema.String).pipe(Schema.fromKey("Job title")), + ).pipe(Schema.fromKey(ApplicationResume.literals[0])), + + employer: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationEmployer.literals[0]), + ), + jobTitle: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationJobTitle.literals[0]), + ), areasOfExpertise: Schema.optional(Schema.String).pipe( - Schema.fromKey("Areas of expertise"), + Schema.fromKey(ApplicationAreasOfExpertise.literals[0]), ), lookingForwardJudge: Schema.optional(Schema.String).pipe( - Schema.fromKey("Looking forward (judge)"), + Schema.fromKey(ApplicationLookingForwardJudge.literals[0]), ), pastProjectJudge: Schema.optional(Schema.String).pipe( - Schema.fromKey("Past project (judge)"), + Schema.fromKey(ApplicationPastProjectJudge.literals[0]), ), volunteerIntroduction: Schema.optional(Schema.String).pipe( - Schema.fromKey("Volunteer introduction"), + Schema.fromKey(ApplicationVolunteerIntroduction.literals[0]), + ), + + status: Schema.optional(Status).pipe( + Schema.fromKey(ApplicationStatus.literals[0]), + ), + reviewer1: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationReviewer1.literals[0]), + ), + reviewer2: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationReviewer2.literals[0]), + ), + reviewer3: Schema.optional(Schema.String).pipe( + Schema.fromKey(ApplicationReviewer3.literals[0]), ), - status: Schema.optional(Status).pipe(Schema.fromKey("Status")), createdAt: Schema.optional(Schema.String).pipe( - Schema.fromKey("Created at"), + Schema.fromKey(ApplicationCreatedAt.literals[0]), + ), + reviewNeeded: Schema.optional(Schema.Number).pipe( + Schema.fromKey(ApplicationReviewNeeded.literals[0]), ), }); @@ -121,7 +270,7 @@ export type ApplicationType = typeof Application.Type; export type ApplicationEncoded = typeof Application.Encoded; export const Decision = Schema.Literal("accept", "reject"); -export type Decision = typeof Decision; +export type Decision = typeof Decision.Type; export const Review = Schema.Struct({ id: Schema.Number, From 3fe1befb10163e405283229e5461bcde3c179927 Mon Sep 17 00:00:00 2001 From: Corey Mostero Date: Fri, 3 Oct 2025 17:59:49 -0700 Subject: [PATCH 2/2] fix: bug --- src/lib/utils/airtable.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/utils/airtable.ts b/src/lib/utils/airtable.ts index 89b36b2..7b8e5d8 100644 --- a/src/lib/utils/airtable.ts +++ b/src/lib/utils/airtable.ts @@ -72,9 +72,8 @@ export const findHackerApplication = Effect.gen(function* () { }), ), ).pipe( - // Effect.flatMap(Random.shuffle), - // Effect.map(Chunk.head), - Effect.map(EffectArray.head), + Effect.flatMap(Random.shuffle), + Effect.map(Chunk.head), Effect.tap( Option.match({ onNone: () => paginate(),