diff --git a/app/student/register/steps/RegisterStep.tsx b/app/student/register/steps/RegisterStep.tsx index 157ea516..39f7e70f 100644 --- a/app/student/register/steps/RegisterStep.tsx +++ b/app/student/register/steps/RegisterStep.tsx @@ -5,7 +5,7 @@ import { UseFormReturn } from "react-hook-form"; import { FormInputs } from "../page"; import { Autocomplete } from "@/components/ui/autocomplete"; import { DEGREES } from "./tempDegrees"; -import { sortUniversityOptions } from "../../../../lib/student-forms-access"; +import { sortUniversityOptions, universityAcronyms } from "../../../../lib/student-forms-access"; import { Accordion, AccordionContent, @@ -38,7 +38,13 @@ export function RegisterStep({ const hasValidUniversity = refs.universities.some( (option) => option.id === university, ); - const universityOptions = sortUniversityOptions(refs.universities); + const universityOptions = sortUniversityOptions(refs.universities).map( + (u) => ({ + id: u.id, + name: u.name, + keywords: universityAcronyms(u.name), + }), + ); const canCreateAccount = firstName.trim() && lastName.trim() && hasValidUniversity && degree.trim(); @@ -78,7 +84,7 @@ export function RegisterStep({ setter={(val) => { regForm.setValue("university", val === null ? "" : String(val)); }} - options={universityOptions as { id: string; name: string }[]} + options={universityOptions} value={university} required={true} preserveOptionOrder={true} diff --git a/components/features/hire/paused-listings-banner.tsx b/components/features/hire/paused-listings-banner.tsx index f1193d99..b202f724 100644 --- a/components/features/hire/paused-listings-banner.tsx +++ b/components/features/hire/paused-listings-banner.tsx @@ -47,9 +47,7 @@ export function PausedListingsBanner({
- - {pausedCount} of your listing{pausedCount !== 1 ? "s" : ""} - {" "} + {pausedCount} of your listings{" "} {pausedCount !== 1 ? "are" : "is"} marked inactive {waitingTotal > 0 && ( <> diff --git a/components/features/student/profile/ProfileEditor.tsx b/components/features/student/profile/ProfileEditor.tsx index e91564b5..b3aed3a9 100644 --- a/components/features/student/profile/ProfileEditor.tsx +++ b/components/features/student/profile/ProfileEditor.tsx @@ -39,7 +39,7 @@ import { isValidOptionalURL, toURL, } from "@/lib/utils/url-utils"; -import { sortUniversityOptions } from "@/lib/student-forms-access"; +import { sortUniversityOptions, universityAcronyms } from "@/lib/student-forms-access"; import { DEGREES } from "@/app/student/register/steps/tempDegrees"; import { ResumeSection } from "./ResumeSection"; import type { ProfileResumeManager, ProfileSectionKey } from "./profile-types"; @@ -485,7 +485,13 @@ export const ProfileEditor = forwardRef<
({ + id: u.id, + name: u.name, + keywords: universityAcronyms(u.name), + }), + )} value={formData.university} setter={fieldSetter("university")} placeholder="Select University" diff --git a/components/ui/autocomplete.tsx b/components/ui/autocomplete.tsx index 1dc92583..686ba7bd 100644 --- a/components/ui/autocomplete.tsx +++ b/components/ui/autocomplete.tsx @@ -12,6 +12,7 @@ import { useAppContext } from "@/lib/ctx-app"; export interface IAutocompleteOption { id: ID; name: string; + keywords?: string[]; } type MobileDropdownMode = "sheet" | "inline"; @@ -70,15 +71,21 @@ function AutocompleteBase({ const findExactOption = (value: string) => { const normalizedValue = value.trim().toLowerCase(); return options.find( - (o) => o.name?.trim().toLowerCase() === normalizedValue, + (o) => + o.name?.trim().toLowerCase() === normalizedValue || + o.keywords?.some((k) => k === normalizedValue), ); }; - const resolveQuerySelection = (nextQuery: string) => { + const resolveQuerySelection = (nextQuery: string, matchKeywords = true) => { const text = nextQuery.trim(); if (!text) return false; - const exact = findExactOption(text); + const exact = matchKeywords + ? findExactOption(text) + : options.find( + (o) => o.name?.trim().toLowerCase() === text.toLowerCase(), + ); if (exact) { setter([exact.id]); setQuery(""); @@ -99,7 +106,11 @@ function AutocompleteBase({ const filtered = useMemo(() => { const q = query.trim().toLowerCase(); const base = q - ? options.filter((o) => o.name?.toLowerCase().includes(q)) + ? options.filter( + (o) => + o.name?.toLowerCase().includes(q) || + o.keywords?.some((k) => k.includes(q)), + ) : options; return preserveOptionOrder ? base @@ -354,7 +365,12 @@ function AutocompleteBase({ } const exact = findExactOption(nextQuery); - if (exact) { + // Only auto-fill on an exact NAME match; keyword/acronym matches + // (e.g. "dlsu") require an explicit Enter, blur, or click. + if ( + exact && + exact.name.trim().toLowerCase() === nextQuery.trim().toLowerCase() + ) { setter([exact.id]); setQuery(""); } else { @@ -371,7 +387,7 @@ function AutocompleteBase({ } }} onKeyDown={(e) => { - if (e.key === "Enter" && resolveQuerySelection(query)) { + if (e.key === "Enter" && resolveQuerySelection(query, true)) { setIsOpen(false); e.preventDefault(); } @@ -382,7 +398,7 @@ function AutocompleteBase({ if (Date.now() - lastSelectionRef.current < 250) { return; } - resolveQuerySelection(query); + resolveQuerySelection(query, false); }, 150); }} /> diff --git a/lib/student-forms-access.ts b/lib/student-forms-access.ts index d6a47dcf..56d5fb65 100644 --- a/lib/student-forms-access.ts +++ b/lib/student-forms-access.ts @@ -25,3 +25,49 @@ export const sortUniversityOptions = (universities: University[]) => { return 0; }); }; + +const ACRONYM_STOPWORDS = new Set([ + "a", + "an", + "and", + "at", + "for", + "in", + "of", + "on", + "or", + "the", + "to", + "with", + "&", +]); + +/** + * Generates candidate search acronyms for a university name so that queries + * like "dlsu" can match "De La Salle University". + * + * Returns two variants: + * - strict: initials of capitalized words only ("Ateneo de Manila University" -> "amu") + * - loose: also includes meaningful lowercase connectors like "de" ("admu") + * + * @param name The university name. + * @returns Unique, lowercased acronym candidates (may be empty). + */ +export const universityAcronyms = (name: string): string[] => { + const parts = name.split(/[\s-]+/).filter((p) => p.length > 0); + const strict = parts + .filter((p) => p[0] === p[0].toUpperCase()) + .map((p) => p[0].toUpperCase()) + .join(""); + const loose = parts + .filter((p) => { + const first = p[0]; + if (first === first.toUpperCase()) return true; + return !ACRONYM_STOPWORDS.has(p.toLowerCase()); + }) + .map((p) => p[0].toUpperCase()) + .join(""); + return Array.from( + new Set([strict, loose].filter(Boolean).map((a) => a.toLowerCase())), + ); +};