mirror of
https://github.com/Leado123/Leado123.github.io.git
synced 2026-10-01 00:32:59 +00:00
Delete App.tsx
This commit is contained in:
@@ -1,360 +0,0 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react'
|
|
||||||
|
|
||||||
import SyllabusListing, { ClassListing } from '../../components/syllabus_comp';
|
|
||||||
|
|
||||||
import { Accordion, FileInput, } from "flowbite-react";
|
|
||||||
|
|
||||||
import Select from "react-select"
|
|
||||||
import AsyncSelect from "react-select/async";
|
|
||||||
|
|
||||||
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import { isMobile } from "is-mobile";
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { School } from '../UploadSyllabi';
|
|
||||||
import { AmbiguousClass } from '../../welcome';
|
|
||||||
|
|
||||||
interface Filter {
|
|
||||||
school?: string;
|
|
||||||
professor?: string;
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [openModal, setOpenModal] = useState(false);
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const [accordionOpen, setAccordionOpen] = useState(false);
|
|
||||||
const [tempFileId, setTempFileId] = useState<string>("");
|
|
||||||
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
|
|
||||||
const [filter, setFilter] = useState<Filter>({});
|
|
||||||
|
|
||||||
const [syllabi, setSyllabi] = useState<ClassListing[]>([]);
|
|
||||||
const [query, setQuery] = useState<string>("");
|
|
||||||
|
|
||||||
const [debouncedQuery, setDebouncedQuery] = useState(query);
|
|
||||||
|
|
||||||
const { college, professor, class: classParam } = useParams();
|
|
||||||
|
|
||||||
const [collegeOptions, setCollegeOptions] = useState<School[]>([]);
|
|
||||||
const [initialSearchPerformed, setInitialSearchPerformed] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (college) {
|
|
||||||
setFilter({ ...filter, school: college });
|
|
||||||
}
|
|
||||||
if (professor) {
|
|
||||||
setFilter({ ...filter, professor: professor });
|
|
||||||
}
|
|
||||||
if (classParam) {
|
|
||||||
setFilter({ ...filter, class: classParam });
|
|
||||||
}
|
|
||||||
}, [college, professor, classParam])
|
|
||||||
|
|
||||||
// Handle query changes with debounce
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setDebouncedQuery(query);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
// Trigger search when debouncedQuery changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialSearchPerformed) {
|
|
||||||
handleSearch();
|
|
||||||
}
|
|
||||||
}, [debouncedQuery]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleSearch();
|
|
||||||
setInitialSearchPerformed(true);
|
|
||||||
}, [filter]);
|
|
||||||
|
|
||||||
async function handleSearch() {
|
|
||||||
if (query === "admin") {
|
|
||||||
navigate('admin');
|
|
||||||
}
|
|
||||||
|
|
||||||
let end = ``;
|
|
||||||
if (filter.school) end += `&s=${filter.school}`;
|
|
||||||
if (filter.professor) end += `&p=${filter.professor}`;
|
|
||||||
if (filter.class) end += `&c=${filter.class}`;
|
|
||||||
try {
|
|
||||||
var response = await fetch(`${import.meta.env.VITE_SERVER_ADDRESS}/search/?q=${debouncedQuery}${end}`);
|
|
||||||
var data = await response.json();
|
|
||||||
setSyllabi(data);
|
|
||||||
} catch {
|
|
||||||
console.error("error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCloseModal() {
|
|
||||||
setOpenModal(false);
|
|
||||||
setEmail('');
|
|
||||||
setName('');
|
|
||||||
setFile(null);
|
|
||||||
setAccordionOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadFile() {
|
|
||||||
let formdata = new FormData();
|
|
||||||
|
|
||||||
if (file === null) {
|
|
||||||
alert("File not uploaded");
|
|
||||||
throw new Error("File not uploaded");
|
|
||||||
}
|
|
||||||
formdata.append("upload", file);
|
|
||||||
formdata.append("email", email);
|
|
||||||
formdata.append("name", name);
|
|
||||||
fetch(`${import.meta.env.VITE_SERVER_ADDRESS}/create`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formdata,
|
|
||||||
})
|
|
||||||
.then(async (response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Server error: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
setAccordionOpen(true);
|
|
||||||
let data = await response.json();
|
|
||||||
setTempFileId(data.fileId);
|
|
||||||
setOpenModal(false);
|
|
||||||
alert("Upload successful! You will receive an email with your volunteer hours within 3 days.");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Upload failed:", error);
|
|
||||||
alert("Upload failed: " + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
}, [tempFileId])
|
|
||||||
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch(`${import.meta.env.VITE_SERVER_ADDRESS}/schools`)
|
|
||||||
.then(async (response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Server error: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
let data = await response.json();
|
|
||||||
setCollegeOptions(data);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to fetch schools:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
[location])
|
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (e.target.files === null) return
|
|
||||||
if (e.target.files[0] === null) return
|
|
||||||
|
|
||||||
setFile(e.target.files[0]);
|
|
||||||
setAccordionOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
handleSearch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" />
|
|
||||||
{isMobile() === false &&
|
|
||||||
<div className="w-full flex place-items-center justify-center pt-1">
|
|
||||||
<div className="w-3/4 flex justify-center items-center">
|
|
||||||
|
|
||||||
<div className="place-items-center justify-center flex w-full flex-col">
|
|
||||||
<div className="w-full flex space-y-2 flex-col">
|
|
||||||
<div className="flex pt-2">
|
|
||||||
<div className="flex-1 flex border border-gray-400 focus:outline-none rounded-lg">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="search, hit enter or the search button to enter query"
|
|
||||||
className="focus:outline-none placeholder:text-gray-500 font-normal focus:ring-0 bg-transparent border-none rounded-lg flex-1 text-black"
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}>
|
|
||||||
</input>
|
|
||||||
<button className="aspect-square p-2 flex justify-center items-center" onClick={handleSearch}>
|
|
||||||
<span className="material-symbols-rounded text-black">
|
|
||||||
search
|
|
||||||
</span>
|
|
||||||
</button></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 h-max gap-2">
|
|
||||||
<div className="text-left w-1/4 rounded-b-none rounded-lg min-h-screen">
|
|
||||||
<div className="flex gap-2 flex-col">
|
|
||||||
<Select
|
|
||||||
className="p-1 text-md bg-[#fafdfb] border-gray-400 border rounded-md"
|
|
||||||
placeholder="School District"
|
|
||||||
options={collegeOptions}
|
|
||||||
getOptionLabel={(option: School) => option.name}
|
|
||||||
getOptionValue={(option: School) => option.name}
|
|
||||||
value={filter.school ? collegeOptions.find(option => option.name === filter.school) : undefined}
|
|
||||||
onChange={(selectedOption: School | null) => {
|
|
||||||
if (selectedOption) {
|
|
||||||
setFilter({ ...filter, school: selectedOption.name });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
isSearchable={true}
|
|
||||||
components={{
|
|
||||||
DropdownIndicator: (props) => (
|
|
||||||
<div {...props.innerProps} onClick={() => {
|
|
||||||
props.selectOption({ id: "", name: "No Filter", fullName: "No Filter" });
|
|
||||||
setFilter({ ...filter, school: undefined });
|
|
||||||
}}>
|
|
||||||
<span className="material-symbols-rounded text-red-700">close</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
styles={{
|
|
||||||
control: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
backgroundColor: '#fafdfb',
|
|
||||||
border: 'none',
|
|
||||||
boxShadow: 'none',
|
|
||||||
}),
|
|
||||||
input: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
boxShadow: 'none',
|
|
||||||
}),
|
|
||||||
placeholder: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
color: '#888',
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AsyncSelect
|
|
||||||
className="p-1 text-md bg-[#fafdfb] border-gray-400 border rounded-md "
|
|
||||||
placeholder="Course Name: ENG-1A, General Psych, etc."
|
|
||||||
value={filter.class ? { className: filter.class, school: { name: '' } } as AmbiguousClass : undefined}
|
|
||||||
styles={{
|
|
||||||
control: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
backgroundColor: '#fafdfb',
|
|
||||||
border: 'none',
|
|
||||||
boxShadow: 'none',
|
|
||||||
}),
|
|
||||||
input: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
boxShadow: 'none',
|
|
||||||
}),
|
|
||||||
placeholder: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
color: '#888',
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
loadOptions={async (inputValue) => {
|
|
||||||
if (!inputValue) return [];
|
|
||||||
let responses = await Promise.all(
|
|
||||||
collegeOptions.map(async (college) => {
|
|
||||||
const res = await fetch(`${import.meta.env.VITE_SERVER_ADDRESS}/search/class/?q=${inputValue}&s=${college.name}`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
return data.map((item: AmbiguousClass) => ({
|
|
||||||
...item,
|
|
||||||
school: college,
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add an option with no class
|
|
||||||
responses.unshift([{ className: "", school: { name: "No Class" } }]);
|
|
||||||
return responses.flat();
|
|
||||||
}}
|
|
||||||
formatOptionLabel={(option: AmbiguousClass) => (
|
|
||||||
<div>
|
|
||||||
<div>{option.className}</div>
|
|
||||||
<div style={{ fontSize: '0.8em', color: '#888' }}>{option.school.name}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
getOptionValue={(option: AmbiguousClass) => option.className}
|
|
||||||
onChange={(selectedOption: AmbiguousClass | null) => {
|
|
||||||
if (selectedOption) {
|
|
||||||
setFilter({ ...filter, class: selectedOption.className });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
isSearchable={true}
|
|
||||||
cacheOptions
|
|
||||||
defaultOptions
|
|
||||||
components={{
|
|
||||||
DropdownIndicator: (props) => (
|
|
||||||
<div {...props.innerProps} onClick={() => {
|
|
||||||
setFilter({ ...filter, class: "" });
|
|
||||||
props.selectOption({ id: 0, className: "", fullClassName: "", disciplineId: "", school: { id: "", name: "No Class", fullName: "No Class" } });
|
|
||||||
}}>
|
|
||||||
<span className="material-symbols-rounded text-red-700">close</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{filter.professor && <div className="flex p-2 bg-black text-white rounded-lg gap-2">
|
|
||||||
<span className="text-md font-bold">Professor:</span>
|
|
||||||
<span className="text-md">{filter.professor}</span>
|
|
||||||
<button className="text-md text-red-700" onClick={() => setFilter({ ...filter, professor: undefined })}>Clear</button>
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1">
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{syllabi.map((syllabus, i) => {
|
|
||||||
return <SyllabusListing key={i} school={syllabus.school} fileName={syllabus.fileName} professorId={syllabus.professorId} className={syllabus.class?.className} description={syllabus.description} fullClassName={syllabus.class?.fullClassName} textbookCost={syllabus.textbookCost} classLength={syllabus.classLength} professor={syllabus.professor} />
|
|
||||||
})}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
{isMobile() === true && <div className="p-1 pt-2">
|
|
||||||
|
|
||||||
<div className="">
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
|
|
||||||
<div className="flex-1 flex bg-white rounded-lg">
|
|
||||||
<input type="search" onKeyDown={handleKeyDown} placeholder="search class or professor" className="focus:outline-none w-4/5 bg-transparent border-none flex-1 text-black" value={query} onChange={(e) => setQuery(e.target.value)}>
|
|
||||||
|
|
||||||
</input>
|
|
||||||
<button className="aspect-square flex justify-center bg-white rounded-lg items-center" onClick={handleSearch}>
|
|
||||||
<span className="material-symbols-rounded text-black">
|
|
||||||
search
|
|
||||||
</span>
|
|
||||||
</button></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="listing">
|
|
||||||
{syllabi.map((syllabus, i) => {
|
|
||||||
return <SyllabusListing key={i} fileName={syllabus.fileName} className={syllabus.className} fullClassName={syllabus.fullClassName} textbookCost={syllabus.textbookCost} description={syllabus.description} classLength={syllabus.classLength} professor={syllabus.professor} />
|
|
||||||
})}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
Reference in New Issue
Block a user