mirror of
https://github.com/Leado123/Leado123.github.io.git
synced 2026-10-01 00:32:59 +00:00
change
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import './App.css'
|
||||
import './pages/App.css'
|
||||
|
||||
import SyllabusListing, { ClassListing } from '../components/syllabus_comp';
|
||||
import SyllabusListing, { ClassListing } from './components/syllabus_comp';
|
||||
|
||||
|
||||
import server from '../main'
|
||||
import server from './main'
|
||||
|
||||
import Select from "react-select"
|
||||
import AsyncSelect from "react-select/async";
|
||||
@@ -12,8 +12,13 @@ import AsyncSelect from "react-select/async";
|
||||
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';
|
||||
import { School } from './pages/UploadSyllabi';
|
||||
import { AmbiguousClass } from './welcome';
|
||||
|
||||
export var initialQuery = "";
|
||||
export function setInitialQuery(value: string) {
|
||||
initialQuery = value;
|
||||
}
|
||||
|
||||
interface Filter {
|
||||
school?: string;
|
||||
@@ -27,6 +32,8 @@ function App() {
|
||||
|
||||
const [querying, setQuerying] = useState(false);
|
||||
|
||||
const [initialized, setIntialized] = useState(false);
|
||||
|
||||
|
||||
const [filter, setFilter] = useState<Filter>({});
|
||||
|
||||
@@ -74,6 +81,7 @@ function App() {
|
||||
}, [filter]);
|
||||
|
||||
async function handleSearch() {
|
||||
if (!initialized) return;
|
||||
if (query === "admin") {
|
||||
navigate('admin');
|
||||
}
|
||||
@@ -115,6 +123,12 @@ function App() {
|
||||
},
|
||||
[location])
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(initialQuery);
|
||||
setIntialized(true);
|
||||
handleSearch();
|
||||
console.log("initialQuery set to" + initialQuery)
|
||||
}, [])
|
||||
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
@@ -133,22 +147,22 @@ function App() {
|
||||
<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-2 border-black focus:outline-none rounded-lg">
|
||||
<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-black font-[Parkinsans] font-bold focus:ring-0 bg-transparent border-none rounded-lg flex-1 text-black"
|
||||
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 flex justify-center items-center" onClick={() => { setQuery(''); handleSearch() }}>
|
||||
<button className="aspect-square p-2 pr-0 flex justify-center items-center" onClick={() => { setQuery(''); handleSearch() }}>
|
||||
<span className="material-symbols-rounded hover:text-black text-gray-500">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
<button className="aspect-square flex justify-center items-center" onClick={handleSearch}>
|
||||
<span className="material-symbols-rounded text-black">
|
||||
<button className="aspect-square p-2 flex justify-center items-center" onClick={handleSearch}>
|
||||
<span className="material-symbols-rounded hover:text-black text-gray-500">
|
||||
search
|
||||
</span>
|
||||
</button></div>
|
||||
+11
-11
@@ -16,10 +16,10 @@ function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<div className="bg-[#fcfffd] text-[#051008] w-full h-full">
|
||||
<div className={`w-full text-left p-1 flex place-items-center justify-center border-b border-gray-300`}>
|
||||
<div className={`flex-1 gap-4 place-items-center ${!isMobile() ? "flex" : "flex flex-col"}`}>
|
||||
<div className={`w-full sticky shadow-sm top-0 left-0 backdrop-blur-md z-50 text-left p-2 flex place-items-center justify-center border-b border-gray-300`}>
|
||||
<div className={`flex-1 gap-4 place-items-center ${!isMobile() ? "flex" : "flex flex-col"}`}>
|
||||
{isMobile() ?
|
||||
<button onClick={() => navigate("/upload")} className="p-1 flex-1 rounded-full hover:drop-shadow-xl hover:bg-slate-950 font-[Parkinsans] pl-2 pr-2 font-bold bg-black">
|
||||
<button onClick={() => navigate("/upload")} className="p-1 flex-1 rounded-full hover:drop-shadow-xl hover:bg-slate-950 pl-2 pr-2 font-bold bg-black">
|
||||
<Marquee className="anim-enter-blur max-w-screen" delay={1} gradient={true} gradientWidth="10%" gradientColor="black" speed={15}>
|
||||
<div className="flex gap-1">
|
||||
<span className="bg-blue-800 pl-2 gap-2 pr-2 rounded-md flex">
|
||||
@@ -34,20 +34,20 @@ function Layout({ children }: { children: React.ReactNode }) {
|
||||
</div>
|
||||
</Marquee>
|
||||
</button> :
|
||||
<div className="flex-1 grid grid-cols-3 gap-4 place-items-center">
|
||||
<text onClick={() => navigate("/")} className="text-xl text-[#29a847] cursor-pointer hover:drop-shadow-md font-extrabold flex-1 font-bol">📚 Share My Syllabus <span className="font-light text-purple-600">beta</span></text>
|
||||
<div className="flex-1 flex place-items-center text-center">
|
||||
<div className="flex-1 grid grid-cols-3 gap-4 justify-center place-items-center">
|
||||
<text onClick={() => navigate("/")} className="text-xl bg-clip-text flex place-items-center text-transparent bg-gradient-to-tl from-purple-600 to-blue-700 cursor-pointer gap-1 hover:drop-shadow-md font-extrabold flex-1 font-bol"><img className="h-6 drop-shadow-lg" src="/logo_borderless.png"></img>Share Syllabus <span className="font-light text-purple-600"> beta</span></text>
|
||||
<div onClick={() => navigate("/upload")} className="flex-1 cursor-pointer hover:backdrop-blur-md rounded-full flex place-items-center text-center">
|
||||
<span className="material-symbols-rounded p-1 text-black">web_traffic</span>
|
||||
<button onClick={() => navigate("/upload")} className="p-1 rounded-full anim-enter-width hover:drop-shadow-xl hover:bg-slate-950 font-[Parkinsans] pl-2 pr-2 font-bold bg-black">
|
||||
<button onClick={() => navigate("/upload")} className="p-1 rounded-full anim-enter-width hover:drop-shadow-xl hover:bg-slate-950 pl-2 pr-2 font-bold bg-black">
|
||||
<Marquee className="anim-enter-blur w-40" delay={1} gradient={true} gradientWidth="10%" gradientColor="black" speed={15}>
|
||||
<div className="flex gap-1">
|
||||
<span className="bg-blue-800 pl-2 gap-2 pr-2 rounded-md flex">
|
||||
<span className="text-white font-sans">Click to Share A Syllabus!</span>
|
||||
<span className="text-white">Click to Share A Syllabus!</span>
|
||||
|
||||
</span>
|
||||
<span className="material-symbols-rounded text-white">web_traffic</span>
|
||||
<span className="bg-purple-700 pl-2 gap-2 pr-2 place-items-center rounded-md flex">
|
||||
<span className="text-white font-bold font-sans">Sharying a syllabus awards 2 volunteer hours</span>
|
||||
<span className="text-white font-bold">Sharying a syllabus awards 2 volunteer hours</span>
|
||||
</span>
|
||||
<span className="material-symbols-rounded pr-1 text-white">web_traffic</span>
|
||||
</div>
|
||||
@@ -55,8 +55,8 @@ function Layout({ children }: { children: React.ReactNode }) {
|
||||
</button>
|
||||
<span className="material-symbols-rounded p-1 text-black">add</span>
|
||||
</div>
|
||||
<div className="flex gap-4 font-light text-gray-600">
|
||||
<Link to="/moderator" className=" text-gray-600">Moderator Panel</Link>
|
||||
<div className="flex flex-row gap-4 font-light text-gray-600 items-center">
|
||||
<Link to="/moderator" className="text-gray-600">Moderator Panel</Link>
|
||||
<Link to="/blog" className="text-gray-600">Blog</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "../syllabuslisting.css";
|
||||
import "../pages/App.tsx";
|
||||
import "../App.tsx";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -99,7 +99,7 @@ function SyllabusListing({ className, school, professor, professorId, fileName,
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full border-gray-400 border flex gap-4 cursor-pointer text-[#051008] rounded-lg p-3" onClick={handleView} >
|
||||
<div className="w-full border-gray-400 bg-[rgba(255,255,255,0.7)] border flex gap-4 cursor-pointer text-[#051008] rounded-lg p-3 backdrop-blur-md" onClick={handleView} >
|
||||
<div className="flex text-left flex-col w-1/4">
|
||||
<span className="font-extrabold">{className}</span>
|
||||
<span className="text-xs text-gray-800">{fullClassName}</span>
|
||||
@@ -111,7 +111,7 @@ function SyllabusListing({ className, school, professor, professorId, fileName,
|
||||
{classLength > 7 ? <span className="text-xs font-light text-green-600">{classLength} weeks course</span> : <span className="text-xs font-light text-red-600">{classLength} weeks course</span>}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col text-center">
|
||||
<span className="w-3/4 font-serif">"{description}"</span>
|
||||
<span className="font-serif">"{description}"</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="flex-1"></span>
|
||||
|
||||
+2
-6
@@ -1,7 +1,7 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Route, Routes, HashRouter } from 'react-router-dom';
|
||||
import './index.css';
|
||||
import App from './pages/App.tsx';
|
||||
import App from './App.tsx';
|
||||
import View from './components/FileView.tsx';
|
||||
import Admin from './pages/Admin.tsx';
|
||||
import Flag from './pages/Flag.tsx';
|
||||
@@ -11,11 +11,9 @@ import Layout from './Layout.tsx';
|
||||
import UploadSyllabi from './pages/UploadSyllabi.tsx';
|
||||
import Professor from './pages/Professor.tsx';
|
||||
import Welcome from './welcome.tsx';
|
||||
import Blog from './pages/Blog.tsx';
|
||||
import Moderator from './pages/Moderator/Moderator.tsx';
|
||||
|
||||
const server = "https://api.sharesyllabus.me";
|
||||
export default server
|
||||
export default server;
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<HashRouter>
|
||||
@@ -30,8 +28,6 @@ createRoot(document.getElementById('root')!).render(
|
||||
<Route path="/syllabus/:id" element={<Layout><Syllabus /></Layout>} />
|
||||
<Route path="/upload" element={<Layout><UploadSyllabi /></Layout>} />
|
||||
<Route path="/professor/:id" element={<Layout><Professor /></Layout>} />
|
||||
<Route path="/blog/:file" element={<Blog/>}/>
|
||||
<Route path="/moderator" element={<Moderator></Moderator>} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
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
|
||||
@@ -25,7 +25,7 @@ function Syllabus() {
|
||||
<div className="text-left place-items-center justify-center flex w-3/4">
|
||||
<div className="w-2/5 h-full flex flex-col">
|
||||
<div className="flex flex-col">
|
||||
<text className="text-3xl text-red-400 font-[Parkinsans] font-black">{syllabus?.className}</text>
|
||||
<text className="text-3xl text-red-400 font-black">{syllabus?.className}</text>
|
||||
<text className="">{syllabus?.fullClassName}</text>
|
||||
<text className="italic underline">{syllabus?.professor}</text>
|
||||
</div>
|
||||
|
||||
+145
-164
@@ -1,71 +1,52 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Marquee from "react-fast-marquee";
|
||||
import type { Class, Professor, School } from "./types";
|
||||
import type { Class, School } from "./types";
|
||||
|
||||
import Select from "react-select";
|
||||
import AsyncSelect from "react-select/async";
|
||||
import server from "./main";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import isMobile from "is-mobile";
|
||||
import { setInitialQuery } from "./App";
|
||||
import SyllabusListing, { ClassListing } from "./components/syllabus_comp";
|
||||
// Removed initialQuery import as it cannot be reassigned
|
||||
|
||||
export interface AmbiguousClass extends Class {
|
||||
school: School;
|
||||
}
|
||||
|
||||
|
||||
function Welcome() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// State for tracking user selections
|
||||
const [selectedCollege, setSelectedCollege] = useState<string>("");
|
||||
const [selectedClass, setSelectedClass] = useState<string>("");
|
||||
const [selectedProfessor, setSelectedProfessor] = useState<string>("");
|
||||
|
||||
const [shouldNavigate, setShouldNavigate] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<ClassListing | string>("");
|
||||
|
||||
const [collegeOptions, setCollegeOptions] = React.useState<School[]>([]);
|
||||
const [defaultClassOptions, setDefaultClassOptions] = useState<AmbiguousClass[]>([]);
|
||||
const [defaultProfessorOptions, setDefaultProfessorOptions] = useState<Professor[]>([]);
|
||||
const [query] = useState<string>("");
|
||||
|
||||
const [syllabi, setSyllabi] = useState<ClassListing[]>([]);
|
||||
|
||||
// Fetch syllabi for an empty search
|
||||
useEffect(() => {
|
||||
fetch(`${server}/schools`, {
|
||||
method: "GET",
|
||||
}).then((res) => res.json()).then((data) => {
|
||||
setCollegeOptions(data);
|
||||
console.log('data', data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadDefaultOptions = async () => {
|
||||
const classResponses = await Promise.all(
|
||||
collegeOptions.map(async (college) => {
|
||||
const res = await fetch(`https://api.sharesyllabus.me/search/class/&q=A&s=${college.name}`, {
|
||||
method: "POST",
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.map((item: AmbiguousClass) => ({
|
||||
...item,
|
||||
school: college,
|
||||
}));
|
||||
})
|
||||
);
|
||||
setDefaultClassOptions(classResponses.flat());
|
||||
|
||||
const professorResponses = await Promise.all(
|
||||
collegeOptions.map(async (college) => {
|
||||
const res = await fetch(`${server}/search/professor/&s=${college.name}`, {
|
||||
method: "POST",
|
||||
});
|
||||
const data = await res.json();
|
||||
return data;
|
||||
})
|
||||
);
|
||||
setDefaultProfessorOptions(professorResponses.flat());
|
||||
const fetchSyllabi = async () => {
|
||||
try {
|
||||
const response = await fetch(`${server}/search/?q=`, {
|
||||
method: "GET",
|
||||
});
|
||||
const data = await response.json();
|
||||
setSyllabi(data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch syllabi:", error);
|
||||
}
|
||||
};
|
||||
|
||||
if (collegeOptions.length > 0) {
|
||||
loadDefaultOptions();
|
||||
}
|
||||
}, [collegeOptions]);
|
||||
fetchSyllabi();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Navigation logic in useEffect to ensure we have the latest state
|
||||
useEffect(() => {
|
||||
@@ -75,152 +56,152 @@ function Welcome() {
|
||||
let path = "/app";
|
||||
|
||||
// Add college parameter (if present)
|
||||
if (selectedCollege) {
|
||||
path += `/${encodeURIComponent(selectedCollege)}`;
|
||||
} else if (selectedProfessor || selectedClass) {
|
||||
// Need placeholder if we have later params but no college
|
||||
path += "/_";
|
||||
}
|
||||
|
||||
// Add professor parameter (if present)
|
||||
if (selectedProfessor) {
|
||||
path += `/${encodeURIComponent(selectedProfessor)}`;
|
||||
} else if (selectedClass && (selectedCollege || path.includes("/_"))) {
|
||||
// Need placeholder if we have class but no professor
|
||||
path += "/_";
|
||||
}
|
||||
|
||||
// Add class parameter (if present)
|
||||
if (selectedClass) {
|
||||
path += `/${encodeURIComponent(selectedClass)}`;
|
||||
}
|
||||
|
||||
console.log("Navigating to:", path);
|
||||
setInitialQuery(query);
|
||||
navigate(path);
|
||||
|
||||
// Reset the navigation flag
|
||||
setShouldNavigate(false);
|
||||
}, [selectedCollege, selectedProfessor, selectedClass, shouldNavigate, navigate]);
|
||||
}, [shouldNavigate, navigate]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={`w-full text-left h-full bg-[#fafdfb] gap-2 text-[#051008] place-items-center ${!isMobile() ? "p-32" : "p-4"} flex flex-col`}>
|
||||
<div className={`w-full flex-1 text-left h-full bg-[#fafdfb] bg-no-repeat bg-center gap-2 text-[#051008] place-items-center ${!isMobile() ? " bg-[url(/logo_big.png)] pt-32" : "p-4"} flex flex-col`}>
|
||||
|
||||
{!isMobile() &&
|
||||
<div className="flex-1 flex flex-col place-items-center gap-2">
|
||||
<h1 className="font-[Parkinsans] text-5xl w-2/3 font-extrabold">📚 Enter your <span className="text-[#47c068]"> School District, Course Name, or Professor's Name, </span> to begin!</h1>
|
||||
|
||||
<Select
|
||||
className="p-1 text-lg bg-[#fafdfb] w-2/3 rounded-md border-gray-300"
|
||||
placeholder="School District: i.e. RCCD, Chaffey, SBVC, etc."
|
||||
options={collegeOptions}
|
||||
getOptionLabel={(option: School) => option.name}
|
||||
getOptionValue={(option: School) => option.name}
|
||||
onChange={(selectedOption: School | null) => {
|
||||
if (selectedOption) {
|
||||
setSelectedCollege(selectedOption.name);
|
||||
setShouldNavigate(true);
|
||||
}
|
||||
}}
|
||||
isSearchable={true}
|
||||
/>
|
||||
<div className="flex flex-col place-items-center z-50 gap-2">
|
||||
<div className="w-2/3 p-2 bg-[rgba(0,0,0,0.75)] shadow-md gap-2 flex flex-col place-items-center backdrop-blur-md rounded-md border border-gray-400 bg-origin-content">
|
||||
<h1 className=" text-5xl font-black text-white">📚 Enter your <span className=" bg-clip-text font-black text-transparent bg-gradient-to-tl from-yellow-400 to-orange-200"> School District, Course Name, or Professor's Name, </span> to begin!</h1>
|
||||
|
||||
<AsyncSelect
|
||||
className="p-1 text-lg bg-[#fafdfb] w-2/3 rounded-md border-gray-300"
|
||||
placeholder="Course Name: ENG-1A, General Psych, etc."
|
||||
value={selectedOption}
|
||||
className="w-full text-md bg-[#fafdfb] border rounded-lg border-gray-400"
|
||||
placeholder="Search for school, course, or professor"
|
||||
loadOptions={async (inputValue) => {
|
||||
if (!inputValue) {
|
||||
inputValue = "A";
|
||||
console.log('inputValue', inputValue);
|
||||
if (!inputValue) return [];
|
||||
|
||||
try {
|
||||
const response = await fetch(`${server}/search/?q=${inputValue}`, {
|
||||
method: "GET",
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Map the fetched data into options
|
||||
const options = data.map((item: ClassListing) => ({
|
||||
label: `${item.class?.className || "Unknown Class"} - ${item.professor || "Unknown Professor"}`,
|
||||
value: item,
|
||||
}));
|
||||
|
||||
// Prepend a custom option based on the input value
|
||||
const customOption = {
|
||||
label: `Search for "${inputValue}"`,
|
||||
value: { customSearch: inputValue },
|
||||
};
|
||||
|
||||
return [customOption, ...options]; // Prepend the custom option
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch options:", error);
|
||||
return [];
|
||||
}
|
||||
const responses = await Promise.all(
|
||||
collegeOptions.map(async (college) => {
|
||||
const res = await fetch(`${server}/search/class/?q=${inputValue}&s=${college.name}`, {
|
||||
method: "POST",
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.map((item: AmbiguousClass) => ({
|
||||
...item,
|
||||
school: college,
|
||||
}));
|
||||
})
|
||||
);
|
||||
return responses.flat();
|
||||
}}
|
||||
defaultOptions={defaultClassOptions}
|
||||
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) {
|
||||
setSelectedClass(selectedOption.className);
|
||||
setShouldNavigate(true);
|
||||
onChange={(option: any) => {
|
||||
if (option) {
|
||||
console.log("Option selected:", option);
|
||||
|
||||
// Handle the custom prepended option differently if needed
|
||||
if (option.value.customSearch) {
|
||||
console.log(`Custom search triggered for: ${option.value.customSearch}`);
|
||||
setInitialQuery(option.value.customSearch)
|
||||
navigate(`/app/`);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedOption(option); // Update the selected option state
|
||||
let path = "/app";
|
||||
setInitialQuery(option.label);
|
||||
navigate(path); // Redirect to the desired path
|
||||
}
|
||||
}}
|
||||
isSearchable={true}
|
||||
cacheOptions
|
||||
|
||||
/>
|
||||
|
||||
<AsyncSelect
|
||||
className="p-1 text-lg bg-[#fafdfb] w-2/3 rounded-md border-gray-300"
|
||||
placeholder="Professor Name: John Doe, Jane Smith, etc."
|
||||
loadOptions={async (inputValue) => {
|
||||
const responses = await Promise.all(
|
||||
collegeOptions.map(async (college) => {
|
||||
const res = await fetch(`${server}/search/professor/?q=${inputValue}&s=${college.name}`, {
|
||||
method: "POST",
|
||||
});
|
||||
const data = await res.json();
|
||||
return data;
|
||||
})
|
||||
);
|
||||
return responses.flat();
|
||||
styles={{
|
||||
control: (provided) => ({
|
||||
...provided,
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
backgroundColor: '#fafdfb',
|
||||
borderRadius: '8px',
|
||||
}),
|
||||
input: (provided) => ({
|
||||
...provided,
|
||||
boxShadow: 'none',
|
||||
}),
|
||||
placeholder: (provided) => ({
|
||||
...provided,
|
||||
color: '#888',
|
||||
}),
|
||||
menu: (provided) => ({
|
||||
...provided,
|
||||
zIndex: 99999,
|
||||
pointerEvents: "all"
|
||||
}),
|
||||
menuPortal: (provided) => ({
|
||||
...provided,
|
||||
zIndex: 99999,
|
||||
pointerEvents: "all"
|
||||
}),
|
||||
}}
|
||||
defaultOptions={defaultProfessorOptions}
|
||||
getOptionLabel={(option: Professor) => option.name}
|
||||
getOptionValue={(option: Professor) => option.name}
|
||||
onChange={(selectedOption: Professor | null) => {
|
||||
if (selectedOption) {
|
||||
setSelectedProfessor(selectedOption.name);
|
||||
setShouldNavigate(true);
|
||||
}
|
||||
}}
|
||||
isSearchable={true}
|
||||
cacheOptions
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{isMobile() &&
|
||||
<div className="text-center flex place-items-center flex-col gap-4 pt-4">
|
||||
{isMobile() &&
|
||||
<div className="text-center z-10 flex place-items-center flex-col gap-4 pt-4">
|
||||
<img className="w-1/2 text-center" src="https://akns-images.eonline.com/eol_images/Entire_Site/201363/rs_600x600-130703124143-600.Gru.mh.070313.jpg?fit=around%7C1080:1080&output-quality=90&crop=1080:1080;center,top"></img>
|
||||
<h1 className="font-sans font-black text-2xl text-blue-600">Search Syllabuses in Your School!</h1>
|
||||
|
||||
{/* Search button as fallback */}
|
||||
<div className="flex flex-col text-center gap-2">
|
||||
{!isMobile() && <text className="relative">or </text>}
|
||||
<button
|
||||
className="p-2 px-4 bg-black text-white rounded-xl drop-shadow-md hover:drop-shadow-xl cursor-pointer transition-colors"
|
||||
onClick={() => setShouldNavigate(true)}
|
||||
>
|
||||
Just Open the Search 🚀
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
{/* Search button as fallback */}
|
||||
<div className="flex flex-col text-center gap-2">
|
||||
{!isMobile() && <text className="relative">or </text>}
|
||||
<button
|
||||
className="p-2 px-4 bg-black text-white rounded-xl drop-shadow-md hover:drop-shadow-xl cursor-pointer transition-colors"
|
||||
onClick={() => setShouldNavigate(true)}
|
||||
>
|
||||
Just Open the Search 🚀
|
||||
</button>
|
||||
|
||||
{!isMobile() ? <Marquee speed={10} gradient={true} className="w-full relative flex-1 gap-2">
|
||||
{syllabi.map((syllabus) => (
|
||||
<div className="mx-1">
|
||||
<SyllabusListing
|
||||
key={syllabus.id || syllabus.fileName}
|
||||
className={syllabus.class?.className}
|
||||
school={syllabus.school}
|
||||
professor={syllabus.professor}
|
||||
professorId={syllabus.professorId}
|
||||
fileName={syllabus.fileName}
|
||||
description={syllabus.description}
|
||||
classLength={syllabus.classLength}
|
||||
fullClassName={syllabus.class?.fullClassName}
|
||||
textbookCost={syllabus.textbookCost}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Marquee> : <div className="h-1/3"></div>}
|
||||
|
||||
|
||||
<div className="text-center ">
|
||||
|
||||
<text className="text-xl ">Made By <span className=" font-bold rounded text-[#47c068]">the College Success Club of JFKMCHS</span>. Gain up to 2 hrs of community service hours by contributing syllabuses into our index. Visit our blog for free resources for college students. </text>
|
||||
|
||||
</div>
|
||||
|
||||
{!isMobile() ? <div className="flex-1"></div> : <div className="h-1/3"></div>}
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<div className="absolute top-0 bottom-0 left-0 w-8 bg-gradient-to-r from-[#fafdfb] to-transparent z-10"></div>
|
||||
<div className="absolute top-0 bottom-0 right-0 w-8 bg-gradient-to-l from-[#fafdfb] to-transparent z-10"></div>
|
||||
<Marquee speed={10} gradient={false}>
|
||||
<text className="text-xl font-[Parkinsans]">Made By <span className=" font-bold rounded text-[#47c068]">the College Success Club of JFKMCHS</span>. Gain up to 2 hrs of community service hours by contributing syllabuses into our index. Visit our blog for free resources for college students. </text>
|
||||
</Marquee>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user