diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx new file mode 100644 index 0000000..032267b --- /dev/null +++ b/src/app/settings/page.tsx @@ -0,0 +1,262 @@ +/*************************************************************** + * + * page.tsx + * + * Author: Will and Hansini + * Date: 12/6/2025 + * + * Summary: Basic outline of settings page + * + **************************************************************/ + +"use client"; + +import { useState } from "react"; +import { MultiSelect } from "../../components/ui/multi-select"; +import { Trash, Plus } from "lucide-react"; + +export default function Settings() { + const [selectedCities, setSelectedCities] = useState([]); + + // Placeholder city options, eventually will be pulled from db + const cityOptions = [ + { value: "city-1", label: "City 1" }, + { value: "city-2", label: "City 2" }, + { value: "city-3", label: "City 3" }, + { value: "city-4", label: "City 4" }, + { value: "city-5", label: "City 5" }, + { value: "city-6", label: "City 6" }, + { value: "city-7", label: "City 7" }, + { value: "city-8", label: "City 8" }, + ]; + + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + + const handleCityChange = (values: string[]) => { + setSelectedCities(values); + setHasUnsavedChanges(true); + }; + + const handleSave = () => { + console.log("Saving cities:", selectedCities); + setHasUnsavedChanges(false); + }; + + const handleDeleteCity = (cityValue: string) => { + setSelectedCities((prevCities) => + prevCities.filter((value) => value !== cityValue), + ); + setHasUnsavedChanges(true); + }; + + const getCityLabel = (value: string) => { + return cityOptions.find((opt) => opt.value === value)?.label || value; + }; + + return ( +
+
+

Settings

+
+ +
+

Preferences

+

+ How would you like to view charts +

+
+ +
+
+

Configuration

+

+ These settings configure how data is calculated. Only + edit these settings if you really mean to. +

+
+ +
+
+
+

+ Gateway Cities +

+
+
+ +
+ {selectedCities.length > 0 && ( +

+ {selectedCities.length}{" "} + {selectedCities.length === 1 + ? "city" + : "cities"}{" "} + selected +

+ )} +
+ + + + + + + + + + {selectedCities.map((cityValue) => ( + + + + + ))} + +
+ City + + Actions +
+ {getCityLabel(cityValue)} + + +
+
+
+ + {/* Permitted Users Section */} +
+
+

+ Permitted Users +

+
+ +

+ These emails are permitted to sign into the + platform. Here you can also revoke access +

+
+ + +
+ +
+ + + + + + + + + + {[ + { + email: "something@gmail.com", + lastSignIn: "2 days ago", + }, + { + email: "something@gmail.com", + lastSignIn: "2 days ago", + }, + { + email: "something@gmail.com", + lastSignIn: "2 days ago", + }, + ].map((user, i) => ( + + + + + + ))} + +
+ Email + + Last Signed In + + Remove +
+ {user.email} + + {user.lastSignIn} + + +
+
+
+
+ + {/* Save Section */} +
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 37b59a4..8ea630d 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -5,6 +5,7 @@ export default function NavBar() { ); diff --git a/src/components/ui/multi-select.tsx b/src/components/ui/multi-select.tsx new file mode 100644 index 0000000..8752a32 --- /dev/null +++ b/src/components/ui/multi-select.tsx @@ -0,0 +1,196 @@ +import React from "react"; +import { CheckIcon, ChevronDown, XIcon } from "lucide-react"; + +export interface MultiSelectOption { + label: string; + value: string; +} + +export interface MultiSelectProps { + options: MultiSelectOption[]; + value?: string[]; + defaultValue?: string[]; + onValueChange: (v: string[]) => void; + placeholder?: string; + className?: string; + disabled?: boolean; + searchable?: boolean; +} + +export const MultiSelect: React.FC = ({ + options, + value, + defaultValue = [], + onValueChange, + placeholder = "Select options", + className, + disabled = false, + searchable = true, +}) => { + const isControlled = value !== undefined; + const [internal, setInternal] = React.useState(defaultValue); + const selected = isControlled ? value! : internal; + + const [open, setOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); + + const triggerRef = React.useRef(null); + + const setSelected = (vals: string[]) => { + if (!isControlled) setInternal(vals); + onValueChange(vals); + }; + + const toggle = (val: string) => { + if (selected.includes(val)) { + setSelected(selected.filter((x) => x !== val)); + } else { + setSelected([...selected, val]); + } + }; + + const clear = () => setSelected([]); + + const filtered = search + ? options.filter((o) => + o.label.toLowerCase().includes(search.toLowerCase()), + ) + : options; + + // Close dropdown when clicking outside + React.useEffect(() => { + const handler = (e: MouseEvent) => { + if ( + triggerRef.current && + !triggerRef.current.contains(e.target as Node) + ) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, []); + + return ( +
+ {/* Trigger */} +
!disabled && setOpen(!open)} + className={` + border rounded px-3 py-2 flex items-center justify-between cursor-pointer + ${disabled ? "opacity-50 cursor-not-allowed" : ""} + `} + > + {/* Selected Items */} + {selected.length === 0 ? ( + {placeholder} + ) : ( +
+ {selected.map((val) => { + const opt = options.find((o) => o.value === val); + if (!opt) return null; + + return ( + e.stopPropagation()} + > + {opt.label} + toggle(val)} + /> + + ); + })} +
+ )} + + +
+ + {/* Dropdown */} + {open && ( +
+ {/* Search */} + {searchable && ( + setSearch(e.target.value)} + placeholder="Search..." + className="w-full border rounded px-2 py-1 mb-2" + /> + )} + +
+ {/* Select All */} +
+ selected.length === options?.length + ? clear() + : setSelected(options?.map((o) => o.value)) + } + className="flex items-center gap-2 px-2 py-1 hover:bg-gray-100 cursor-pointer" + > +
+ +
+ Select All +
+ + {/* Options */} + {filtered?.map((o) => { + const isSelected = selected.includes(o.value); + + return ( +
toggle(o.value)} + > +
+ +
+ {o.label} +
+ ); + })} + +
+ + {/* Clear */} + {selected.length > 0 && ( +
+ Clear +
+ )} + + {/* Close */} +
setOpen(false)} + > + Close +
+
+
+ )} +
+ ); +}; diff --git a/src/lib/school_name_standardize.ts b/src/lib/school_name_standardize.ts new file mode 100644 index 0000000..97bb650 --- /dev/null +++ b/src/lib/school_name_standardize.ts @@ -0,0 +1,54 @@ +/*************************************************************** + * + * school_name_standardize.ts + * + * Author: Will and Hansini + * Date: 12/6/2025 + * + * Summary: Standardizes a school's name by removing + * certain words, characters, and white space + * + **************************************************************/ + +const words_to_remove: string[] = [ + "public", + "school", + "schools", + "district", + "the", + "of", + "+", + "-", + "=", + "and", + "at", + "!", + "@", + "#", + "$", + "%", + "^", + "&", + "*", + "(", + ")", + ".", +]; + +export function standardize(name: string) { + // Trim whitespace + var school_name = name.trim(); + + // Convert to lowercase + school_name = school_name.toLowerCase(); + + const words = school_name.split(" "); + + // Filtering out extraneous words referring to the array above + // Remove the extraneous words + const filtered = words.filter((word) => { + return !words_to_remove.includes(word); + }); + + return filtered.join(""); +}