From fb9df9cbdeb38dc5d92a3d6f7438c6d1efcb7ccc Mon Sep 17 00:00:00 2001 From: Aidan Kelly Date: Thu, 26 Feb 2026 14:17:37 -0500 Subject: [PATCH 1/2] Fix mentor and mentee questions and adjust algo --- server/drizzle/schema.ts | 272 ++--- server/scripts/test-recommendations.ts | 59 +- server/src/data/db/recommendation-queries.ts | 11 +- server/src/data/db/schema.ts | 234 +--- server/src/data/repository/mentee-repo.ts | 41 +- server/src/data/repository/mentor-repo.ts | 32 +- server/src/types/mentee-types.ts | 32 +- server/src/types/mentor-types.ts | 22 +- web/package-lock.json | 1022 ++++++++++++++++-- web/package.json | 1 + web/src/app/mentorship/apply/mentee/page.tsx | 87 +- web/src/app/mentorship/apply/mentor/page.tsx | 106 +- 12 files changed, 1180 insertions(+), 739 deletions(-) diff --git a/server/drizzle/schema.ts b/server/drizzle/schema.ts index 087ac14a..071f733c 100644 --- a/server/drizzle/schema.ts +++ b/server/drizzle/schema.ts @@ -23,60 +23,30 @@ export const careerStageEnum = pgEnum("career_stage_enum", [ "transitioning", "no-preference", ]); -export const channelPostPermissionEnum = pgEnum( - "channel_post_permission_enum", - ["admin", "everyone", "custom"], -); -export const matchStatusEnum = pgEnum("match_status_enum", [ - "pending", - "accepted", - "declined", -]); -export const meetingFormatEnum = pgEnum("meeting_format_enum", [ - "in-person", - "virtual", - "hybrid", - "no-preference", -]); -export const menteeStatusEnum = pgEnum("mentee_status_enum", [ - "active", - "inactive", - "matched", -]); -export const mentorStatusEnum = pgEnum("mentor_status_enum", [ - "requested", - "approved", - "active", -]); -export const mentorshipUserTypeEnum = pgEnum("mentorship_user_type_enum", [ - "mentor", - "mentee", +export const channelPostPermissionEnum = pgEnum("channel_post_permission_enum", [ + "admin", + "everyone", + "custom", ]); +export const matchStatusEnum = pgEnum("match_status_enum", ["pending", "accepted", "declined"]); +export const meetingFormatEnum = pgEnum("meeting_format_enum", ["in-person", "virtual", "hybrid"]); +export const menteeStatusEnum = pgEnum("mentee_status_enum", ["active", "inactive", "matched"]); +export const mentorStatusEnum = pgEnum("mentor_status_enum", ["requested", "approved", "active"]); +export const mentorshipUserTypeEnum = pgEnum("mentorship_user_type_enum", ["mentor", "mentee"]); export const messageBlastStatusEnum = pgEnum("message_blast_status_enum", [ "draft", "sent", "failed", ]); -export const permissionEnum = pgEnum("permission_enum", [ - "read", - "write", - "both", -]); -export const positionTypeEnum = pgEnum("position_type_enum", [ - "active", - "part-time", -]); +export const permissionEnum = pgEnum("permission_enum", ["read", "write", "both"]); +export const positionTypeEnum = pgEnum("position_type_enum", ["active", "part-time"]); export const reportCategoryEnum = pgEnum("report_category_enum", [ "Communication", "Mentorship", "Training", "Resources", ]); -export const reportStatusEnum = pgEnum("report_status_enum", [ - "Pending", - "Assigned", - "Resolved", -]); +export const reportStatusEnum = pgEnum("report_status_enum", ["Pending", "Assigned", "Resolved"]); export const roleNamespaceEnum = pgEnum("role_namespace_enum", [ "global", "channel", @@ -84,10 +54,7 @@ export const roleNamespaceEnum = pgEnum("role_namespace_enum", [ "broadcast", "reporting", ]); -export const serviceTypeEnum = pgEnum("service_type_enum", [ - "enlisted", - "officer", -]); +export const serviceTypeEnum = pgEnum("service_type_enum", ["enlisted", "officer"]); export const visibilityEnum = pgEnum("visibility_enum", ["private", "public"]); export const account = pgTable( @@ -108,9 +75,7 @@ export const account = pgTable( }), scope: text(), password: text(), - createdAt: timestamp("created_at", { mode: "string" }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { mode: "string" }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { mode: "string" }).notNull(), }, () => [ @@ -126,43 +91,30 @@ export const account = pgTable( export const channelSubscriptions = pgTable( "channel_subscriptions", { - subscriptionId: integer("subscription_id") - .primaryKey() - .generatedAlwaysAsIdentity({ - name: "channel_subscriptions_subscription_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + subscriptionId: integer("subscription_id").primaryKey().generatedAlwaysAsIdentity({ + name: "channel_subscriptions_subscription_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), userId: text("user_id").notNull(), channelId: integer("channel_id").notNull(), - notificationsEnabled: boolean("notifications_enabled") - .default(true) - .notNull(), + notificationsEnabled: boolean("notifications_enabled").default(true).notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) .defaultNow() .notNull(), }, () => [ - check( - "channel_subscriptions_subscription_id_not_null", - sql`NOT NULL subscription_id`, - ), + check("channel_subscriptions_subscription_id_not_null", sql`NOT NULL subscription_id`), check("channel_subscriptions_user_id_not_null", sql`NOT NULL user_id`), - check( - "channel_subscriptions_channel_id_not_null", - sql`NOT NULL channel_id`, - ), + check("channel_subscriptions_channel_id_not_null", sql`NOT NULL channel_id`), check( "channel_subscriptions_notifications_enabled_not_null", sql`NOT NULL notifications_enabled`, ), - check( - "channel_subscriptions_created_at_not_null", - sql`NOT NULL created_at`, - ), + check("channel_subscriptions_created_at_not_null", sql`NOT NULL created_at`), ], ); @@ -191,10 +143,7 @@ export const channels = pgTable( check("channels_channel_id_not_null", sql`NOT NULL channel_id`), check("channels_name_not_null", sql`NOT NULL name`), check("channels_created_at_not_null", sql`NOT NULL created_at`), - check( - "channels_post_permission_level_not_null", - sql`NOT NULL post_permission_level`, - ), + check("channels_post_permission_level_not_null", sql`NOT NULL post_permission_level`), ], ); @@ -291,16 +240,14 @@ export const mentees = pgTable( export const mentorRecommendations = pgTable( "mentor_recommendations", { - recommendationId: integer("recommendation_id") - .primaryKey() - .generatedAlwaysAsIdentity({ - name: "mentor_recommendations_recommendation_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + recommendationId: integer("recommendation_id").primaryKey().generatedAlwaysAsIdentity({ + name: "mentor_recommendations_recommendation_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), userId: text("user_id").notNull(), recommendedMentorIds: jsonb("recommended_mentor_ids").notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) @@ -309,19 +256,13 @@ export const mentorRecommendations = pgTable( expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }), }, () => [ - check( - "mentor_recommendations_recommendation_id_not_null", - sql`NOT NULL recommendation_id`, - ), + check("mentor_recommendations_recommendation_id_not_null", sql`NOT NULL recommendation_id`), check("mentor_recommendations_user_id_not_null", sql`NOT NULL user_id`), check( "mentor_recommendations_recommended_mentor_ids_not_null", sql`NOT NULL recommended_mentor_ids`, ), - check( - "mentor_recommendations_created_at_not_null", - sql`NOT NULL created_at`, - ), + check("mentor_recommendations_created_at_not_null", sql`NOT NULL created_at`), ], ); @@ -368,16 +309,14 @@ export const mentors = pgTable( export const mentorshipEmbeddings = pgTable( "mentorship_embeddings", { - embeddingId: integer("embedding_id") - .primaryKey() - .generatedAlwaysAsIdentity({ - name: "mentorship_embeddings_embedding_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + embeddingId: integer("embedding_id").primaryKey().generatedAlwaysAsIdentity({ + name: "mentorship_embeddings_embedding_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), userId: text("user_id").notNull(), userType: mentorshipUserTypeEnum("user_type").notNull(), whyInterestedEmbedding: vector("why_interested_embedding", { @@ -393,20 +332,11 @@ export const mentorshipEmbeddings = pgTable( .notNull(), }, () => [ - check( - "mentorship_embeddings_embedding_id_not_null", - sql`NOT NULL embedding_id`, - ), + check("mentorship_embeddings_embedding_id_not_null", sql`NOT NULL embedding_id`), check("mentorship_embeddings_user_id_not_null", sql`NOT NULL user_id`), check("mentorship_embeddings_user_type_not_null", sql`NOT NULL user_type`), - check( - "mentorship_embeddings_created_at_not_null", - sql`NOT NULL created_at`, - ), - check( - "mentorship_embeddings_updated_at_not_null", - sql`NOT NULL updated_at`, - ), + check("mentorship_embeddings_created_at_not_null", sql`NOT NULL created_at`), + check("mentorship_embeddings_updated_at_not_null", sql`NOT NULL updated_at`), ], ); @@ -439,16 +369,14 @@ export const mentorshipMatches = pgTable( export const messageAttachments = pgTable( "message_attachments", { - attachmentId: integer("attachment_id") - .primaryKey() - .generatedAlwaysAsIdentity({ - name: "message_attachments_attachment_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + attachmentId: integer("attachment_id").primaryKey().generatedAlwaysAsIdentity({ + name: "message_attachments_attachment_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), messageId: integer("message_id").notNull(), fileId: uuid("file_id").notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) @@ -456,10 +384,7 @@ export const messageAttachments = pgTable( .notNull(), }, () => [ - check( - "message_attachments_attachment_id_not_null", - sql`NOT NULL attachment_id`, - ), + check("message_attachments_attachment_id_not_null", sql`NOT NULL attachment_id`), check("message_attachments_message_id_not_null", sql`NOT NULL message_id`), check("message_attachments_file_id_not_null", sql`NOT NULL file_id`), check("message_attachments_created_at_not_null", sql`NOT NULL created_at`), @@ -561,16 +486,14 @@ export const messages = pgTable( export const pushSubscriptions = pgTable( "push_subscriptions", { - subscriptionId: integer("subscription_id") - .primaryKey() - .generatedAlwaysAsIdentity({ - name: "push_subscriptions_subscription_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + subscriptionId: integer("subscription_id").primaryKey().generatedAlwaysAsIdentity({ + name: "push_subscriptions_subscription_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), userId: text("user_id").notNull(), endpoint: text().notNull(), p256Dh: text().notNull(), @@ -583,10 +506,7 @@ export const pushSubscriptions = pgTable( isActive: boolean("is_active").default(true).notNull(), }, () => [ - check( - "push_subscriptions_subscription_id_not_null", - sql`NOT NULL subscription_id`, - ), + check("push_subscriptions_subscription_id_not_null", sql`NOT NULL subscription_id`), check("push_subscriptions_user_id_not_null", sql`NOT NULL user_id`), check("push_subscriptions_endpoint_not_null", sql`NOT NULL endpoint`), check("push_subscriptions_p256dh_not_null", sql`NOT NULL p256dh`), @@ -599,16 +519,14 @@ export const pushSubscriptions = pgTable( export const reportAttachments = pgTable( "report_attachments", { - attachmentId: integer("attachment_id") - .primaryKey() - .generatedAlwaysAsIdentity({ - name: "report_attachments_attachment_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + attachmentId: integer("attachment_id").primaryKey().generatedAlwaysAsIdentity({ + name: "report_attachments_attachment_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), reportId: uuid("report_id").notNull(), fileId: uuid("file_id").notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) @@ -616,10 +534,7 @@ export const reportAttachments = pgTable( .notNull(), }, () => [ - check( - "report_attachments_attachment_id_not_null", - sql`NOT NULL attachment_id`, - ), + check("report_attachments_attachment_id_not_null", sql`NOT NULL attachment_id`), check("report_attachments_report_id_not_null", sql`NOT NULL report_id`), check("report_attachments_file_id_not_null", sql`NOT NULL file_id`), check("report_attachments_created_at_not_null", sql`NOT NULL created_at`), @@ -697,9 +612,7 @@ export const session = pgTable( id: text().primaryKey().notNull(), expiresAt: timestamp("expires_at", { mode: "string" }).notNull(), token: text().notNull(), - createdAt: timestamp("created_at", { mode: "string" }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { mode: "string" }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { mode: "string" }).notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), @@ -734,21 +647,11 @@ export const user = pgTable( interests: jsonb().default([]), civilianCareer: text("civilian_career"), linkedin: text(), - signalVisibility: visibilityEnum("signal_visibility") - .default("private") - .notNull(), - emailVisibility: visibilityEnum("email_visibility") - .default("private") - .notNull(), - linkedinVisibility: visibilityEnum("linkedin_visibility") - .default("public") - .notNull(), - createdAt: timestamp("created_at", { mode: "string" }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { mode: "string" }) - .defaultNow() - .notNull(), + signalVisibility: visibilityEnum("signal_visibility").default("private").notNull(), + emailVisibility: visibilityEnum("email_visibility").default("private").notNull(), + linkedinVisibility: visibilityEnum("linkedin_visibility").default("public").notNull(), + createdAt: timestamp("created_at", { mode: "string" }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { mode: "string" }).defaultNow().notNull(), }, (table) => [ unique("user_email_unique").on(table.email), @@ -758,10 +661,7 @@ export const user = pgTable( check("user_email_verified_not_null", sql`NOT NULL email_verified`), check("user_signal_visibility_not_null", sql`NOT NULL signal_visibility`), check("user_email_visibility_not_null", sql`NOT NULL email_visibility`), - check( - "user_linkedin_visibility_not_null", - sql`NOT NULL linkedin_visibility`, - ), + check("user_linkedin_visibility_not_null", sql`NOT NULL linkedin_visibility`), check("user_created_at_not_null", sql`NOT NULL created_at`), check("user_updated_at_not_null", sql`NOT NULL updated_at`), ], @@ -774,12 +674,8 @@ export const verification = pgTable( identifier: text().notNull(), value: text().notNull(), expiresAt: timestamp("expires_at", { mode: "string" }).notNull(), - createdAt: timestamp("created_at", { mode: "string" }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { mode: "string" }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { mode: "string" }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { mode: "string" }).defaultNow().notNull(), }, () => [ check("verification_id_not_null", sql`NOT NULL id`), diff --git a/server/scripts/test-recommendations.ts b/server/scripts/test-recommendations.ts index f277a52c..8d082624 100644 --- a/server/scripts/test-recommendations.ts +++ b/server/scripts/test-recommendations.ts @@ -37,7 +37,7 @@ type MentorInput = { whyInterestedResponses: string[]; careerAdvice: string; preferredMenteeCareerStages: string[]; - preferredMeetingFormat: "in-person" | "virtual" | "hybrid" | "no-preference"; + preferredMeetingFormat: "in-person" | "virtual" | "hybrid"; hoursPerMonthCommitment: number; }; @@ -49,16 +49,12 @@ type MenteeInput = { roleModelInspiration: string; hopeToGainResponses: string[]; mentorQualities: string[]; - preferredMeetingFormat: "in-person" | "virtual" | "hybrid" | "no-preference"; + preferredMeetingFormat: "in-person" | "virtual" | "hybrid"; hoursPerMonthCommitment: number; }; async function ensureUser(input: SeedUserInput) { - const [existing] = await db - .select() - .from(users) - .where(eq(users.email, input.email)) - .limit(1); + const [existing] = await db.select().from(users).where(eq(users.email, input.email)).limit(1); if (existing) { const [updated] = await db @@ -108,11 +104,7 @@ async function ensureUser(input: SeedUserInput) { } async function ensureMentor(userId: string, input: MentorInput) { - const [existing] = await db - .select() - .from(mentors) - .where(eq(mentors.userId, userId)) - .limit(1); + const [existing] = await db.select().from(mentors).where(eq(mentors.userId, userId)).limit(1); if (existing) { const [updated] = await db @@ -138,11 +130,7 @@ async function ensureMentor(userId: string, input: MentorInput) { } async function ensureMentee(userId: string, input: MenteeInput) { - const [existing] = await db - .select() - .from(mentees) - .where(eq(mentees.userId, userId)) - .limit(1); + const [existing] = await db.select().from(mentees).where(eq(mentees.userId, userId)).limit(1); if (existing) { const [updated] = await db @@ -279,11 +267,7 @@ const MENTOR_DATA: Array<{ user: SeedUserInput; mentor: MentorInput }> = [ "Diversity makes our force stronger.", ], careerAdvice: "Your background is your strength, not a limitation.", - preferredMenteeCareerStages: [ - "new-soldiers", - "junior-ncos", - "transitioning", - ], + preferredMenteeCareerStages: ["new-soldiers", "junior-ncos", "transitioning"], preferredMeetingFormat: "hybrid", hoursPerMonthCommitment: 4, }, @@ -368,11 +352,7 @@ const MENTOR_DATA: Array<{ user: SeedUserInput; mentor: MentorInput }> = [ }, mentor: { yearsOfService: 4, - strengths: [ - "work-life-balance", - "civilian-military-integration", - "education", - ], + strengths: ["work-life-balance", "civilian-military-integration", "education"], personalInterests: "yoga, graduate school, startup culture", whyInterestedResponses: [ "Balancing civilian career and Guard service is challenging but rewarding.", @@ -493,8 +473,7 @@ const MENTOR_DATA: Array<{ user: SeedUserInput; mentor: MentorInput }> = [ "First Sergeants shape unit culture.", "I want to develop future senior NCOs.", ], - careerAdvice: - "Take care of your soldiers and they will take care of the mission.", + careerAdvice: "Take care of your soldiers and they will take care of the mission.", preferredMenteeCareerStages: ["junior-ncos", "senior-ncos"], preferredMeetingFormat: "in-person", hoursPerMonthCommitment: 4, @@ -549,11 +528,7 @@ const TEST_MENTEE: { user: SeedUserInput; mentee: MenteeInput } = { "Work-life balance strategies", "Networking within the Guard", ], - mentorQualities: [ - "strong-communicator", - "experienced-leader", - "encouraging-and-empathetic", - ], + mentorQualities: ["strong-communicator", "experienced-leader", "encouraging-and-empathetic"], preferredMeetingFormat: "hybrid", hoursPerMonthCommitment: 3, }, @@ -605,15 +580,9 @@ async function clearTestData() { // Delete in order due to foreign key constraints for (const userId of testUserIds) { - await db - .delete(mentorRecommendations) - .where(eq(mentorRecommendations.userId, userId)); - await db - .delete(mentorshipMatches) - .where(eq(mentorshipMatches.requestorUserId, userId)); - await db - .delete(mentorshipMatches) - .where(eq(mentorshipMatches.mentorUserId, userId)); + await db.delete(mentorRecommendations).where(eq(mentorRecommendations.userId, userId)); + await db.delete(mentorshipMatches).where(eq(mentorshipMatches.requestorUserId, userId)); + await db.delete(mentorshipMatches).where(eq(mentorshipMatches.mentorUserId, userId)); await db.delete(mentees).where(eq(mentees.userId, userId)); await db.delete(mentors).where(eq(mentors.userId, userId)); await db.delete(account).where(eq(account.userId, userId)); @@ -761,9 +730,7 @@ async function main() { console.log(` Mentee 1: ${TEST_MENTEE.user.email} (leadership-focused)`); console.log(` Mentee 2: ${TEST_MENTEE_TECH.user.email} (tech-focused)`); console.log(`\nPassword for all accounts: ${DEFAULT_PASSWORD}`); - console.log( - "\nLog in as a mentee to see their personalized mentor recommendations.", - ); + console.log("\nLog in as a mentee to see their personalized mentor recommendations."); console.log("The algorithm should rank mentors based on:"); console.log(" - Vector similarity (embeddings match)"); console.log(" - Meeting format compatibility"); diff --git a/server/src/data/db/recommendation-queries.ts b/server/src/data/db/recommendation-queries.ts index fca25ee9..14fddf2c 100644 --- a/server/src/data/db/recommendation-queries.ts +++ b/server/src/data/db/recommendation-queries.ts @@ -140,16 +140,13 @@ scored_mentors AS ( ) * ${sql.raw(String(VECTOR_SIMILARITY_WEIGHT))} AS vector_score, -- Meeting format compatibility (weight: 0.15) - -- Full match: 1.0, partial match: 0.6, no preference involved: 0.8, no match: 0.3 + -- Full match: 1.0, partial match: 0.8, no match: 0.3 CASE -- Exact match WHEN m.preferred_meeting_format = md.mentee_meeting_format THEN 1.0 - -- Either has no preference - WHEN m.preferred_meeting_format = 'no-preference' OR md.mentee_meeting_format = 'no-preference' THEN 0.9 - WHEN m.preferred_meeting_format IS NULL OR md.mentee_meeting_format IS NULL THEN 0.8 - -- Hybrid matches with in-person or virtual - WHEN m.preferred_meeting_format = 'hybrid' OR md.mentee_meeting_format = 'hybrid' THEN 0.7 - -- No match but still consider (diffusion) + -- Either has hybrid + WHEN m.preferred_meeting_format = 'hybrid' AND md.mentee_meeting_format != 'hybrid' THEN 0.8 + WHEN m.preferred_meeting_format != 'hybrid' AND md.mentee_meeting_format = 'hybrid' THEN 0.8 ELSE 0.3 END * ${sql.raw(String(MEETING_FORMAT_WEIGHT))} AS format_score, diff --git a/server/src/data/db/schema.ts b/server/src/data/db/schema.ts index c75c72c8..c42241bf 100644 --- a/server/src/data/db/schema.ts +++ b/server/src/data/db/schema.ts @@ -16,23 +16,11 @@ import { import type { RoleKey } from "../../data/roles.js"; // Enums -export const permissionEnum = pgEnum("permission_enum", [ - "read", - "write", - "both", -]); +export const permissionEnum = pgEnum("permission_enum", ["read", "write", "both"]); -export const mentorStatusEnum = pgEnum("mentor_status_enum", [ - "requested", - "approved", - "active", -]); +export const mentorStatusEnum = pgEnum("mentor_status_enum", ["requested", "approved", "active"]); -export const menteeStatusEnum = pgEnum("mentee_status_enum", [ - "active", - "inactive", - "matched", -]); +export const menteeStatusEnum = pgEnum("mentee_status_enum", ["active", "inactive", "matched"]); export const matchStatusEnum = pgEnum("match_status_enum", [ "pending", // Mentee requested, waiting for mentor acceptance @@ -58,28 +46,18 @@ export const visibilityEnum = pgEnum("visibility_enum", ["private", "public"]); export type RoleNamespace = (typeof roleNamespaceEnum.enumValues)[number]; -export const channelPostPermissionEnum = pgEnum( - "channel_post_permission_enum", - ["admin", "everyone", "custom"], -); +export const channelPostPermissionEnum = pgEnum("channel_post_permission_enum", [ + "admin", + "everyone", + "custom", +]); // Mentorship application enums -export const positionTypeEnum = pgEnum("position_type_enum", [ - "active", - "part-time", -]); +export const positionTypeEnum = pgEnum("position_type_enum", ["active", "part-time"]); -export const serviceTypeEnum = pgEnum("service_type_enum", [ - "enlisted", - "officer", -]); +export const serviceTypeEnum = pgEnum("service_type_enum", ["enlisted", "officer"]); -export const meetingFormatEnum = pgEnum("meeting_format_enum", [ - "in-person", - "virtual", - "hybrid", - "no-preference", -]); +export const meetingFormatEnum = pgEnum("meeting_format_enum", ["in-person", "virtual", "hybrid"]); export const careerStageEnum = pgEnum("career_stage_enum", [ "new-soldiers", @@ -91,10 +69,7 @@ export const careerStageEnum = pgEnum("career_stage_enum", [ "no-preference", ]); -export const mentorshipUserTypeEnum = pgEnum("mentorship_user_type_enum", [ - "mentor", - "mentee", -]); +export const mentorshipUserTypeEnum = pgEnum("mentorship_user_type_enum", ["mentor", "mentee"]); // pgvector support - custom type for vector columns const vector = customType<{ data: number[]; driverData: string }>({ @@ -132,15 +107,9 @@ export const users = pgTable( civilianCareer: text("civilian_career"), linkedin: text("linkedin"), - signalVisibility: visibilityEnum("signal_visibility") - .notNull() - .default("private"), - emailVisibility: visibilityEnum("email_visibility") - .notNull() - .default("private"), - linkedinVisibility: visibilityEnum("linkedin_visibility") - .notNull() - .default("public"), + signalVisibility: visibilityEnum("signal_visibility").notNull().default("private"), + emailVisibility: visibilityEnum("email_visibility").notNull().default("private"), + linkedinVisibility: visibilityEnum("linkedin_visibility").notNull().default("public"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") @@ -208,9 +177,7 @@ export const channels = pgTable( channelId: integer("channel_id").primaryKey().generatedAlwaysAsIdentity(), name: text("name").notNull(), description: text("description"), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), metadata: jsonb("metadata"), postPermissionLevel: channelPostPermissionEnum("post_permission_level") .notNull() @@ -253,12 +220,8 @@ export const roles = pgTable( }), metadata: jsonb("metadata"), description: text("description"), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ sql`CONSTRAINT ck_roles_channel_namespace CHECK (${table.namespace.name} <> 'channel' OR ${table.channelId.name} IS NOT NULL)`, @@ -287,9 +250,7 @@ export const userRoles = pgTable( roleId: integer("role_id") .references(() => roles.roleId, { onDelete: "cascade" }) .notNull(), - assignedAt: timestamp("assigned_at", { withTimezone: true }) - .defaultNow() - .notNull(), + assignedAt: timestamp("assigned_at", { withTimezone: true }).defaultNow().notNull(), assignedBy: text("assigned_by").references(() => users.id, { onDelete: "set null", }), @@ -310,27 +271,18 @@ export const userRoles = pgTable( export const channelSubscriptions = pgTable( "channel_subscriptions", { - subscriptionId: integer("subscription_id") - .primaryKey() - .generatedAlwaysAsIdentity(), + subscriptionId: integer("subscription_id").primaryKey().generatedAlwaysAsIdentity(), userId: text("user_id") .references(() => users.id, { onDelete: "cascade" }) .notNull(), channelId: integer("channel_id") .references(() => channels.channelId, { onDelete: "cascade" }) .notNull(), - notificationsEnabled: boolean("notifications_enabled") - .default(true) - .notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), + notificationsEnabled: boolean("notifications_enabled").default(true).notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ - uniqueIndex("ux_channel_subscriptions_user_channel").on( - table.userId, - table.channelId, - ), + uniqueIndex("ux_channel_subscriptions_user_channel").on(table.userId, table.channelId), index("ix_channel_subscriptions_user_id").on(table.userId), index("ix_channel_subscriptions_channel_id").on(table.channelId), ], @@ -349,9 +301,7 @@ export const messages = pgTable( }), message: text("message"), attachmentUrl: text("attachment_url"), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ index("ix_messages_channel_id").on(table.channelId), @@ -362,26 +312,19 @@ export const messages = pgTable( export const messageAttachments = pgTable( "message_attachments", { - attachmentId: integer("attachment_id") - .primaryKey() - .generatedAlwaysAsIdentity(), + attachmentId: integer("attachment_id").primaryKey().generatedAlwaysAsIdentity(), messageId: integer("message_id") .references(() => messages.messageId, { onDelete: "cascade" }) .notNull(), fileId: uuid("file_id") .references(() => files.fileId, { onDelete: "cascade" }) .notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ index("ix_message_attachments_message_id").on(table.messageId), index("ix_message_attachments_file_id").on(table.fileId), - uniqueIndex("ux_message_attachments_message_file").on( - table.messageId, - table.fileId, - ), + uniqueIndex("ux_message_attachments_message_file").on(table.messageId, table.fileId), ], ); @@ -396,16 +339,10 @@ export const messageReactions = pgTable( .references(() => users.id, { onDelete: "cascade" }) .notNull(), emoji: text("emoji").notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ - uniqueIndex("ux_message_reactions_user").on( - table.messageId, - table.userId, - table.emoji, - ), + uniqueIndex("ux_message_reactions_user").on(table.messageId, table.userId, table.emoji), index("ix_message_reactions_message_id").on(table.messageId), index("ix_message_reactions_user_id").on(table.userId), ], @@ -424,9 +361,7 @@ export const mentors = pgTable( .notNull(), mentorshipPreferences: text("mentorship_preferences"), yearsOfService: integer("years_of_service"), - eligibilityData: jsonb("eligibility_data").$type< - Record | null | undefined - >(), + eligibilityData: jsonb("eligibility_data").$type | null | undefined>(), status: mentorStatusEnum("status").default("requested").notNull(), // New application fields resumeFileId: uuid("resume_file_id").references(() => files.fileId, { @@ -436,17 +371,11 @@ export const mentors = pgTable( personalInterests: text("personal_interests"), whyInterestedResponses: jsonb("why_interested_responses").$type(), // Ordered responses careerAdvice: text("career_advice"), // Text response to advice question - preferredMenteeCareerStages: jsonb("preferred_mentee_career_stages").$type< - string[] - >(), // Array of career stage enum values + preferredMenteeCareerStages: jsonb("preferred_mentee_career_stages").$type(), // Array of career stage enum values preferredMeetingFormat: meetingFormatEnum("preferred_meeting_format"), hoursPerMonthCommitment: integer("hours_per_month_commitment"), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ // CHECK (years_of_service IS NULL OR years_of_service >= 0) @@ -463,23 +392,15 @@ export const mentors = pgTable( export const mentorRecommendations = pgTable( "mentor_recommendations", { - recommendationId: integer("recommendation_id") - .primaryKey() - .generatedAlwaysAsIdentity(), + recommendationId: integer("recommendation_id").primaryKey().generatedAlwaysAsIdentity(), userId: text("user_id") .references(() => users.id, { onDelete: "cascade" }) .notNull(), - recommendedMentorIds: jsonb("recommended_mentor_ids") - .$type() - .notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), + recommendedMentorIds: jsonb("recommended_mentor_ids").$type().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), expiresAt: timestamp("expires_at", { withTimezone: true }), }, - (table) => [ - uniqueIndex("ux_mentor_recommendations_user_id").on(table.userId), - ], + (table) => [uniqueIndex("ux_mentor_recommendations_user_id").on(table.userId)], ); // MENTORSHIP MATCHES @@ -490,16 +411,11 @@ export const mentorshipMatches = pgTable( requestorUserId: text("requestor_user_id").references(() => users.id), mentorUserId: text("mentor_user_id").references(() => users.id), status: matchStatusEnum("status").default("pending").notNull(), - matchedAt: timestamp("matched_at", { withTimezone: true }) - .defaultNow() - .notNull(), + matchedAt: timestamp("matched_at", { withTimezone: true }).defaultNow().notNull(), message: text("message"), // Optional personalized message from mentee to mentor }, (table) => [ - uniqueIndex("ux_mentorship_matches_pair").on( - table.requestorUserId, - table.mentorUserId, - ), + uniqueIndex("ux_mentorship_matches_pair").on(table.requestorUserId, table.mentorUserId), index("ix_mentorship_matches_requestor_user_id").on(table.requestorUserId), index("ix_mentorship_matches_mentor_user_id").on(table.mentorUserId), index("ix_mentorship_matches_status").on(table.status), @@ -510,9 +426,7 @@ export const mentorshipMatches = pgTable( export const pushSubscriptions = pgTable( "push_subscriptions", { - subscriptionId: integer("subscription_id") - .primaryKey() - .generatedAlwaysAsIdentity(), + subscriptionId: integer("subscription_id").primaryKey().generatedAlwaysAsIdentity(), userId: text("user_id") .references(() => users.id, { onDelete: "cascade" }) .notNull(), @@ -521,9 +435,7 @@ export const pushSubscriptions = pgTable( auth: text("auth").notNull(), keys: jsonb("keys"), topics: jsonb("topics"), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), isActive: boolean("is_active").default(true).notNull(), }, (table) => [ @@ -554,12 +466,8 @@ export const mentees = pgTable( mentorQualities: jsonb("mentor_qualities").$type(), // What qualities look for preferredMeetingFormat: meetingFormatEnum("preferred_meeting_format"), hoursPerMonthCommitment: integer("hours_per_month_commitment"), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ sql`CONSTRAINT ck_mentees_hours_per_month CHECK (${table.hoursPerMonthCommitment.name} IS NULL OR ${table.hoursPerMonthCommitment.name} > 0)`, @@ -585,12 +493,8 @@ export const messageBlasts = pgTable( .notNull() .default(sql`NOW() + INTERVAL '24 hours'`), status: messageBlastStatusEnum("status").default("draft").notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ index("ix_message_blasts_sender_id").on(table.senderId), @@ -600,11 +504,7 @@ export const messageBlasts = pgTable( ); // Reports -export const reportStatusEnum = pgEnum("report_status_enum", [ - "Pending", - "Assigned", - "Resolved", -]); +export const reportStatusEnum = pgEnum("report_status_enum", ["Pending", "Assigned", "Resolved"]); export const reportCategoryEnum = pgEnum("report_category_enum", [ "Communication", @@ -629,36 +529,25 @@ export const reports = pgTable("reports", { assignedBy: text("assigned_by").references(() => users.id, { onDelete: "set null", }), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), resolvedAt: timestamp("resolved", { withTimezone: true }), }); export const reportAttachments = pgTable( "report_attachments", { - attachmentId: integer("attachment_id") - .primaryKey() - .generatedAlwaysAsIdentity(), + attachmentId: integer("attachment_id").primaryKey().generatedAlwaysAsIdentity(), reportId: uuid("report_id") .references(() => reports.reportId, { onDelete: "cascade" }) .notNull(), fileId: uuid("file_id") .references(() => files.fileId, { onDelete: "cascade" }) .notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ - uniqueIndex("ux_report_attachments_report_file").on( - table.reportId, - table.fileId, - ), + uniqueIndex("ux_report_attachments_report_file").on(table.reportId, table.fileId), index("ix_report_attachments_report_id").on(table.reportId), ], ); @@ -667,9 +556,7 @@ export const reportAttachments = pgTable( export const mentorshipEmbeddings = pgTable( "mentorship_embeddings", { - embeddingId: integer("embedding_id") - .primaryKey() - .generatedAlwaysAsIdentity(), + embeddingId: integer("embedding_id").primaryKey().generatedAlwaysAsIdentity(), userId: text("user_id") .references(() => users.id, { onDelete: "cascade" }) .notNull(), @@ -679,18 +566,11 @@ export const mentorshipEmbeddings = pgTable( hopeToGainEmbedding: vector("hope_to_gain_embedding"), // For mentees // Combined embedding for overall profile matching profileEmbedding: vector("profile_embedding"), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ - uniqueIndex("ux_mentorship_embeddings_user_type").on( - table.userId, - table.userType, - ), + uniqueIndex("ux_mentorship_embeddings_user_type").on(table.userId, table.userType), index("ix_mentorship_embeddings_user_id").on(table.userId), index("ix_mentorship_embeddings_user_type").on(table.userType), sql`CREATE INDEX IF NOT EXISTS ix_mentorship_embeddings_profile_embedding ON mentorship_embeddings USING ivfflat (profile_embedding vector_cosine_ops) WITH (lists = 100)`, @@ -707,9 +587,7 @@ export const inviteCodes = pgTable( createdBy: text("created_by") .references(() => users.id, { onDelete: "set null" }) .notNull(), - createdAt: timestamp("created_at", { withTimezone: true }) - .defaultNow() - .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), usedBy: text("used_by").references(() => users.id, { onDelete: "set null", diff --git a/server/src/data/repository/mentee-repo.ts b/server/src/data/repository/mentee-repo.ts index 45f5a156..1a15103f 100644 --- a/server/src/data/repository/mentee-repo.ts +++ b/server/src/data/repository/mentee-repo.ts @@ -1,10 +1,5 @@ import { and, eq, inArray } from "drizzle-orm"; -import { - mentees, - mentors, - mentorshipMatches, - users, -} from "../../data/db/schema.js"; +import { mentees, mentors, mentorshipMatches, users } from "../../data/db/schema.js"; import { db } from "../../data/db/sql.js"; import { ConflictError, NotFoundError } from "../../types/errors.js"; import type { @@ -46,11 +41,7 @@ export class MenteeRepository { roleModelInspiration?: string, hopeToGainResponses?: string[], mentorQualities?: string[], - preferredMeetingFormat?: - | "in-person" - | "virtual" - | "hybrid" - | "no-preference", + preferredMeetingFormat?: "in-person" | "virtual" | "hybrid", hoursPerMonthCommitment?: number, ): Promise { // Check if mentee already exists for this user @@ -220,11 +211,7 @@ export class MenteeRepository { roleModelInspiration?: string, hopeToGainResponses?: string[], mentorQualities?: string[], - preferredMeetingFormat?: - | "in-person" - | "virtual" - | "hybrid" - | "no-preference", + preferredMeetingFormat?: "in-person" | "virtual" | "hybrid", hoursPerMonthCommitment?: number, ): Promise { const updateData: Partial = { @@ -232,20 +219,14 @@ export class MenteeRepository { }; if (learningGoals !== undefined) updateData.learningGoals = learningGoals; - if (experienceLevel !== undefined) - updateData.experienceLevel = experienceLevel; - if (preferredMentorType !== undefined) - updateData.preferredMentorType = preferredMentorType; + if (experienceLevel !== undefined) updateData.experienceLevel = experienceLevel; + if (preferredMentorType !== undefined) updateData.preferredMentorType = preferredMentorType; if (status !== undefined) updateData.status = status; if (resumeFileId !== undefined) updateData.resumeFileId = resumeFileId; - if (personalInterests !== undefined) - updateData.personalInterests = personalInterests; - if (roleModelInspiration !== undefined) - updateData.roleModelInspiration = roleModelInspiration; - if (hopeToGainResponses !== undefined) - updateData.hopeToGainResponses = hopeToGainResponses; - if (mentorQualities !== undefined) - updateData.mentorQualities = mentorQualities; + if (personalInterests !== undefined) updateData.personalInterests = personalInterests; + if (roleModelInspiration !== undefined) updateData.roleModelInspiration = roleModelInspiration; + if (hopeToGainResponses !== undefined) updateData.hopeToGainResponses = hopeToGainResponses; + if (mentorQualities !== undefined) updateData.mentorQualities = mentorQualities; if (preferredMeetingFormat !== undefined) updateData.preferredMeetingFormat = preferredMeetingFormat; if (hoursPerMonthCommitment !== undefined) @@ -301,9 +282,7 @@ export class MenteeRepository { * @param status Mentee status * @returns Array of mentee profiles */ - async getMenteesByStatus( - status: "active" | "inactive" | "matched", - ): Promise { + async getMenteesByStatus(status: "active" | "inactive" | "matched"): Promise { return await db .select({ menteeId: mentees.menteeId, diff --git a/server/src/data/repository/mentor-repo.ts b/server/src/data/repository/mentor-repo.ts index a9869ae1..308ed93f 100644 --- a/server/src/data/repository/mentor-repo.ts +++ b/server/src/data/repository/mentor-repo.ts @@ -1,17 +1,9 @@ import { and, eq, inArray } from "drizzle-orm"; -import { - mentees, - mentors, - mentorshipMatches, - users, -} from "../../data/db/schema.js"; +import { mentees, mentors, mentorshipMatches, users } from "../../data/db/schema.js"; import { db } from "../../data/db/sql.js"; import { ConflictError, NotFoundError } from "../../types/errors.js"; import type { GetMenteeOutput } from "../../types/mentee-types.js"; -import type { - CreateMentorOutput, - GetMentorOutput, -} from "../../types/mentor-types.js"; +import type { CreateMentorOutput, GetMentorOutput } from "../../types/mentor-types.js"; import type { PendingMenteeRequest } from "../../types/mentorship-types.js"; /** @@ -48,11 +40,7 @@ export class MentorRepository { whyInterestedResponses?: string[], careerAdvice?: string, preferredMenteeCareerStages?: string[], - preferredMeetingFormat?: - | "in-person" - | "virtual" - | "hybrid" - | "no-preference", + preferredMeetingFormat?: "in-person" | "virtual" | "hybrid", hoursPerMonthCommitment?: number, ): Promise { // Check if mentor already exists for this user @@ -205,9 +193,7 @@ export class MentorRepository { /** * Get pending mentee requests for a mentor */ - async getPendingMenteeRequests( - userId: string, - ): Promise { + async getPendingMenteeRequests(userId: string): Promise { const pendingRows = await db .select({ matchId: mentorshipMatches.matchId, @@ -242,10 +228,7 @@ export class MentorRepository { .innerJoin(mentees, eq(mentees.userId, mentorshipMatches.requestorUserId)) .innerJoin(users, eq(users.id, mentees.userId)) .where( - and( - eq(mentorshipMatches.mentorUserId, userId), - eq(mentorshipMatches.status, "pending"), - ), + and(eq(mentorshipMatches.mentorUserId, userId), eq(mentorshipMatches.status, "pending")), ); return pendingRows.map((row) => ({ @@ -363,10 +346,7 @@ export class MentorRepository { .innerJoin(mentees, eq(mentees.userId, mentorshipMatches.requestorUserId)) .innerJoin(users, eq(users.id, mentees.userId)) .where( - and( - eq(mentorshipMatches.mentorUserId, userId), - eq(mentorshipMatches.status, "accepted"), - ), + and(eq(mentorshipMatches.mentorUserId, userId), eq(mentorshipMatches.status, "accepted")), ); return { mentor, activeMentees }; diff --git a/server/src/types/mentee-types.ts b/server/src/types/mentee-types.ts index 745b4f1e..ecb1872d 100644 --- a/server/src/types/mentee-types.ts +++ b/server/src/types/mentee-types.ts @@ -12,10 +12,7 @@ export const menteeSchema = z.object({ roleModelInspiration: z.string().nullable().optional(), hopeToGainResponses: z.array(z.string()).nullable().optional(), mentorQualities: z.array(z.string()).nullable().optional(), - preferredMeetingFormat: z - .enum(["in-person", "virtual", "hybrid", "no-preference"]) - .nullable() - .optional(), + preferredMeetingFormat: z.enum(["in-person", "virtual", "hybrid"]).nullable().optional(), hoursPerMonthCommitment: z.number().int().positive().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), @@ -28,18 +25,13 @@ export const createMenteeInputSchema = z.object({ learningGoals: z.string().optional(), experienceLevel: z.string().optional(), preferredMentorType: z.string().optional(), - status: z - .enum(["active", "inactive", "matched"]) - .optional() - .default("active"), + status: z.enum(["active", "inactive", "matched"]).optional().default("active"), resumeFileId: z.string().uuid().optional(), personalInterests: z.string().optional(), roleModelInspiration: z.string().optional(), hopeToGainResponses: z.array(z.string()).optional(), mentorQualities: z.array(z.string()).optional(), - preferredMeetingFormat: z - .enum(["in-person", "virtual", "hybrid", "no-preference"]) - .optional(), + preferredMeetingFormat: z.enum(["in-person", "virtual", "hybrid"]).optional(), hoursPerMonthCommitment: z.number().int().positive().optional(), }); @@ -76,12 +68,7 @@ export type CreateMenteeOutput = { roleModelInspiration?: string | null; hopeToGainResponses?: string[] | null; mentorQualities?: string[] | null; - preferredMeetingFormat?: - | "in-person" - | "virtual" - | "hybrid" - | "no-preference" - | null; + preferredMeetingFormat?: "in-person" | "virtual" | "hybrid" | null; hoursPerMonthCommitment?: number | null; createdAt: string | Date; updatedAt: string | Date; @@ -99,9 +86,7 @@ export const getMenteeOutputSchema = z.object({ roleModelInspiration: z.string().nullish(), hopeToGainResponses: z.array(z.string()).nullish(), mentorQualities: z.array(z.string()).nullish(), - preferredMeetingFormat: z - .enum(["in-person", "virtual", "hybrid", "no-preference"]) - .nullish(), + preferredMeetingFormat: z.enum(["in-person", "virtual", "hybrid"]).nullish(), hoursPerMonthCommitment: z.number().nullish(), createdAt: z.union([z.string(), z.date()]), updatedAt: z.union([z.string(), z.date()]), @@ -135,12 +120,7 @@ export type UpdateMenteeOutput = { roleModelInspiration?: string | null; hopeToGainResponses?: string[] | null; mentorQualities?: string[] | null; - preferredMeetingFormat?: - | "in-person" - | "virtual" - | "hybrid" - | "no-preference" - | null; + preferredMeetingFormat?: "in-person" | "virtual" | "hybrid" | null; hoursPerMonthCommitment?: number | null; createdAt: string | Date; updatedAt: string | Date; diff --git a/server/src/types/mentor-types.ts b/server/src/types/mentor-types.ts index 7258ab48..454af284 100644 --- a/server/src/types/mentor-types.ts +++ b/server/src/types/mentor-types.ts @@ -26,10 +26,7 @@ export const mentorSchema = z.object({ ) .nullable() .optional(), - preferredMeetingFormat: z - .enum(["in-person", "virtual", "hybrid", "no-preference"]) - .nullable() - .optional(), + preferredMeetingFormat: z.enum(["in-person", "virtual", "hybrid"]).nullable().optional(), hoursPerMonthCommitment: z.number().int().positive().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), @@ -42,10 +39,7 @@ export const createMentorInputSchema = z.object({ mentorshipPreferences: z.string().optional(), yearsOfService: z.number().int().nonnegative().optional(), eligibilityData: z.record(z.string(), z.unknown()).nullish(), - status: z - .enum(["requested", "approved", "active"]) - .optional() - .default("requested"), + status: z.enum(["requested", "approved", "active"]).optional().default("requested"), resumeFileId: z.string().uuid().optional(), strengths: z.array(z.string()).max(5).optional().default([]), personalInterests: z.string().optional(), @@ -64,9 +58,7 @@ export const createMentorInputSchema = z.object({ ]), ) .optional(), - preferredMeetingFormat: z - .enum(["in-person", "virtual", "hybrid", "no-preference"]) - .optional(), + preferredMeetingFormat: z.enum(["in-person", "virtual", "hybrid"]).optional(), hoursPerMonthCommitment: z.number().int().positive().optional(), }); @@ -85,9 +77,7 @@ export const createMentorOutputSchema = z.object({ whyInterestedResponses: z.array(z.string()).nullish(), careerAdvice: z.string().nullish(), preferredMenteeCareerStages: z.array(z.string()).nullish(), - preferredMeetingFormat: z - .enum(["in-person", "virtual", "hybrid", "no-preference"]) - .nullish(), + preferredMeetingFormat: z.enum(["in-person", "virtual", "hybrid"]).nullish(), hoursPerMonthCommitment: z.number().nullish(), createdAt: z.date(), updatedAt: z.date(), @@ -108,9 +98,7 @@ export const getMentorOutputSchema = z.object({ whyInterestedResponses: z.array(z.string()).nullish(), careerAdvice: z.string().nullish(), preferredMenteeCareerStages: z.array(z.string()).nullish(), - preferredMeetingFormat: z - .enum(["in-person", "virtual", "hybrid", "no-preference"]) - .nullish(), + preferredMeetingFormat: z.enum(["in-person", "virtual", "hybrid"]).nullish(), hoursPerMonthCommitment: z.number().nullish(), createdAt: z.date(), updatedAt: z.date(), diff --git a/web/package-lock.json b/web/package-lock.json index 1acaaacb..4efee75d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -28,6 +28,7 @@ "lucide-react": "^0.545.0", "next": "^15.5.12", "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", "react": "^19.2.1", "react-dom": "^19.2.1", "react-dropzone": "^14.3.8", @@ -978,6 +979,88 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -1001,6 +1084,86 @@ } } }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", @@ -1087,6 +1250,34 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", @@ -1217,7 +1408,506 @@ "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -1234,30 +1924,44 @@ } } }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", "license": "MIT", "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", "license": "MIT", "dependencies": { + "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", @@ -1269,11 +1973,13 @@ "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, @@ -1292,22 +1998,13 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", @@ -1324,14 +2021,23 @@ } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", "license": "MIT", "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1348,14 +2054,36 @@ } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", "license": "MIT", "dependencies": { + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1372,13 +2100,20 @@ } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -1395,20 +2130,48 @@ } } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -1426,33 +2189,19 @@ } } }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -1469,37 +2218,53 @@ } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -1586,6 +2351,24 @@ } } }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", @@ -2940,6 +3723,83 @@ "react-is": "^16.13.1" } }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/react": { "version": "19.2.1", "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", diff --git a/web/package.json b/web/package.json index 9217eee4..de9cd7dd 100644 --- a/web/package.json +++ b/web/package.json @@ -31,6 +31,7 @@ "lucide-react": "^0.545.0", "next": "^15.5.12", "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", "react": "^19.2.1", "react-dom": "^19.2.1", "react-dropzone": "^14.3.8", diff --git a/web/src/app/mentorship/apply/mentee/page.tsx b/web/src/app/mentorship/apply/mentee/page.tsx index ae4c8fa0..6663f013 100644 --- a/web/src/app/mentorship/apply/mentee/page.tsx +++ b/web/src/app/mentorship/apply/mentee/page.tsx @@ -10,11 +10,7 @@ import { DragReorderFrame } from "@/components/drag-and-drop"; import { icons } from "@/components/icons"; import { MultiSelect, type MultiSelectOption } from "@/components/multi-select"; import { TextInput } from "@/components/text-input"; -import { - Dropzone, - DropzoneContent, - DropzoneEmptyState, -} from "@/components/ui/shadcn-io/dropzone"; +import { Dropzone, DropzoneContent, DropzoneEmptyState } from "@/components/ui/shadcn-io/dropzone"; import { authClient } from "@/lib/auth-client"; import { useTRPC, useTRPCClient } from "@/lib/trpc"; @@ -57,7 +53,6 @@ const mentorQualitiesOptions: MultiSelectOption[] = [ const mentorMeetingFormat: MultiSelectOption[] = [ { label: "In-person", value: "in-person" }, { label: "Online", value: "online" }, - { label: "No preference", value: "no-preference" }, ]; export default function MentorshipApplyMenteePage() { @@ -69,10 +64,7 @@ export default function MentorshipApplyMenteePage() { const { data: sessionData } = authClient.useSession(); const userId = sessionData?.user?.id ?? null; const backHref = useMemo( - () => - searchParams.get("from") === "dashboard" - ? "/mentorship/dashboard" - : "/mentorship", + () => (searchParams.get("from") === "dashboard" ? "/mentorship/dashboard" : "/mentorship"), [searchParams], ); const BackIcon = icons.arrowLeft; @@ -80,9 +72,7 @@ export default function MentorshipApplyMenteePage() { const [resume, setResume] = useState(null); const [selectedQualities, setSelectedQualities] = useState([]); const [selectedInterests, setSelectedInterests] = useState([]); - const [selectedMeetingFormats, setSelectedMeetingFormats] = useState< - string[] - >([]); + const [selectedMeetingFormats, setSelectedMeetingFormats] = useState([]); const [hopeToGainOrder, setHopeToGainOrder] = useState([]); const [multiLineText, setMultiLineText] = useState(""); const [desiredMentorHours, setDesiredMentorHours] = useState(""); @@ -92,9 +82,8 @@ export default function MentorshipApplyMenteePage() { // Aligned with server `appRouter.mentorship.createMentee` const mentorshipQueryKey = trpc.mentorship.getMentorshipData.queryKey(); const createMentee = useMutation({ - mutationFn: async ( - input: Parameters[0], - ) => trpcClient.mentorship.createMentee.mutate(input), + mutationFn: async (input: Parameters[0]) => + trpcClient.mentorship.createMentee.mutate(input), onSuccess: () => { queryClient.invalidateQueries({ queryKey: mentorshipQueryKey }); }, @@ -139,10 +128,7 @@ export default function MentorshipApplyMenteePage() { fileId: presign.fileId, }); } catch (error) { - const message = - error instanceof Error - ? error.message - : "We couldn't upload that file."; + const message = error instanceof Error ? error.message : "We couldn't upload that file."; setResume({ file, status: "error", @@ -190,15 +176,11 @@ export default function MentorshipApplyMenteePage() { try { // Map meeting formats to backend enum const preferredMeetingFormat = - selectedMeetingFormats.length > 0 - ? ((selectedMeetingFormats[0] === "online" - ? "virtual" - : selectedMeetingFormats[0]) as - | "in-person" - | "virtual" - | "hybrid" - | "no-preference") - : undefined; + selectedMeetingFormats.includes("in-person") && selectedMeetingFormats.includes("online") + ? "hybrid" + : selectedMeetingFormats.includes("online") + ? "virtual" + : "in-person"; const hoursPerMonthCommitment = (() => { if (!desiredMentorHours) return undefined; @@ -209,15 +191,10 @@ export default function MentorshipApplyMenteePage() { await createMentee.mutateAsync({ userId, resumeFileId: resume?.status === "uploaded" ? resume.fileId : undefined, - personalInterests: - selectedInterests.length > 0 - ? selectedInterests.join(", ") - : undefined, + personalInterests: selectedInterests.length > 0 ? selectedInterests.join(", ") : undefined, roleModelInspiration: multiLineText.trim() || undefined, - hopeToGainResponses: - hopeToGainOrder.length > 0 ? hopeToGainOrder : undefined, - mentorQualities: - selectedQualities.length > 0 ? selectedQualities : undefined, + hopeToGainResponses: hopeToGainOrder.length > 0 ? hopeToGainOrder : undefined, + mentorQualities: selectedQualities.length > 0 ? selectedQualities : undefined, preferredMeetingFormat, hoursPerMonthCommitment, }); @@ -267,13 +244,11 @@ export default function MentorshipApplyMenteePage() {

- Thank you for your interest in the mentorship program. Give yourself - enough time to thoughtfully complete this application. Your responses - will help us match you with a mentor who can best support your goals. -

-

- *Required Information + Thank you for your interest in the mentorship program. Give yourself enough time to + thoughtfully complete this application. Your responses will help us match you with a + mentor who can best support your goals.

+

*Required Information

@@ -313,8 +288,7 @@ export default function MentorshipApplyMenteePage() {
- 3. Who has been an important role model or source of inspiration for - you, and why? + 3. Who has been an important role model or source of inspiration for you, and why?
4. What do you hope to get out of the mentorship program?
- Rank the following reasons from most important (1) to least - important (5). + Rank the following reasons from most important (1) to least important (5).
- 6. What meeting formats work best for you?*{" "} + 6. Which meeting formats work for you?*{" "} (Select all that apply)

- 7. How much time would you like to spend with your mentor?* + 7. How much time would you like to spend with your mentor per month?*

- {formError && ( -

{formError}

- )} + {formError &&

{formError}

} - searchParams.get("from") === "dashboard" - ? "/mentorship/dashboard" - : "/mentorship", + () => (searchParams.get("from") === "dashboard" ? "/mentorship/dashboard" : "/mentorship"), [searchParams], ); const BackIcon = icons.arrowLeft; @@ -45,13 +38,8 @@ export default function MentorshipApplyMentorPage() { const [selectedInterests, setSelectedInterests] = useState([]); const [multiLineText, setMultiLineText] = useState(""); const [whyInterestedOrder, setWhyInterestedOrder] = useState([]); - const [selectedCareerStages, setSelectedCareerStages] = useState( - [], - ); - const [selectedMeetingFormats, setSelectedMeetingFormats] = useState< - string[] - >([]); - const [desiredMentorHours, setDesiredMentorHours] = useState(""); + const [selectedCareerStages, setSelectedCareerStages] = useState([]); + const [selectedMeetingFormats, setSelectedMeetingFormats] = useState([]); const [availableMentorHours, setAvailableMentorHours] = useState(""); const [formError, setFormError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -60,9 +48,7 @@ export default function MentorshipApplyMentorPage() { // Aligned with server `appRouter.mentorship.createMentor` const createMentor = useMutation({ mutationFn: async ( - input: Parameters< - (typeof trpcClient.mentorship.createMentor)["mutate"] - >[0], + input: Parameters<(typeof trpcClient.mentorship.createMentor)["mutate"]>[0], ) => trpcClient.mentorship.createMentor.mutate(input), }); @@ -105,10 +91,7 @@ export default function MentorshipApplyMentorPage() { fileId: presign.fileId, }); } catch (error) { - const message = - error instanceof Error - ? error.message - : "We couldn't upload that file."; + const message = error instanceof Error ? error.message : "We couldn't upload that file."; setResume({ file, status: "error", @@ -159,15 +142,11 @@ export default function MentorshipApplyMentorPage() { try { // Map meeting formats to backend enum const preferredMeetingFormat = - selectedMeetingFormats.length > 0 - ? ((selectedMeetingFormats[0] === "online" - ? "virtual" - : selectedMeetingFormats[0]) as - | "in-person" - | "virtual" - | "hybrid" - | "no-preference") - : undefined; + selectedMeetingFormats.includes("in-person") && selectedMeetingFormats.includes("online") + ? "hybrid" + : selectedMeetingFormats.includes("online") + ? "virtual" + : "in-person"; // Map career stages to backend enum (fix transitioning-soldiers -> transitioning) type PreferredMenteeCareerStage = @@ -179,9 +158,7 @@ export default function MentorshipApplyMentorPage() { | "transitioning" | "no-preference"; - const preferredMenteeCareerStages: - | PreferredMenteeCareerStage[] - | undefined = + const preferredMenteeCareerStages: PreferredMenteeCareerStage[] | undefined = selectedCareerStages.length > 0 ? selectedCareerStages.map((stage) => stage === "transitioning-soldiers" @@ -200,12 +177,8 @@ export default function MentorshipApplyMentorPage() { userId, resumeFileId: resume?.status === "uploaded" ? resume.fileId : undefined, strengths: selectedQualities, - personalInterests: - selectedInterests.length > 0 - ? selectedInterests.join(", ") - : undefined, - whyInterestedResponses: - whyInterestedOrder.length > 0 ? whyInterestedOrder : undefined, + personalInterests: selectedInterests.length > 0 ? selectedInterests.join(", ") : undefined, + whyInterestedResponses: whyInterestedOrder.length > 0 ? whyInterestedOrder : undefined, careerAdvice: multiLineText.trim() || undefined, preferredMenteeCareerStages, preferredMeetingFormat, @@ -308,7 +281,6 @@ export default function MentorshipApplyMentorPage() { const mentorMeetingFormat: MultiSelectOption[] = [ { label: "In-person", value: "in-person" }, { label: "Online", value: "online" }, - { label: "No preference", value: "no-preference" }, ]; return ( @@ -327,21 +299,17 @@ export default function MentorshipApplyMentorPage() {

- Thank you for your interest in mentoring. Give yourself 20-25 minutes - to thoughtfully complete this application. Your responses are used to - match you with potential mentees and will be shared with mentees who - are interested in connecting. -

-

- *Required Information + Thank you for your interest in mentoring. Give yourself 20-25 minutes to thoughtfully + complete this application. Your responses are used to match you with potential mentees and + will be shared with mentees who are interested in connecting.

+

*Required Information

- 1. Upload a resume to share your educational and career history with - potential mentees. + 1. Upload a resume to share your educational and career history with potential mentees.

4. What are you interested in becoming a mentor?
- Rank the following reasons from most important (1) to least - important (5). + Rank the following reasons from most important (1) to least important (5).

- 5. What is one piece of advice that you wish you had received - earlier in your career? + 5. What is one piece of advice that you wish you had received earlier in your career?

- 7. What meeting formats do you prefer?*{" "} + 7. Which meeting formats work for you?*{" "} (Select all that apply)

- 8. How much time would you like to spend with your mentor?* -

- -
-
-

- 9. How much time can you commit per month to mentoring?*{" "} + 8. How much time can you commit per month to mentoring?*{" "}

- {formError && ( -

{formError}

- )} + {formError &&

{formError}

} Date: Thu, 26 Feb 2026 14:19:38 -0500 Subject: [PATCH 2/2] format and lint --- server/drizzle/schema.ts | 271 +++++++++++++------ server/scripts/test-recommendations.ts | 55 +++- server/src/data/db/schema.ts | 233 ++++++++++++---- server/src/data/repository/mentee-repo.ts | 29 +- server/src/data/repository/mentor-repo.ts | 26 +- server/src/types/mentee-types.ts | 10 +- server/src/types/mentor-types.ts | 10 +- web/src/app/mentorship/apply/mentee/page.tsx | 74 +++-- web/src/app/mentorship/apply/mentor/page.tsx | 76 ++++-- 9 files changed, 573 insertions(+), 211 deletions(-) diff --git a/server/drizzle/schema.ts b/server/drizzle/schema.ts index 071f733c..703779a6 100644 --- a/server/drizzle/schema.ts +++ b/server/drizzle/schema.ts @@ -23,30 +23,59 @@ export const careerStageEnum = pgEnum("career_stage_enum", [ "transitioning", "no-preference", ]); -export const channelPostPermissionEnum = pgEnum("channel_post_permission_enum", [ - "admin", - "everyone", - "custom", +export const channelPostPermissionEnum = pgEnum( + "channel_post_permission_enum", + ["admin", "everyone", "custom"], +); +export const matchStatusEnum = pgEnum("match_status_enum", [ + "pending", + "accepted", + "declined", +]); +export const meetingFormatEnum = pgEnum("meeting_format_enum", [ + "in-person", + "virtual", + "hybrid", +]); +export const menteeStatusEnum = pgEnum("mentee_status_enum", [ + "active", + "inactive", + "matched", +]); +export const mentorStatusEnum = pgEnum("mentor_status_enum", [ + "requested", + "approved", + "active", +]); +export const mentorshipUserTypeEnum = pgEnum("mentorship_user_type_enum", [ + "mentor", + "mentee", ]); -export const matchStatusEnum = pgEnum("match_status_enum", ["pending", "accepted", "declined"]); -export const meetingFormatEnum = pgEnum("meeting_format_enum", ["in-person", "virtual", "hybrid"]); -export const menteeStatusEnum = pgEnum("mentee_status_enum", ["active", "inactive", "matched"]); -export const mentorStatusEnum = pgEnum("mentor_status_enum", ["requested", "approved", "active"]); -export const mentorshipUserTypeEnum = pgEnum("mentorship_user_type_enum", ["mentor", "mentee"]); export const messageBlastStatusEnum = pgEnum("message_blast_status_enum", [ "draft", "sent", "failed", ]); -export const permissionEnum = pgEnum("permission_enum", ["read", "write", "both"]); -export const positionTypeEnum = pgEnum("position_type_enum", ["active", "part-time"]); +export const permissionEnum = pgEnum("permission_enum", [ + "read", + "write", + "both", +]); +export const positionTypeEnum = pgEnum("position_type_enum", [ + "active", + "part-time", +]); export const reportCategoryEnum = pgEnum("report_category_enum", [ "Communication", "Mentorship", "Training", "Resources", ]); -export const reportStatusEnum = pgEnum("report_status_enum", ["Pending", "Assigned", "Resolved"]); +export const reportStatusEnum = pgEnum("report_status_enum", [ + "Pending", + "Assigned", + "Resolved", +]); export const roleNamespaceEnum = pgEnum("role_namespace_enum", [ "global", "channel", @@ -54,7 +83,10 @@ export const roleNamespaceEnum = pgEnum("role_namespace_enum", [ "broadcast", "reporting", ]); -export const serviceTypeEnum = pgEnum("service_type_enum", ["enlisted", "officer"]); +export const serviceTypeEnum = pgEnum("service_type_enum", [ + "enlisted", + "officer", +]); export const visibilityEnum = pgEnum("visibility_enum", ["private", "public"]); export const account = pgTable( @@ -75,7 +107,9 @@ export const account = pgTable( }), scope: text(), password: text(), - createdAt: timestamp("created_at", { mode: "string" }).defaultNow().notNull(), + createdAt: timestamp("created_at", { mode: "string" }) + .defaultNow() + .notNull(), updatedAt: timestamp("updated_at", { mode: "string" }).notNull(), }, () => [ @@ -91,30 +125,43 @@ export const account = pgTable( export const channelSubscriptions = pgTable( "channel_subscriptions", { - subscriptionId: integer("subscription_id").primaryKey().generatedAlwaysAsIdentity({ - name: "channel_subscriptions_subscription_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + subscriptionId: integer("subscription_id") + .primaryKey() + .generatedAlwaysAsIdentity({ + name: "channel_subscriptions_subscription_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), userId: text("user_id").notNull(), channelId: integer("channel_id").notNull(), - notificationsEnabled: boolean("notifications_enabled").default(true).notNull(), + notificationsEnabled: boolean("notifications_enabled") + .default(true) + .notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) .defaultNow() .notNull(), }, () => [ - check("channel_subscriptions_subscription_id_not_null", sql`NOT NULL subscription_id`), + check( + "channel_subscriptions_subscription_id_not_null", + sql`NOT NULL subscription_id`, + ), check("channel_subscriptions_user_id_not_null", sql`NOT NULL user_id`), - check("channel_subscriptions_channel_id_not_null", sql`NOT NULL channel_id`), + check( + "channel_subscriptions_channel_id_not_null", + sql`NOT NULL channel_id`, + ), check( "channel_subscriptions_notifications_enabled_not_null", sql`NOT NULL notifications_enabled`, ), - check("channel_subscriptions_created_at_not_null", sql`NOT NULL created_at`), + check( + "channel_subscriptions_created_at_not_null", + sql`NOT NULL created_at`, + ), ], ); @@ -143,7 +190,10 @@ export const channels = pgTable( check("channels_channel_id_not_null", sql`NOT NULL channel_id`), check("channels_name_not_null", sql`NOT NULL name`), check("channels_created_at_not_null", sql`NOT NULL created_at`), - check("channels_post_permission_level_not_null", sql`NOT NULL post_permission_level`), + check( + "channels_post_permission_level_not_null", + sql`NOT NULL post_permission_level`, + ), ], ); @@ -240,14 +290,16 @@ export const mentees = pgTable( export const mentorRecommendations = pgTable( "mentor_recommendations", { - recommendationId: integer("recommendation_id").primaryKey().generatedAlwaysAsIdentity({ - name: "mentor_recommendations_recommendation_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + recommendationId: integer("recommendation_id") + .primaryKey() + .generatedAlwaysAsIdentity({ + name: "mentor_recommendations_recommendation_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), userId: text("user_id").notNull(), recommendedMentorIds: jsonb("recommended_mentor_ids").notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) @@ -256,13 +308,19 @@ export const mentorRecommendations = pgTable( expiresAt: timestamp("expires_at", { withTimezone: true, mode: "string" }), }, () => [ - check("mentor_recommendations_recommendation_id_not_null", sql`NOT NULL recommendation_id`), + check( + "mentor_recommendations_recommendation_id_not_null", + sql`NOT NULL recommendation_id`, + ), check("mentor_recommendations_user_id_not_null", sql`NOT NULL user_id`), check( "mentor_recommendations_recommended_mentor_ids_not_null", sql`NOT NULL recommended_mentor_ids`, ), - check("mentor_recommendations_created_at_not_null", sql`NOT NULL created_at`), + check( + "mentor_recommendations_created_at_not_null", + sql`NOT NULL created_at`, + ), ], ); @@ -309,14 +367,16 @@ export const mentors = pgTable( export const mentorshipEmbeddings = pgTable( "mentorship_embeddings", { - embeddingId: integer("embedding_id").primaryKey().generatedAlwaysAsIdentity({ - name: "mentorship_embeddings_embedding_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + embeddingId: integer("embedding_id") + .primaryKey() + .generatedAlwaysAsIdentity({ + name: "mentorship_embeddings_embedding_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), userId: text("user_id").notNull(), userType: mentorshipUserTypeEnum("user_type").notNull(), whyInterestedEmbedding: vector("why_interested_embedding", { @@ -332,11 +392,20 @@ export const mentorshipEmbeddings = pgTable( .notNull(), }, () => [ - check("mentorship_embeddings_embedding_id_not_null", sql`NOT NULL embedding_id`), + check( + "mentorship_embeddings_embedding_id_not_null", + sql`NOT NULL embedding_id`, + ), check("mentorship_embeddings_user_id_not_null", sql`NOT NULL user_id`), check("mentorship_embeddings_user_type_not_null", sql`NOT NULL user_type`), - check("mentorship_embeddings_created_at_not_null", sql`NOT NULL created_at`), - check("mentorship_embeddings_updated_at_not_null", sql`NOT NULL updated_at`), + check( + "mentorship_embeddings_created_at_not_null", + sql`NOT NULL created_at`, + ), + check( + "mentorship_embeddings_updated_at_not_null", + sql`NOT NULL updated_at`, + ), ], ); @@ -369,14 +438,16 @@ export const mentorshipMatches = pgTable( export const messageAttachments = pgTable( "message_attachments", { - attachmentId: integer("attachment_id").primaryKey().generatedAlwaysAsIdentity({ - name: "message_attachments_attachment_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + attachmentId: integer("attachment_id") + .primaryKey() + .generatedAlwaysAsIdentity({ + name: "message_attachments_attachment_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), messageId: integer("message_id").notNull(), fileId: uuid("file_id").notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) @@ -384,7 +455,10 @@ export const messageAttachments = pgTable( .notNull(), }, () => [ - check("message_attachments_attachment_id_not_null", sql`NOT NULL attachment_id`), + check( + "message_attachments_attachment_id_not_null", + sql`NOT NULL attachment_id`, + ), check("message_attachments_message_id_not_null", sql`NOT NULL message_id`), check("message_attachments_file_id_not_null", sql`NOT NULL file_id`), check("message_attachments_created_at_not_null", sql`NOT NULL created_at`), @@ -486,14 +560,16 @@ export const messages = pgTable( export const pushSubscriptions = pgTable( "push_subscriptions", { - subscriptionId: integer("subscription_id").primaryKey().generatedAlwaysAsIdentity({ - name: "push_subscriptions_subscription_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + subscriptionId: integer("subscription_id") + .primaryKey() + .generatedAlwaysAsIdentity({ + name: "push_subscriptions_subscription_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), userId: text("user_id").notNull(), endpoint: text().notNull(), p256Dh: text().notNull(), @@ -506,7 +582,10 @@ export const pushSubscriptions = pgTable( isActive: boolean("is_active").default(true).notNull(), }, () => [ - check("push_subscriptions_subscription_id_not_null", sql`NOT NULL subscription_id`), + check( + "push_subscriptions_subscription_id_not_null", + sql`NOT NULL subscription_id`, + ), check("push_subscriptions_user_id_not_null", sql`NOT NULL user_id`), check("push_subscriptions_endpoint_not_null", sql`NOT NULL endpoint`), check("push_subscriptions_p256dh_not_null", sql`NOT NULL p256dh`), @@ -519,14 +598,16 @@ export const pushSubscriptions = pgTable( export const reportAttachments = pgTable( "report_attachments", { - attachmentId: integer("attachment_id").primaryKey().generatedAlwaysAsIdentity({ - name: "report_attachments_attachment_id_seq", - startWith: 1, - increment: 1, - minValue: 1, - maxValue: 2147483647, - cache: 1, - }), + attachmentId: integer("attachment_id") + .primaryKey() + .generatedAlwaysAsIdentity({ + name: "report_attachments_attachment_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 2147483647, + cache: 1, + }), reportId: uuid("report_id").notNull(), fileId: uuid("file_id").notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }) @@ -534,7 +615,10 @@ export const reportAttachments = pgTable( .notNull(), }, () => [ - check("report_attachments_attachment_id_not_null", sql`NOT NULL attachment_id`), + check( + "report_attachments_attachment_id_not_null", + sql`NOT NULL attachment_id`, + ), check("report_attachments_report_id_not_null", sql`NOT NULL report_id`), check("report_attachments_file_id_not_null", sql`NOT NULL file_id`), check("report_attachments_created_at_not_null", sql`NOT NULL created_at`), @@ -612,7 +696,9 @@ export const session = pgTable( id: text().primaryKey().notNull(), expiresAt: timestamp("expires_at", { mode: "string" }).notNull(), token: text().notNull(), - createdAt: timestamp("created_at", { mode: "string" }).defaultNow().notNull(), + createdAt: timestamp("created_at", { mode: "string" }) + .defaultNow() + .notNull(), updatedAt: timestamp("updated_at", { mode: "string" }).notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), @@ -647,11 +733,21 @@ export const user = pgTable( interests: jsonb().default([]), civilianCareer: text("civilian_career"), linkedin: text(), - signalVisibility: visibilityEnum("signal_visibility").default("private").notNull(), - emailVisibility: visibilityEnum("email_visibility").default("private").notNull(), - linkedinVisibility: visibilityEnum("linkedin_visibility").default("public").notNull(), - createdAt: timestamp("created_at", { mode: "string" }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { mode: "string" }).defaultNow().notNull(), + signalVisibility: visibilityEnum("signal_visibility") + .default("private") + .notNull(), + emailVisibility: visibilityEnum("email_visibility") + .default("private") + .notNull(), + linkedinVisibility: visibilityEnum("linkedin_visibility") + .default("public") + .notNull(), + createdAt: timestamp("created_at", { mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { mode: "string" }) + .defaultNow() + .notNull(), }, (table) => [ unique("user_email_unique").on(table.email), @@ -661,7 +757,10 @@ export const user = pgTable( check("user_email_verified_not_null", sql`NOT NULL email_verified`), check("user_signal_visibility_not_null", sql`NOT NULL signal_visibility`), check("user_email_visibility_not_null", sql`NOT NULL email_visibility`), - check("user_linkedin_visibility_not_null", sql`NOT NULL linkedin_visibility`), + check( + "user_linkedin_visibility_not_null", + sql`NOT NULL linkedin_visibility`, + ), check("user_created_at_not_null", sql`NOT NULL created_at`), check("user_updated_at_not_null", sql`NOT NULL updated_at`), ], @@ -674,8 +773,12 @@ export const verification = pgTable( identifier: text().notNull(), value: text().notNull(), expiresAt: timestamp("expires_at", { mode: "string" }).notNull(), - createdAt: timestamp("created_at", { mode: "string" }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { mode: "string" }).defaultNow().notNull(), + createdAt: timestamp("created_at", { mode: "string" }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { mode: "string" }) + .defaultNow() + .notNull(), }, () => [ check("verification_id_not_null", sql`NOT NULL id`), diff --git a/server/scripts/test-recommendations.ts b/server/scripts/test-recommendations.ts index 8d082624..b7bc5823 100644 --- a/server/scripts/test-recommendations.ts +++ b/server/scripts/test-recommendations.ts @@ -54,7 +54,11 @@ type MenteeInput = { }; async function ensureUser(input: SeedUserInput) { - const [existing] = await db.select().from(users).where(eq(users.email, input.email)).limit(1); + const [existing] = await db + .select() + .from(users) + .where(eq(users.email, input.email)) + .limit(1); if (existing) { const [updated] = await db @@ -104,7 +108,11 @@ async function ensureUser(input: SeedUserInput) { } async function ensureMentor(userId: string, input: MentorInput) { - const [existing] = await db.select().from(mentors).where(eq(mentors.userId, userId)).limit(1); + const [existing] = await db + .select() + .from(mentors) + .where(eq(mentors.userId, userId)) + .limit(1); if (existing) { const [updated] = await db @@ -130,7 +138,11 @@ async function ensureMentor(userId: string, input: MentorInput) { } async function ensureMentee(userId: string, input: MenteeInput) { - const [existing] = await db.select().from(mentees).where(eq(mentees.userId, userId)).limit(1); + const [existing] = await db + .select() + .from(mentees) + .where(eq(mentees.userId, userId)) + .limit(1); if (existing) { const [updated] = await db @@ -267,7 +279,11 @@ const MENTOR_DATA: Array<{ user: SeedUserInput; mentor: MentorInput }> = [ "Diversity makes our force stronger.", ], careerAdvice: "Your background is your strength, not a limitation.", - preferredMenteeCareerStages: ["new-soldiers", "junior-ncos", "transitioning"], + preferredMenteeCareerStages: [ + "new-soldiers", + "junior-ncos", + "transitioning", + ], preferredMeetingFormat: "hybrid", hoursPerMonthCommitment: 4, }, @@ -352,7 +368,11 @@ const MENTOR_DATA: Array<{ user: SeedUserInput; mentor: MentorInput }> = [ }, mentor: { yearsOfService: 4, - strengths: ["work-life-balance", "civilian-military-integration", "education"], + strengths: [ + "work-life-balance", + "civilian-military-integration", + "education", + ], personalInterests: "yoga, graduate school, startup culture", whyInterestedResponses: [ "Balancing civilian career and Guard service is challenging but rewarding.", @@ -473,7 +493,8 @@ const MENTOR_DATA: Array<{ user: SeedUserInput; mentor: MentorInput }> = [ "First Sergeants shape unit culture.", "I want to develop future senior NCOs.", ], - careerAdvice: "Take care of your soldiers and they will take care of the mission.", + careerAdvice: + "Take care of your soldiers and they will take care of the mission.", preferredMenteeCareerStages: ["junior-ncos", "senior-ncos"], preferredMeetingFormat: "in-person", hoursPerMonthCommitment: 4, @@ -528,7 +549,11 @@ const TEST_MENTEE: { user: SeedUserInput; mentee: MenteeInput } = { "Work-life balance strategies", "Networking within the Guard", ], - mentorQualities: ["strong-communicator", "experienced-leader", "encouraging-and-empathetic"], + mentorQualities: [ + "strong-communicator", + "experienced-leader", + "encouraging-and-empathetic", + ], preferredMeetingFormat: "hybrid", hoursPerMonthCommitment: 3, }, @@ -580,9 +605,15 @@ async function clearTestData() { // Delete in order due to foreign key constraints for (const userId of testUserIds) { - await db.delete(mentorRecommendations).where(eq(mentorRecommendations.userId, userId)); - await db.delete(mentorshipMatches).where(eq(mentorshipMatches.requestorUserId, userId)); - await db.delete(mentorshipMatches).where(eq(mentorshipMatches.mentorUserId, userId)); + await db + .delete(mentorRecommendations) + .where(eq(mentorRecommendations.userId, userId)); + await db + .delete(mentorshipMatches) + .where(eq(mentorshipMatches.requestorUserId, userId)); + await db + .delete(mentorshipMatches) + .where(eq(mentorshipMatches.mentorUserId, userId)); await db.delete(mentees).where(eq(mentees.userId, userId)); await db.delete(mentors).where(eq(mentors.userId, userId)); await db.delete(account).where(eq(account.userId, userId)); @@ -730,7 +761,9 @@ async function main() { console.log(` Mentee 1: ${TEST_MENTEE.user.email} (leadership-focused)`); console.log(` Mentee 2: ${TEST_MENTEE_TECH.user.email} (tech-focused)`); console.log(`\nPassword for all accounts: ${DEFAULT_PASSWORD}`); - console.log("\nLog in as a mentee to see their personalized mentor recommendations."); + console.log( + "\nLog in as a mentee to see their personalized mentor recommendations.", + ); console.log("The algorithm should rank mentors based on:"); console.log(" - Vector similarity (embeddings match)"); console.log(" - Meeting format compatibility"); diff --git a/server/src/data/db/schema.ts b/server/src/data/db/schema.ts index c42241bf..4cb8f460 100644 --- a/server/src/data/db/schema.ts +++ b/server/src/data/db/schema.ts @@ -16,11 +16,23 @@ import { import type { RoleKey } from "../../data/roles.js"; // Enums -export const permissionEnum = pgEnum("permission_enum", ["read", "write", "both"]); +export const permissionEnum = pgEnum("permission_enum", [ + "read", + "write", + "both", +]); -export const mentorStatusEnum = pgEnum("mentor_status_enum", ["requested", "approved", "active"]); +export const mentorStatusEnum = pgEnum("mentor_status_enum", [ + "requested", + "approved", + "active", +]); -export const menteeStatusEnum = pgEnum("mentee_status_enum", ["active", "inactive", "matched"]); +export const menteeStatusEnum = pgEnum("mentee_status_enum", [ + "active", + "inactive", + "matched", +]); export const matchStatusEnum = pgEnum("match_status_enum", [ "pending", // Mentee requested, waiting for mentor acceptance @@ -46,18 +58,27 @@ export const visibilityEnum = pgEnum("visibility_enum", ["private", "public"]); export type RoleNamespace = (typeof roleNamespaceEnum.enumValues)[number]; -export const channelPostPermissionEnum = pgEnum("channel_post_permission_enum", [ - "admin", - "everyone", - "custom", -]); +export const channelPostPermissionEnum = pgEnum( + "channel_post_permission_enum", + ["admin", "everyone", "custom"], +); // Mentorship application enums -export const positionTypeEnum = pgEnum("position_type_enum", ["active", "part-time"]); +export const positionTypeEnum = pgEnum("position_type_enum", [ + "active", + "part-time", +]); -export const serviceTypeEnum = pgEnum("service_type_enum", ["enlisted", "officer"]); +export const serviceTypeEnum = pgEnum("service_type_enum", [ + "enlisted", + "officer", +]); -export const meetingFormatEnum = pgEnum("meeting_format_enum", ["in-person", "virtual", "hybrid"]); +export const meetingFormatEnum = pgEnum("meeting_format_enum", [ + "in-person", + "virtual", + "hybrid", +]); export const careerStageEnum = pgEnum("career_stage_enum", [ "new-soldiers", @@ -69,7 +90,10 @@ export const careerStageEnum = pgEnum("career_stage_enum", [ "no-preference", ]); -export const mentorshipUserTypeEnum = pgEnum("mentorship_user_type_enum", ["mentor", "mentee"]); +export const mentorshipUserTypeEnum = pgEnum("mentorship_user_type_enum", [ + "mentor", + "mentee", +]); // pgvector support - custom type for vector columns const vector = customType<{ data: number[]; driverData: string }>({ @@ -107,9 +131,15 @@ export const users = pgTable( civilianCareer: text("civilian_career"), linkedin: text("linkedin"), - signalVisibility: visibilityEnum("signal_visibility").notNull().default("private"), - emailVisibility: visibilityEnum("email_visibility").notNull().default("private"), - linkedinVisibility: visibilityEnum("linkedin_visibility").notNull().default("public"), + signalVisibility: visibilityEnum("signal_visibility") + .notNull() + .default("private"), + emailVisibility: visibilityEnum("email_visibility") + .notNull() + .default("private"), + linkedinVisibility: visibilityEnum("linkedin_visibility") + .notNull() + .default("public"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") @@ -177,7 +207,9 @@ export const channels = pgTable( channelId: integer("channel_id").primaryKey().generatedAlwaysAsIdentity(), name: text("name").notNull(), description: text("description"), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), metadata: jsonb("metadata"), postPermissionLevel: channelPostPermissionEnum("post_permission_level") .notNull() @@ -220,8 +252,12 @@ export const roles = pgTable( }), metadata: jsonb("metadata"), description: text("description"), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ sql`CONSTRAINT ck_roles_channel_namespace CHECK (${table.namespace.name} <> 'channel' OR ${table.channelId.name} IS NOT NULL)`, @@ -250,7 +286,9 @@ export const userRoles = pgTable( roleId: integer("role_id") .references(() => roles.roleId, { onDelete: "cascade" }) .notNull(), - assignedAt: timestamp("assigned_at", { withTimezone: true }).defaultNow().notNull(), + assignedAt: timestamp("assigned_at", { withTimezone: true }) + .defaultNow() + .notNull(), assignedBy: text("assigned_by").references(() => users.id, { onDelete: "set null", }), @@ -271,18 +309,27 @@ export const userRoles = pgTable( export const channelSubscriptions = pgTable( "channel_subscriptions", { - subscriptionId: integer("subscription_id").primaryKey().generatedAlwaysAsIdentity(), + subscriptionId: integer("subscription_id") + .primaryKey() + .generatedAlwaysAsIdentity(), userId: text("user_id") .references(() => users.id, { onDelete: "cascade" }) .notNull(), channelId: integer("channel_id") .references(() => channels.channelId, { onDelete: "cascade" }) .notNull(), - notificationsEnabled: boolean("notifications_enabled").default(true).notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + notificationsEnabled: boolean("notifications_enabled") + .default(true) + .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ - uniqueIndex("ux_channel_subscriptions_user_channel").on(table.userId, table.channelId), + uniqueIndex("ux_channel_subscriptions_user_channel").on( + table.userId, + table.channelId, + ), index("ix_channel_subscriptions_user_id").on(table.userId), index("ix_channel_subscriptions_channel_id").on(table.channelId), ], @@ -301,7 +348,9 @@ export const messages = pgTable( }), message: text("message"), attachmentUrl: text("attachment_url"), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ index("ix_messages_channel_id").on(table.channelId), @@ -312,19 +361,26 @@ export const messages = pgTable( export const messageAttachments = pgTable( "message_attachments", { - attachmentId: integer("attachment_id").primaryKey().generatedAlwaysAsIdentity(), + attachmentId: integer("attachment_id") + .primaryKey() + .generatedAlwaysAsIdentity(), messageId: integer("message_id") .references(() => messages.messageId, { onDelete: "cascade" }) .notNull(), fileId: uuid("file_id") .references(() => files.fileId, { onDelete: "cascade" }) .notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ index("ix_message_attachments_message_id").on(table.messageId), index("ix_message_attachments_file_id").on(table.fileId), - uniqueIndex("ux_message_attachments_message_file").on(table.messageId, table.fileId), + uniqueIndex("ux_message_attachments_message_file").on( + table.messageId, + table.fileId, + ), ], ); @@ -339,10 +395,16 @@ export const messageReactions = pgTable( .references(() => users.id, { onDelete: "cascade" }) .notNull(), emoji: text("emoji").notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ - uniqueIndex("ux_message_reactions_user").on(table.messageId, table.userId, table.emoji), + uniqueIndex("ux_message_reactions_user").on( + table.messageId, + table.userId, + table.emoji, + ), index("ix_message_reactions_message_id").on(table.messageId), index("ix_message_reactions_user_id").on(table.userId), ], @@ -361,7 +423,9 @@ export const mentors = pgTable( .notNull(), mentorshipPreferences: text("mentorship_preferences"), yearsOfService: integer("years_of_service"), - eligibilityData: jsonb("eligibility_data").$type | null | undefined>(), + eligibilityData: jsonb("eligibility_data").$type< + Record | null | undefined + >(), status: mentorStatusEnum("status").default("requested").notNull(), // New application fields resumeFileId: uuid("resume_file_id").references(() => files.fileId, { @@ -371,11 +435,17 @@ export const mentors = pgTable( personalInterests: text("personal_interests"), whyInterestedResponses: jsonb("why_interested_responses").$type(), // Ordered responses careerAdvice: text("career_advice"), // Text response to advice question - preferredMenteeCareerStages: jsonb("preferred_mentee_career_stages").$type(), // Array of career stage enum values + preferredMenteeCareerStages: jsonb("preferred_mentee_career_stages").$type< + string[] + >(), // Array of career stage enum values preferredMeetingFormat: meetingFormatEnum("preferred_meeting_format"), hoursPerMonthCommitment: integer("hours_per_month_commitment"), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ // CHECK (years_of_service IS NULL OR years_of_service >= 0) @@ -392,15 +462,23 @@ export const mentors = pgTable( export const mentorRecommendations = pgTable( "mentor_recommendations", { - recommendationId: integer("recommendation_id").primaryKey().generatedAlwaysAsIdentity(), + recommendationId: integer("recommendation_id") + .primaryKey() + .generatedAlwaysAsIdentity(), userId: text("user_id") .references(() => users.id, { onDelete: "cascade" }) .notNull(), - recommendedMentorIds: jsonb("recommended_mentor_ids").$type().notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + recommendedMentorIds: jsonb("recommended_mentor_ids") + .$type() + .notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), expiresAt: timestamp("expires_at", { withTimezone: true }), }, - (table) => [uniqueIndex("ux_mentor_recommendations_user_id").on(table.userId)], + (table) => [ + uniqueIndex("ux_mentor_recommendations_user_id").on(table.userId), + ], ); // MENTORSHIP MATCHES @@ -411,11 +489,16 @@ export const mentorshipMatches = pgTable( requestorUserId: text("requestor_user_id").references(() => users.id), mentorUserId: text("mentor_user_id").references(() => users.id), status: matchStatusEnum("status").default("pending").notNull(), - matchedAt: timestamp("matched_at", { withTimezone: true }).defaultNow().notNull(), + matchedAt: timestamp("matched_at", { withTimezone: true }) + .defaultNow() + .notNull(), message: text("message"), // Optional personalized message from mentee to mentor }, (table) => [ - uniqueIndex("ux_mentorship_matches_pair").on(table.requestorUserId, table.mentorUserId), + uniqueIndex("ux_mentorship_matches_pair").on( + table.requestorUserId, + table.mentorUserId, + ), index("ix_mentorship_matches_requestor_user_id").on(table.requestorUserId), index("ix_mentorship_matches_mentor_user_id").on(table.mentorUserId), index("ix_mentorship_matches_status").on(table.status), @@ -426,7 +509,9 @@ export const mentorshipMatches = pgTable( export const pushSubscriptions = pgTable( "push_subscriptions", { - subscriptionId: integer("subscription_id").primaryKey().generatedAlwaysAsIdentity(), + subscriptionId: integer("subscription_id") + .primaryKey() + .generatedAlwaysAsIdentity(), userId: text("user_id") .references(() => users.id, { onDelete: "cascade" }) .notNull(), @@ -435,7 +520,9 @@ export const pushSubscriptions = pgTable( auth: text("auth").notNull(), keys: jsonb("keys"), topics: jsonb("topics"), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), isActive: boolean("is_active").default(true).notNull(), }, (table) => [ @@ -466,8 +553,12 @@ export const mentees = pgTable( mentorQualities: jsonb("mentor_qualities").$type(), // What qualities look for preferredMeetingFormat: meetingFormatEnum("preferred_meeting_format"), hoursPerMonthCommitment: integer("hours_per_month_commitment"), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ sql`CONSTRAINT ck_mentees_hours_per_month CHECK (${table.hoursPerMonthCommitment.name} IS NULL OR ${table.hoursPerMonthCommitment.name} > 0)`, @@ -493,8 +584,12 @@ export const messageBlasts = pgTable( .notNull() .default(sql`NOW() + INTERVAL '24 hours'`), status: messageBlastStatusEnum("status").default("draft").notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ index("ix_message_blasts_sender_id").on(table.senderId), @@ -504,7 +599,11 @@ export const messageBlasts = pgTable( ); // Reports -export const reportStatusEnum = pgEnum("report_status_enum", ["Pending", "Assigned", "Resolved"]); +export const reportStatusEnum = pgEnum("report_status_enum", [ + "Pending", + "Assigned", + "Resolved", +]); export const reportCategoryEnum = pgEnum("report_category_enum", [ "Communication", @@ -529,25 +628,36 @@ export const reports = pgTable("reports", { assignedBy: text("assigned_by").references(() => users.id, { onDelete: "set null", }), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), resolvedAt: timestamp("resolved", { withTimezone: true }), }); export const reportAttachments = pgTable( "report_attachments", { - attachmentId: integer("attachment_id").primaryKey().generatedAlwaysAsIdentity(), + attachmentId: integer("attachment_id") + .primaryKey() + .generatedAlwaysAsIdentity(), reportId: uuid("report_id") .references(() => reports.reportId, { onDelete: "cascade" }) .notNull(), fileId: uuid("file_id") .references(() => files.fileId, { onDelete: "cascade" }) .notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ - uniqueIndex("ux_report_attachments_report_file").on(table.reportId, table.fileId), + uniqueIndex("ux_report_attachments_report_file").on( + table.reportId, + table.fileId, + ), index("ix_report_attachments_report_id").on(table.reportId), ], ); @@ -556,7 +666,9 @@ export const reportAttachments = pgTable( export const mentorshipEmbeddings = pgTable( "mentorship_embeddings", { - embeddingId: integer("embedding_id").primaryKey().generatedAlwaysAsIdentity(), + embeddingId: integer("embedding_id") + .primaryKey() + .generatedAlwaysAsIdentity(), userId: text("user_id") .references(() => users.id, { onDelete: "cascade" }) .notNull(), @@ -566,11 +678,18 @@ export const mentorshipEmbeddings = pgTable( hopeToGainEmbedding: vector("hope_to_gain_embedding"), // For mentees // Combined embedding for overall profile matching profileEmbedding: vector("profile_embedding"), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), }, (table) => [ - uniqueIndex("ux_mentorship_embeddings_user_type").on(table.userId, table.userType), + uniqueIndex("ux_mentorship_embeddings_user_type").on( + table.userId, + table.userType, + ), index("ix_mentorship_embeddings_user_id").on(table.userId), index("ix_mentorship_embeddings_user_type").on(table.userType), sql`CREATE INDEX IF NOT EXISTS ix_mentorship_embeddings_profile_embedding ON mentorship_embeddings USING ivfflat (profile_embedding vector_cosine_ops) WITH (lists = 100)`, @@ -587,7 +706,9 @@ export const inviteCodes = pgTable( createdBy: text("created_by") .references(() => users.id, { onDelete: "set null" }) .notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(), usedBy: text("used_by").references(() => users.id, { onDelete: "set null", diff --git a/server/src/data/repository/mentee-repo.ts b/server/src/data/repository/mentee-repo.ts index 1a15103f..975b75f8 100644 --- a/server/src/data/repository/mentee-repo.ts +++ b/server/src/data/repository/mentee-repo.ts @@ -1,5 +1,10 @@ import { and, eq, inArray } from "drizzle-orm"; -import { mentees, mentors, mentorshipMatches, users } from "../../data/db/schema.js"; +import { + mentees, + mentors, + mentorshipMatches, + users, +} from "../../data/db/schema.js"; import { db } from "../../data/db/sql.js"; import { ConflictError, NotFoundError } from "../../types/errors.js"; import type { @@ -219,14 +224,20 @@ export class MenteeRepository { }; if (learningGoals !== undefined) updateData.learningGoals = learningGoals; - if (experienceLevel !== undefined) updateData.experienceLevel = experienceLevel; - if (preferredMentorType !== undefined) updateData.preferredMentorType = preferredMentorType; + if (experienceLevel !== undefined) + updateData.experienceLevel = experienceLevel; + if (preferredMentorType !== undefined) + updateData.preferredMentorType = preferredMentorType; if (status !== undefined) updateData.status = status; if (resumeFileId !== undefined) updateData.resumeFileId = resumeFileId; - if (personalInterests !== undefined) updateData.personalInterests = personalInterests; - if (roleModelInspiration !== undefined) updateData.roleModelInspiration = roleModelInspiration; - if (hopeToGainResponses !== undefined) updateData.hopeToGainResponses = hopeToGainResponses; - if (mentorQualities !== undefined) updateData.mentorQualities = mentorQualities; + if (personalInterests !== undefined) + updateData.personalInterests = personalInterests; + if (roleModelInspiration !== undefined) + updateData.roleModelInspiration = roleModelInspiration; + if (hopeToGainResponses !== undefined) + updateData.hopeToGainResponses = hopeToGainResponses; + if (mentorQualities !== undefined) + updateData.mentorQualities = mentorQualities; if (preferredMeetingFormat !== undefined) updateData.preferredMeetingFormat = preferredMeetingFormat; if (hoursPerMonthCommitment !== undefined) @@ -282,7 +293,9 @@ export class MenteeRepository { * @param status Mentee status * @returns Array of mentee profiles */ - async getMenteesByStatus(status: "active" | "inactive" | "matched"): Promise { + async getMenteesByStatus( + status: "active" | "inactive" | "matched", + ): Promise { return await db .select({ menteeId: mentees.menteeId, diff --git a/server/src/data/repository/mentor-repo.ts b/server/src/data/repository/mentor-repo.ts index 308ed93f..035a5b51 100644 --- a/server/src/data/repository/mentor-repo.ts +++ b/server/src/data/repository/mentor-repo.ts @@ -1,9 +1,17 @@ import { and, eq, inArray } from "drizzle-orm"; -import { mentees, mentors, mentorshipMatches, users } from "../../data/db/schema.js"; +import { + mentees, + mentors, + mentorshipMatches, + users, +} from "../../data/db/schema.js"; import { db } from "../../data/db/sql.js"; import { ConflictError, NotFoundError } from "../../types/errors.js"; import type { GetMenteeOutput } from "../../types/mentee-types.js"; -import type { CreateMentorOutput, GetMentorOutput } from "../../types/mentor-types.js"; +import type { + CreateMentorOutput, + GetMentorOutput, +} from "../../types/mentor-types.js"; import type { PendingMenteeRequest } from "../../types/mentorship-types.js"; /** @@ -193,7 +201,9 @@ export class MentorRepository { /** * Get pending mentee requests for a mentor */ - async getPendingMenteeRequests(userId: string): Promise { + async getPendingMenteeRequests( + userId: string, + ): Promise { const pendingRows = await db .select({ matchId: mentorshipMatches.matchId, @@ -228,7 +238,10 @@ export class MentorRepository { .innerJoin(mentees, eq(mentees.userId, mentorshipMatches.requestorUserId)) .innerJoin(users, eq(users.id, mentees.userId)) .where( - and(eq(mentorshipMatches.mentorUserId, userId), eq(mentorshipMatches.status, "pending")), + and( + eq(mentorshipMatches.mentorUserId, userId), + eq(mentorshipMatches.status, "pending"), + ), ); return pendingRows.map((row) => ({ @@ -346,7 +359,10 @@ export class MentorRepository { .innerJoin(mentees, eq(mentees.userId, mentorshipMatches.requestorUserId)) .innerJoin(users, eq(users.id, mentees.userId)) .where( - and(eq(mentorshipMatches.mentorUserId, userId), eq(mentorshipMatches.status, "accepted")), + and( + eq(mentorshipMatches.mentorUserId, userId), + eq(mentorshipMatches.status, "accepted"), + ), ); return { mentor, activeMentees }; diff --git a/server/src/types/mentee-types.ts b/server/src/types/mentee-types.ts index ecb1872d..9b0f9f64 100644 --- a/server/src/types/mentee-types.ts +++ b/server/src/types/mentee-types.ts @@ -12,7 +12,10 @@ export const menteeSchema = z.object({ roleModelInspiration: z.string().nullable().optional(), hopeToGainResponses: z.array(z.string()).nullable().optional(), mentorQualities: z.array(z.string()).nullable().optional(), - preferredMeetingFormat: z.enum(["in-person", "virtual", "hybrid"]).nullable().optional(), + preferredMeetingFormat: z + .enum(["in-person", "virtual", "hybrid"]) + .nullable() + .optional(), hoursPerMonthCommitment: z.number().int().positive().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), @@ -25,7 +28,10 @@ export const createMenteeInputSchema = z.object({ learningGoals: z.string().optional(), experienceLevel: z.string().optional(), preferredMentorType: z.string().optional(), - status: z.enum(["active", "inactive", "matched"]).optional().default("active"), + status: z + .enum(["active", "inactive", "matched"]) + .optional() + .default("active"), resumeFileId: z.string().uuid().optional(), personalInterests: z.string().optional(), roleModelInspiration: z.string().optional(), diff --git a/server/src/types/mentor-types.ts b/server/src/types/mentor-types.ts index 454af284..9f16cd25 100644 --- a/server/src/types/mentor-types.ts +++ b/server/src/types/mentor-types.ts @@ -26,7 +26,10 @@ export const mentorSchema = z.object({ ) .nullable() .optional(), - preferredMeetingFormat: z.enum(["in-person", "virtual", "hybrid"]).nullable().optional(), + preferredMeetingFormat: z + .enum(["in-person", "virtual", "hybrid"]) + .nullable() + .optional(), hoursPerMonthCommitment: z.number().int().positive().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), @@ -39,7 +42,10 @@ export const createMentorInputSchema = z.object({ mentorshipPreferences: z.string().optional(), yearsOfService: z.number().int().nonnegative().optional(), eligibilityData: z.record(z.string(), z.unknown()).nullish(), - status: z.enum(["requested", "approved", "active"]).optional().default("requested"), + status: z + .enum(["requested", "approved", "active"]) + .optional() + .default("requested"), resumeFileId: z.string().uuid().optional(), strengths: z.array(z.string()).max(5).optional().default([]), personalInterests: z.string().optional(), diff --git a/web/src/app/mentorship/apply/mentee/page.tsx b/web/src/app/mentorship/apply/mentee/page.tsx index 6663f013..b0d9dad0 100644 --- a/web/src/app/mentorship/apply/mentee/page.tsx +++ b/web/src/app/mentorship/apply/mentee/page.tsx @@ -10,7 +10,11 @@ import { DragReorderFrame } from "@/components/drag-and-drop"; import { icons } from "@/components/icons"; import { MultiSelect, type MultiSelectOption } from "@/components/multi-select"; import { TextInput } from "@/components/text-input"; -import { Dropzone, DropzoneContent, DropzoneEmptyState } from "@/components/ui/shadcn-io/dropzone"; +import { + Dropzone, + DropzoneContent, + DropzoneEmptyState, +} from "@/components/ui/shadcn-io/dropzone"; import { authClient } from "@/lib/auth-client"; import { useTRPC, useTRPCClient } from "@/lib/trpc"; @@ -64,7 +68,10 @@ export default function MentorshipApplyMenteePage() { const { data: sessionData } = authClient.useSession(); const userId = sessionData?.user?.id ?? null; const backHref = useMemo( - () => (searchParams.get("from") === "dashboard" ? "/mentorship/dashboard" : "/mentorship"), + () => + searchParams.get("from") === "dashboard" + ? "/mentorship/dashboard" + : "/mentorship", [searchParams], ); const BackIcon = icons.arrowLeft; @@ -72,7 +79,9 @@ export default function MentorshipApplyMenteePage() { const [resume, setResume] = useState(null); const [selectedQualities, setSelectedQualities] = useState([]); const [selectedInterests, setSelectedInterests] = useState([]); - const [selectedMeetingFormats, setSelectedMeetingFormats] = useState([]); + const [selectedMeetingFormats, setSelectedMeetingFormats] = useState< + string[] + >([]); const [hopeToGainOrder, setHopeToGainOrder] = useState([]); const [multiLineText, setMultiLineText] = useState(""); const [desiredMentorHours, setDesiredMentorHours] = useState(""); @@ -82,8 +91,9 @@ export default function MentorshipApplyMenteePage() { // Aligned with server `appRouter.mentorship.createMentee` const mentorshipQueryKey = trpc.mentorship.getMentorshipData.queryKey(); const createMentee = useMutation({ - mutationFn: async (input: Parameters[0]) => - trpcClient.mentorship.createMentee.mutate(input), + mutationFn: async ( + input: Parameters[0], + ) => trpcClient.mentorship.createMentee.mutate(input), onSuccess: () => { queryClient.invalidateQueries({ queryKey: mentorshipQueryKey }); }, @@ -128,7 +138,10 @@ export default function MentorshipApplyMenteePage() { fileId: presign.fileId, }); } catch (error) { - const message = error instanceof Error ? error.message : "We couldn't upload that file."; + const message = + error instanceof Error + ? error.message + : "We couldn't upload that file."; setResume({ file, status: "error", @@ -176,7 +189,8 @@ export default function MentorshipApplyMenteePage() { try { // Map meeting formats to backend enum const preferredMeetingFormat = - selectedMeetingFormats.includes("in-person") && selectedMeetingFormats.includes("online") + selectedMeetingFormats.includes("in-person") && + selectedMeetingFormats.includes("online") ? "hybrid" : selectedMeetingFormats.includes("online") ? "virtual" @@ -191,10 +205,15 @@ export default function MentorshipApplyMenteePage() { await createMentee.mutateAsync({ userId, resumeFileId: resume?.status === "uploaded" ? resume.fileId : undefined, - personalInterests: selectedInterests.length > 0 ? selectedInterests.join(", ") : undefined, + personalInterests: + selectedInterests.length > 0 + ? selectedInterests.join(", ") + : undefined, roleModelInspiration: multiLineText.trim() || undefined, - hopeToGainResponses: hopeToGainOrder.length > 0 ? hopeToGainOrder : undefined, - mentorQualities: selectedQualities.length > 0 ? selectedQualities : undefined, + hopeToGainResponses: + hopeToGainOrder.length > 0 ? hopeToGainOrder : undefined, + mentorQualities: + selectedQualities.length > 0 ? selectedQualities : undefined, preferredMeetingFormat, hoursPerMonthCommitment, }); @@ -244,11 +263,13 @@ export default function MentorshipApplyMenteePage() {

- Thank you for your interest in the mentorship program. Give yourself enough time to - thoughtfully complete this application. Your responses will help us match you with a - mentor who can best support your goals. + Thank you for your interest in the mentorship program. Give yourself + enough time to thoughtfully complete this application. Your responses + will help us match you with a mentor who can best support your goals. +

+

+ *Required Information

-

*Required Information

@@ -288,7 +309,8 @@ export default function MentorshipApplyMenteePage() {
- 3. Who has been an important role model or source of inspiration for you, and why? + 3. Who has been an important role model or source of inspiration for + you, and why?
4. What do you hope to get out of the mentorship program?
- Rank the following reasons from most important (1) to least important (5). + Rank the following reasons from most important (1) to least + important (5).

- 7. How much time would you like to spend with your mentor per month?* + 7. How much time would you like to spend with your mentor per + month?*

- {formError &&

{formError}

} + {formError && ( +

{formError}

+ )} (searchParams.get("from") === "dashboard" ? "/mentorship/dashboard" : "/mentorship"), + () => + searchParams.get("from") === "dashboard" + ? "/mentorship/dashboard" + : "/mentorship", [searchParams], ); const BackIcon = icons.arrowLeft; @@ -38,8 +45,12 @@ export default function MentorshipApplyMentorPage() { const [selectedInterests, setSelectedInterests] = useState([]); const [multiLineText, setMultiLineText] = useState(""); const [whyInterestedOrder, setWhyInterestedOrder] = useState([]); - const [selectedCareerStages, setSelectedCareerStages] = useState([]); - const [selectedMeetingFormats, setSelectedMeetingFormats] = useState([]); + const [selectedCareerStages, setSelectedCareerStages] = useState( + [], + ); + const [selectedMeetingFormats, setSelectedMeetingFormats] = useState< + string[] + >([]); const [availableMentorHours, setAvailableMentorHours] = useState(""); const [formError, setFormError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -48,7 +59,9 @@ export default function MentorshipApplyMentorPage() { // Aligned with server `appRouter.mentorship.createMentor` const createMentor = useMutation({ mutationFn: async ( - input: Parameters<(typeof trpcClient.mentorship.createMentor)["mutate"]>[0], + input: Parameters< + (typeof trpcClient.mentorship.createMentor)["mutate"] + >[0], ) => trpcClient.mentorship.createMentor.mutate(input), }); @@ -91,7 +104,10 @@ export default function MentorshipApplyMentorPage() { fileId: presign.fileId, }); } catch (error) { - const message = error instanceof Error ? error.message : "We couldn't upload that file."; + const message = + error instanceof Error + ? error.message + : "We couldn't upload that file."; setResume({ file, status: "error", @@ -142,7 +158,8 @@ export default function MentorshipApplyMentorPage() { try { // Map meeting formats to backend enum const preferredMeetingFormat = - selectedMeetingFormats.includes("in-person") && selectedMeetingFormats.includes("online") + selectedMeetingFormats.includes("in-person") && + selectedMeetingFormats.includes("online") ? "hybrid" : selectedMeetingFormats.includes("online") ? "virtual" @@ -158,7 +175,9 @@ export default function MentorshipApplyMentorPage() { | "transitioning" | "no-preference"; - const preferredMenteeCareerStages: PreferredMenteeCareerStage[] | undefined = + const preferredMenteeCareerStages: + | PreferredMenteeCareerStage[] + | undefined = selectedCareerStages.length > 0 ? selectedCareerStages.map((stage) => stage === "transitioning-soldiers" @@ -177,8 +196,12 @@ export default function MentorshipApplyMentorPage() { userId, resumeFileId: resume?.status === "uploaded" ? resume.fileId : undefined, strengths: selectedQualities, - personalInterests: selectedInterests.length > 0 ? selectedInterests.join(", ") : undefined, - whyInterestedResponses: whyInterestedOrder.length > 0 ? whyInterestedOrder : undefined, + personalInterests: + selectedInterests.length > 0 + ? selectedInterests.join(", ") + : undefined, + whyInterestedResponses: + whyInterestedOrder.length > 0 ? whyInterestedOrder : undefined, careerAdvice: multiLineText.trim() || undefined, preferredMenteeCareerStages, preferredMeetingFormat, @@ -299,17 +322,21 @@ export default function MentorshipApplyMentorPage() {

- Thank you for your interest in mentoring. Give yourself 20-25 minutes to thoughtfully - complete this application. Your responses are used to match you with potential mentees and - will be shared with mentees who are interested in connecting. + Thank you for your interest in mentoring. Give yourself 20-25 minutes + to thoughtfully complete this application. Your responses are used to + match you with potential mentees and will be shared with mentees who + are interested in connecting. +

+

+ *Required Information

-

*Required Information

- 1. Upload a resume to share your educational and career history with potential mentees. + 1. Upload a resume to share your educational and career history with + potential mentees.

4. What are you interested in becoming a mentor?
- Rank the following reasons from most important (1) to least important (5). + Rank the following reasons from most important (1) to least + important (5).

- 5. What is one piece of advice that you wish you had received earlier in your career? + 5. What is one piece of advice that you wish you had received + earlier in your career?

- {formError &&

{formError}

} + {formError && ( +

{formError}

+ )}