Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions app/student/register/steps/RegisterStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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}
Expand Down
4 changes: 1 addition & 3 deletions components/features/hire/paused-listings-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ export function PausedListingsBanner({
<div className="flex items-center">
<AlertTriangle className="w-5 h-5 mr-2 shrink-0" />
<span>
<span className="font-medium">
{pausedCount} of your listing{pausedCount !== 1 ? "s" : ""}
</span>{" "}
<span className="font-medium">{pausedCount} of your listings</span>{" "}
{pausedCount !== 1 ? "are" : "is"} marked inactive
{waitingTotal > 0 && (
<>
Expand Down
10 changes: 8 additions & 2 deletions components/features/student/profile/ProfileEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -485,7 +485,13 @@ export const ProfileEditor = forwardRef<
<div className="mt-3 flex flex-col space-y-3">
<EditFieldRow label="University">
<Autocomplete
options={sortUniversityOptions(universityOptions)}
options={sortUniversityOptions(universityOptions).map(
(u) => ({
id: u.id,
name: u.name,
keywords: universityAcronyms(u.name),
}),
)}
value={formData.university}
setter={fieldSetter("university")}
placeholder="Select University"
Expand Down
30 changes: 23 additions & 7 deletions components/ui/autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useAppContext } from "@/lib/ctx-app";
export interface IAutocompleteOption<ID extends number | string> {
id: ID;
name: string;
keywords?: string[];
}

type MobileDropdownMode = "sheet" | "inline";
Expand Down Expand Up @@ -70,15 +71,21 @@ function AutocompleteBase<ID extends number | string>({
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("");
Expand All @@ -99,7 +106,11 @@ function AutocompleteBase<ID extends number | string>({
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
Expand Down Expand Up @@ -354,7 +365,12 @@ function AutocompleteBase<ID extends number | string>({
}

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 {
Expand All @@ -371,7 +387,7 @@ function AutocompleteBase<ID extends number | string>({
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" && resolveQuerySelection(query)) {
if (e.key === "Enter" && resolveQuerySelection(query, true)) {
setIsOpen(false);
e.preventDefault();
}
Expand All @@ -382,7 +398,7 @@ function AutocompleteBase<ID extends number | string>({
if (Date.now() - lastSelectionRef.current < 250) {
return;
}
resolveQuerySelection(query);
resolveQuerySelection(query, false);
}, 150);
}}
/>
Expand Down
46 changes: 46 additions & 0 deletions lib/student-forms-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())),
);
};