diff --git a/client/web/package-lock.json b/client/web/package-lock.json index 407533e8..1561340d 100644 --- a/client/web/package-lock.json +++ b/client/web/package-lock.json @@ -49,6 +49,7 @@ "react-day-picker": "^9.13.0", "react-dom": "^19.2.0", "react-hook-form": "^7.71.1", + "react-phone-number-input": "^3.4.16", "react-resizable-panels": "^4.4.1", "react-router-dom": "^7.12.0", "recharts": "^2.15.4", @@ -3988,6 +3989,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4053,6 +4060,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/country-flag-icons": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.15.tgz", + "integrity": "sha512-92HoA8l6DluEidku8tKBftjuFRj4Rv3zDW1lXxCuNnqAxhUSkvso9gM/Afj4F5BnK+wneHIe3ydI+s+4NA29/Q==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4753,9 +4766,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4933,6 +4946,27 @@ "node": ">=0.8.19" } }, + "node_modules/input-format": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/input-format/-/input-format-0.3.14.tgz", + "integrity": "sha512-gHMrgrbCgmT4uK5Um5eVDUohuV9lcs95ZUUN9Px2Y0VIfjTzT2wF8Q3Z4fwLFm7c5Z2OXCm53FHoovj6SlOKdg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=18.1.0", + "react-dom": ">=18.1.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/input-otp": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", @@ -5119,6 +5153,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.38", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.38.tgz", + "integrity": "sha512-vwzxmasAy9hZigxtqTbFEwp8ZdZ975TiqVDwj5bKx5sR+zi5ucUQy9mbVTkKM9GzqdLdxux/hTw2nmN5J7POMA==", + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", @@ -5639,9 +5679,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -5802,6 +5842,23 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-phone-number-input": { + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/react-phone-number-input/-/react-phone-number-input-3.4.16.tgz", + "integrity": "sha512-ix1+SIyGuLxnlAeW+XQNhwOmmDd4Zmr6Ng1eQl0E2XS8xIuue3gdZeBG4LGTMjTIFneOTO9pUPL+AEDhV6+r+A==", + "license": "MIT", + "dependencies": { + "classnames": "^2.5.1", + "country-flag-icons": "^1.6.14", + "input-format": "^0.3.14", + "libphonenumber-js": "^1.12.37", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-qr-code": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.18.tgz", @@ -6314,9 +6371,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -6659,9 +6716,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" diff --git a/client/web/package.json b/client/web/package.json index 35f50e12..8d74e951 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -53,6 +53,7 @@ "react-day-picker": "^9.13.0", "react-dom": "^19.2.0", "react-hook-form": "^7.71.1", + "react-phone-number-input": "^3.4.16", "react-resizable-panels": "^4.4.1", "react-router-dom": "^7.12.0", "recharts": "^2.15.4", diff --git a/client/web/src/components/ui/phone-input.tsx b/client/web/src/components/ui/phone-input.tsx new file mode 100644 index 00000000..79a8df52 --- /dev/null +++ b/client/web/src/components/ui/phone-input.tsx @@ -0,0 +1,171 @@ +import * as React from "react"; + +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/shared/lib/utils"; + +// Country codes list +const COUNTRY_CODES = [ + { code: "US", dialCode: "+1", name: "United States" }, + { code: "CA", dialCode: "+1", name: "Canada" }, + { code: "MX", dialCode: "+52", name: "Mexico" }, + { code: "GB", dialCode: "+44", name: "United Kingdom" }, + { code: "DE", dialCode: "+49", name: "Germany" }, + { code: "FR", dialCode: "+33", name: "France" }, + { code: "IN", dialCode: "+91", name: "India" }, + { code: "CN", dialCode: "+86", name: "China" }, + { code: "JP", dialCode: "+81", name: "Japan" }, + { code: "KR", dialCode: "+82", name: "South Korea" }, + { code: "AU", dialCode: "+61", name: "Australia" }, + { code: "BR", dialCode: "+55", name: "Brazil" }, + { code: "ES", dialCode: "+34", name: "Spain" }, + { code: "IT", dialCode: "+39", name: "Italy" }, + { code: "NL", dialCode: "+31", name: "Netherlands" }, + { code: "SE", dialCode: "+46", name: "Sweden" }, + { code: "CH", dialCode: "+41", name: "Switzerland" }, + { code: "SG", dialCode: "+65", name: "Singapore" }, + { code: "AE", dialCode: "+971", name: "UAE" }, + { code: "SA", dialCode: "+966", name: "Saudi Arabia" }, + { code: "NG", dialCode: "+234", name: "Nigeria" }, + { code: "ZA", dialCode: "+27", name: "South Africa" }, + { code: "PH", dialCode: "+63", name: "Philippines" }, + { code: "VN", dialCode: "+84", name: "Vietnam" }, + { code: "ID", dialCode: "+62", name: "Indonesia" }, + { code: "MY", dialCode: "+60", name: "Malaysia" }, + { code: "TH", dialCode: "+66", name: "Thailand" }, + { code: "PL", dialCode: "+48", name: "Poland" }, + { code: "TR", dialCode: "+90", name: "Turkey" }, + { code: "EG", dialCode: "+20", name: "Egypt" }, +] as const; + +// Format phone number for display (US format) +function formatPhoneDisplay(digits: string): string { + if (digits.length <= 3) return digits; + if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`; + return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`; +} + +// Parse E.164 to extract country code and national number +function parseE164(e164: string): { + countryCode: string; + nationalNumber: string; +} { + if (!e164 || !e164.startsWith("+")) { + return { countryCode: "US", nationalNumber: "" }; + } + + // Sort by dial code length (longest first) to match correctly + const sortedCodes = [...COUNTRY_CODES].sort( + (a, b) => b.dialCode.length - a.dialCode.length, + ); + + for (const country of sortedCodes) { + if (e164.startsWith(country.dialCode)) { + return { + countryCode: country.code, + nationalNumber: e164.slice(country.dialCode.length), + }; + } + } + + return { countryCode: "US", nationalNumber: e164.slice(1) }; +} + +interface PhoneInputProps { + value?: string; + onChange?: (value: string | undefined) => void; + placeholder?: string; + disabled?: boolean; + className?: string; +} + +function PhoneInput({ + value = "", + onChange, + placeholder = "(555) 123-4567", + disabled, + className, +}: PhoneInputProps) { + // Parse the E.164 value + const parsed = parseE164(value); + const [countryCode, setCountryCode] = React.useState(parsed.countryCode); + const [nationalNumber, setNationalNumber] = React.useState( + parsed.nationalNumber, + ); + + // Get the dial code for selected country + const selectedCountry = + COUNTRY_CODES.find((c) => c.code === countryCode) || COUNTRY_CODES[0]; + + // Sync when value prop changes externally + React.useEffect(() => { + const parsed = parseE164(value); + setCountryCode(parsed.countryCode); + setNationalNumber(parsed.nationalNumber); + }, [value]); + + // Update the E.164 value when country or number changes + const updateValue = (newCountryCode: string, newNationalNumber: string) => { + const country = + COUNTRY_CODES.find((c) => c.code === newCountryCode) || COUNTRY_CODES[0]; + const digits = newNationalNumber.replace(/\D/g, ""); + if (digits) { + onChange?.(country.dialCode + digits); + } else { + onChange?.(undefined); + } + }; + + const handleCountryChange = (newCode: string) => { + setCountryCode(newCode); + updateValue(newCode, nationalNumber); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const digits = e.target.value.replace(/\D/g, "").slice(0, 15); + setNationalNumber(digits); + updateValue(countryCode, digits); + }; + + return ( +
+ {/* Country Code Dropdown */} + + + {/* Phone Number Input */} + +
+ ); +} + +export { PhoneInput }; diff --git a/client/web/src/pages/admin/_shared/grading/GradingDetailsPanel.tsx b/client/web/src/pages/admin/_shared/grading/GradingDetailsPanel.tsx index 41a71c29..74e2f1fa 100644 --- a/client/web/src/pages/admin/_shared/grading/GradingDetailsPanel.tsx +++ b/client/web/src/pages/admin/_shared/grading/GradingDetailsPanel.tsx @@ -5,6 +5,7 @@ import { toast } from "sonner"; import { Skeleton } from "@/components/ui/skeleton"; import { fetchApplicationResumeURL } from "@/pages/admin/all-applicants/api"; import { + AgreementsSection, DemographicsSection, EducationSection, EventPreferencesSection, @@ -83,6 +84,7 @@ export const GradingDetailsPanel = memo(function GradingDetailsPanel({ onViewResume={handleViewResume} isOpeningResume={isOpeningResume} /> + {children} diff --git a/client/web/src/pages/admin/all-applicants/components/ApplicationDetailPanel.tsx b/client/web/src/pages/admin/all-applicants/components/ApplicationDetailPanel.tsx index 21d66870..a558810e 100644 --- a/client/web/src/pages/admin/all-applicants/components/ApplicationDetailPanel.tsx +++ b/client/web/src/pages/admin/all-applicants/components/ApplicationDetailPanel.tsx @@ -17,6 +17,7 @@ import type { Application } from "@/types"; import { fetchApplicationResumeURL } from "../api"; import { formatName, getStatusColor } from "../utils"; import { + AgreementsSection, DemographicsSection, EducationSection, EventPreferencesSection, @@ -133,6 +134,7 @@ export const ApplicationDetailPanel = memo(function ApplicationDetailPanel({ onViewResume={handleViewResume} isOpeningResume={isOpeningResume} /> + ) : null} diff --git a/client/web/src/pages/admin/all-applicants/components/detail-sections/AgreementsSection.tsx b/client/web/src/pages/admin/all-applicants/components/detail-sections/AgreementsSection.tsx new file mode 100644 index 00000000..6782cb84 --- /dev/null +++ b/client/web/src/pages/admin/all-applicants/components/detail-sections/AgreementsSection.tsx @@ -0,0 +1,68 @@ +import { CheckCircle2, XCircle } from "lucide-react"; + +import { cn } from "@/shared/lib/utils"; +import type { Application } from "@/types"; + +interface AgreementsSectionProps { + application: Application; +} + +function AgreementItem({ + label, + checked, + required = false, +}: { + label: string; + checked: boolean; + required?: boolean; +}) { + return ( +
+ {checked ? ( + + ) : ( + + )} + + {label} + {required && *} + +
+ ); +} + +export function AgreementsSection({ application }: AgreementsSectionProps) { + return ( +
+

+ Agreements & Acknowledgments +

+
+ + + + +
+
+ ); +} diff --git a/client/web/src/pages/admin/all-applicants/components/detail-sections/index.ts b/client/web/src/pages/admin/all-applicants/components/detail-sections/index.ts index bd1b0417..5590a804 100644 --- a/client/web/src/pages/admin/all-applicants/components/detail-sections/index.ts +++ b/client/web/src/pages/admin/all-applicants/components/detail-sections/index.ts @@ -1,3 +1,4 @@ +export { AgreementsSection } from "./AgreementsSection"; export { DemographicsSection } from "./DemographicsSection"; export { EducationSection } from "./EducationSection"; export { EventPreferencesSection } from "./EventPreferencesSection"; diff --git a/client/web/src/pages/admin/reviews/components/ApplicationDetailsPanel.tsx b/client/web/src/pages/admin/reviews/components/ApplicationDetailsPanel.tsx index ce64c2ae..ba50a367 100644 --- a/client/web/src/pages/admin/reviews/components/ApplicationDetailsPanel.tsx +++ b/client/web/src/pages/admin/reviews/components/ApplicationDetailsPanel.tsx @@ -1,4 +1,4 @@ -import { ExternalLink, Loader2 } from "lucide-react"; +import { CheckCircle2, ExternalLink, Loader2, XCircle } from "lucide-react"; import { useCallback, useState } from "react"; import { toast } from "sonner"; @@ -6,11 +6,41 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { errorAlert } from "@/shared/lib/api"; +import { cn } from "@/shared/lib/utils"; import type { Application } from "@/types"; import { fetchApplicationResumeURL } from "../../all-applicants/api"; import type { Review } from "../types"; +function AgreementItem({ + label, + checked, + required = false, +}: { + label: string; + checked: boolean; + required?: boolean; +}) { + return ( +
+ {checked ? ( + + ) : ( + + )} + + {label} + {required && *} + +
+ ); +} + interface ApplicationDetailsPanelProps { application: Application; selectedReview: Review; @@ -284,6 +314,34 @@ export function ApplicationDetailsPanel({ )} + {/* Agreements & Acknowledgments */} +
+

+ Agreements & Acknowledgments +

+
+ + + + +
+
+ {/* Timeline */}

Timeline

diff --git a/client/web/src/pages/hacker/apply/api.ts b/client/web/src/pages/hacker/apply/api.ts index 29a1e0d3..a60582a1 100644 --- a/client/web/src/pages/hacker/apply/api.ts +++ b/client/web/src/pages/hacker/apply/api.ts @@ -93,3 +93,46 @@ export async function uploadResumeToSignedURL( } export { MAX_RESUME_SIZE_BYTES }; + +// University Search API (Hipo) +export interface University { + name: string; + country: string; + alpha_two_code: string; + domains: string[]; + web_pages: string[]; + state_province: string | null; +} + +const HIPO_API_BASE = "https://universities.hipolabs.com"; +const universityCache = new Map(); + +//Search universities via Hipo API +export async function searchUniversities(query: string): Promise { + if (query.length < 2) return []; + + const cacheKey = query.toLowerCase(); + if (universityCache.has(cacheKey)) { + return universityCache.get(cacheKey)!; + } + + try { + const params = new URLSearchParams({ name: query }); + const response = await fetch(`${HIPO_API_BASE}/search?${params}`); + + if (!response.ok) return []; + + const universities: University[] = await response.json(); + + // Cache results (limit cache size) + if (universityCache.size > 100) { + const firstKey = universityCache.keys().next().value; + if (firstKey) universityCache.delete(firstKey); + } + universityCache.set(cacheKey, universities); + + return universities; + } catch { + return []; + } +} diff --git a/client/web/src/pages/hacker/apply/components/SelectWithOther.tsx b/client/web/src/pages/hacker/apply/components/SelectWithOther.tsx new file mode 100644 index 00000000..40ea3f54 --- /dev/null +++ b/client/web/src/pages/hacker/apply/components/SelectWithOther.tsx @@ -0,0 +1,123 @@ +import * as React from "react"; + +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface Option { + value: string; + label: string; +} + +interface SelectWithOtherProps { + options: Option[]; + value?: string; + onChange?: (value: string) => void; + placeholder?: string; + otherPlaceholder?: string; + disabled?: boolean; +} + +export function SelectWithOther({ + options, + value, + onChange, + placeholder = "Select an option", + otherPlaceholder = "Please specify...", + disabled, +}: SelectWithOtherProps) { + // Check if current value is a predefined option + const isPredefinedValue = options.some((opt) => opt.value === value); + + // Determine initial state: show input if value exists but isn't a predefined option + const getInitialOtherMode = () => { + if (!value) return false; + if (value === "other") return true; + return !isPredefinedValue; + }; + + const [isOtherMode, setIsOtherMode] = React.useState(getInitialOtherMode); + const [customValue, setCustomValue] = React.useState( + value && !isPredefinedValue && value !== "other" ? value : "", + ); + + // Only sync from props on mount - intentionally empty deps to run once + const initializedRef = React.useRef(false); + React.useEffect(() => { + if (!initializedRef.current) { + initializedRef.current = true; + if (value && !isPredefinedValue && value !== "other") { + setIsOtherMode(true); + setCustomValue(value); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleSelectChange = (newValue: string) => { + if (newValue === "other") { + setIsOtherMode(true); + setCustomValue(""); + onChange?.(""); // Clear value until they type + } else { + setIsOtherMode(false); + setCustomValue(""); + onChange?.(newValue); + } + }; + + const handleOtherInputChange = (e: React.ChangeEvent) => { + const newCustomValue = e.target.value; + setCustomValue(newCustomValue); + onChange?.(newCustomValue); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + // Blur the input to indicate the value is saved + (e.target as HTMLInputElement).blur(); + } + }; + + // Determine what to show in the select + const selectValue = isOtherMode ? "other" : value || ""; + + return ( +
+ + + {isOtherMode && ( + + )} +
+ ); +} diff --git a/client/web/src/pages/hacker/apply/components/UniversityComboBox.tsx b/client/web/src/pages/hacker/apply/components/UniversityComboBox.tsx new file mode 100644 index 00000000..aae87ca9 --- /dev/null +++ b/client/web/src/pages/hacker/apply/components/UniversityComboBox.tsx @@ -0,0 +1,152 @@ +import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/shared/lib/utils"; + +import { searchUniversities } from "../api"; + +interface UniversityComboboxProps { + value?: string; + onChange?: (value: string) => void; + placeholder?: string; + disabled?: boolean; +} + +export function UniversityCombobox({ + value, + onChange, + placeholder = "Select university...", + disabled, +}: UniversityComboboxProps) { + const [open, setOpen] = React.useState(false); + const [searchQuery, setSearchQuery] = React.useState(""); + const [universities, setUniversities] = React.useState([]); + const [loading, setLoading] = React.useState(false); + + // Debounced search + React.useEffect(() => { + if (searchQuery.length < 2) { + setUniversities([]); + return; + } + + const timeoutId = setTimeout(async () => { + setLoading(true); + try { + const results = await searchUniversities(searchQuery); + const names = [...new Set(results.map((u) => u.name))]; + setUniversities(names.slice(0, 50)); + } finally { + setLoading(false); + } + }, 300); + + return () => clearTimeout(timeoutId); + }, [searchQuery]); + + const handleSelect = (selectedValue: string) => { + onChange?.(selectedValue); + setOpen(false); + setSearchQuery(""); + }; + + return ( + + + + + + + + + {loading ? ( +
+ + + Searching... + +
+ ) : ( + <> + +

+ {searchQuery.length < 2 + ? "Type at least 2 characters to search..." + : "No universities found."} +

+
+ + {universities.map((university) => ( + handleSelect(university)} + > + + {university} + + ))} + + {searchQuery.length >= 2 && + !universities.includes(searchQuery) && ( + + handleSelect(searchQuery)} + > + + Use "{searchQuery}" + + + )} + + )} +
+
+
+
+ ); +} diff --git a/client/web/src/pages/hacker/apply/steps/ExperienceStep.tsx b/client/web/src/pages/hacker/apply/steps/ExperienceStep.tsx index c33f7f76..e1ae862b 100644 --- a/client/web/src/pages/hacker/apply/steps/ExperienceStep.tsx +++ b/client/web/src/pages/hacker/apply/steps/ExperienceStep.tsx @@ -16,6 +16,7 @@ import { SelectValue, } from "@/components/ui/select"; +import { SelectWithOther } from "../components/SelectWithOther"; import type { ApplicationFormData } from "../validations"; import { EXPERIENCE_LEVEL_OPTIONS, HEARD_ABOUT_OPTIONS } from "../validations"; @@ -87,20 +88,15 @@ export function ExperienceStep() { render={({ field }) => ( Where did you hear about this event? * - + + + )} diff --git a/client/web/src/pages/hacker/apply/steps/PersonalInfoStep.tsx b/client/web/src/pages/hacker/apply/steps/PersonalInfoStep.tsx index f1183b15..6d07a321 100644 --- a/client/web/src/pages/hacker/apply/steps/PersonalInfoStep.tsx +++ b/client/web/src/pages/hacker/apply/steps/PersonalInfoStep.tsx @@ -8,6 +8,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { PhoneInput } from "@/components/ui/phone-input"; import { Select, SelectContent, @@ -16,6 +17,7 @@ import { SelectValue, } from "@/components/ui/select"; +import { SelectWithOther } from "../components/SelectWithOther"; import type { ApplicationFormData } from "../validations"; import { COUNTRY_OPTIONS, @@ -89,12 +91,13 @@ export function PersonalInfoStep({ userEmail }: PersonalInfoStepProps) { Phone Number * - + -

- Include country code (e.g., +1 for US) -

)} /> @@ -127,20 +130,15 @@ export function PersonalInfoStep({ userEmail }: PersonalInfoStepProps) { render={({ field }) => ( Country of Residence * - + + + )} @@ -152,20 +150,15 @@ export function PersonalInfoStep({ userEmail }: PersonalInfoStepProps) { render={({ field }) => ( Gender * - + + + )} diff --git a/client/web/src/pages/hacker/apply/steps/ReviewStep.tsx b/client/web/src/pages/hacker/apply/steps/ReviewStep.tsx index 57391783..edf4837a 100644 --- a/client/web/src/pages/hacker/apply/steps/ReviewStep.tsx +++ b/client/web/src/pages/hacker/apply/steps/ReviewStep.tsx @@ -136,7 +136,7 @@ export function ReviewStep({ onEdit={onEditStep} > - + Which university do you attend? * - + @@ -50,9 +49,12 @@ export function SchoolInfoStep() { name="major" render={({ field }) => ( - What is your major? * + Field(s) of study * - + @@ -65,20 +67,15 @@ export function SchoolInfoStep() { render={({ field }) => ( Current level of study * - + + + )}