mirror of
https://github.com/Leado123/Leado123.github.io.git
synced 2026-10-01 00:32:59 +00:00
added new feature
This commit is contained in:
-275
@@ -1,275 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import './App.css'
|
||||
|
||||
import SyllabusListing, { ClassListing } from './syllabus_comp';
|
||||
|
||||
import { Accordion, FileInput } from "flowbite-react";
|
||||
|
||||
import { Button, Label, Modal, TextInput } from "flowbite-react";
|
||||
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { isMobile } from "is-mobile";
|
||||
|
||||
|
||||
|
||||
//TODO: MAKE when you scroll to bottom of page, adds more results
|
||||
//TODO: make it so that when you type in stuff, it automatically searches on the server, but there is a tick rate for responses
|
||||
|
||||
//TODO: create an ELABORATE WHOLE Uploading syllabus page to make the process easier
|
||||
|
||||
var searchQuery = "";
|
||||
|
||||
|
||||
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 [syllabi, setSyllabi] = useState<ClassListing[]>([]);
|
||||
const [query, setQuery] = useState<string>("");
|
||||
|
||||
|
||||
async function handleSearch() {
|
||||
if (query != "") searchQuery = query;
|
||||
if (query === "admin") {
|
||||
navigate('admin');
|
||||
}
|
||||
console.log(`searching for ${query}`);
|
||||
try {
|
||||
if (searchQuery === "") {
|
||||
var response = await fetch(`https://api.sharesyllabus.me/search/?q=${query}`);
|
||||
} else {
|
||||
var response = await fetch(`https://api.sharesyllabus.me/search/?q=${searchQuery}`);
|
||||
}
|
||||
var data = await response.json();
|
||||
console.log(data);
|
||||
setSyllabi(data);
|
||||
} catch {
|
||||
console.log("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(`https://api.sharesyllabus.me/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();
|
||||
console.log(data);
|
||||
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(() => {
|
||||
console.log("temp file is", tempFileId)
|
||||
}, [tempFileId])
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(searchQuery);
|
||||
console.log("use effect ran");
|
||||
handleSearch();
|
||||
|
||||
|
||||
},
|
||||
[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 justify-center items-center">
|
||||
<Tabs className="w-full bg-zinc-950 p-4">
|
||||
<TabList className="flex space-x-2 pb-1">
|
||||
<Tab selectedClassName="rounded-lg p-1 underline decoration-wavy" className=" focus:outline-none p-1 cursor-pointer">Syllabuses</Tab>
|
||||
<Tab selectedClassName="rounded-lg p-1 underline decoration-wavy" className=" focus:outline-none p-1 cursor-pointer">ObjectSearch (In Development)</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<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-zinc-800 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 focus:ring-0 bg-transparent border-none focus:bg-black rounded-lg flex-1 text-white" value={query} onChange={(e) => setQuery(e.target.value)}>
|
||||
|
||||
</input>
|
||||
<button className="aspect-square flex justify-center items-center" onClick={handleSearch}>
|
||||
<span className="material-symbols-rounded text-white">
|
||||
search
|
||||
</span>
|
||||
</button></div>
|
||||
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<div className="text-left">
|
||||
<text>Filters:</text>
|
||||
<div className="">
|
||||
<select className="w-full mb-2 mt-2 bg-black p-1 rounded-lg">
|
||||
<option>10 results</option>
|
||||
<option>15 results</option>
|
||||
<option>20 results</option>
|
||||
</select>
|
||||
<span>
|
||||
<select className="w-full bg-black p-1 rounded-lg" >
|
||||
<option>all results (include unverified) </option>
|
||||
<option>verified results only</option>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
|
||||
|
||||
<div className="listing">
|
||||
{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>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
</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>}
|
||||
|
||||
|
||||
<Modal show={openModal} size="md" onClose={onCloseModal} popup>
|
||||
<Modal.Header />
|
||||
<Modal.Body>
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-xl font-medium text-gray-900 dark:text-white">Upload a Syllabus</h3>
|
||||
<div>
|
||||
<div className="mb-2 block">
|
||||
<Label htmlFor="large-file-upload" value="(pdf, doc, or docx) *"></Label>
|
||||
</div>
|
||||
<form method="post" className="space-y-6">
|
||||
<FileInput id="large-file-upload" onChange={handleFileChange} sizing="lg" />
|
||||
|
||||
<Accordion>
|
||||
<Accordion.Panel isOpen={accordionOpen}>
|
||||
<Accordion.Title className="">(Optional) Fill out information to get volunteer hours</Accordion.Title>
|
||||
<Accordion.Content className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-2 block">
|
||||
<Label htmlFor="email" value="Your email (cnusd school email is fine)" />
|
||||
</div>
|
||||
<TextInput
|
||||
id="email"
|
||||
placeholder="name@company.com"
|
||||
value={email}
|
||||
// @ts-ignore
|
||||
onChange={(event: Event) => setEmail(event?.target?.value)}
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 block">
|
||||
<Label value="Your Name" />
|
||||
</div>
|
||||
<TextInput
|
||||
placeholder="John Doe"
|
||||
value={name}
|
||||
// @ts-ignore
|
||||
onChange={(event: Event) => setName(event?.target?.value)}
|
||||
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-black">We will handle classifying and organizing your syllabus and will contact you within 3 days to give you community service hours — College Success Club</p>
|
||||
</Accordion.Content>
|
||||
</Accordion.Panel>
|
||||
</Accordion>
|
||||
|
||||
<Button onClick={uploadFile} className="bg-blue-700 text-white">Upload File</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Modal.Body>
|
||||
|
||||
</Modal>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
+30
-29
@@ -1,6 +1,7 @@
|
||||
import Marquee from "react-fast-marquee";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { isMobile } from "is-mobile";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
interface BlogPosts {
|
||||
@@ -9,14 +10,14 @@ interface BlogPosts {
|
||||
link: string;
|
||||
}
|
||||
|
||||
export const blogPosts: BlogPosts[] = [{ title: "What is 𝐒𝐡𝐚𝐫𝐞 𝐌𝐲 𝐒𝐲𝐥𝐥𝐚𝐛𝐮𝐬 & why is it 𝖇𝖊𝖙𝖙𝖊𝖗 than ℝ𝕒𝕥𝕖 𝕄𝕪 ℙ𝕣𝕠𝕗𝕖𝕤𝕤𝕠𝕣?", icon: "star", link: "/useful_resources" },{ title: "🆄🆂🅴🅵🆄🅻 resources to not miss out for highschool & college students!", icon: "bolt", link: "/useful_resources" }]
|
||||
export const blogPosts: BlogPosts[] = [{ title: "What is 𝐒𝐡𝐚𝐫𝐞 𝐌𝐲 𝐒𝐲𝐥𝐥𝐚𝐛𝐮𝐬 & why is it 𝖇𝖊𝖙𝖙𝖊𝖗 than ℝ𝕒𝕥𝕖 𝕄𝕪 ℙ𝕣𝕠𝕗𝕖𝕤𝕤𝕠𝕣?", icon: "star", link: "/useful_resources" }, { title: "🆄🆂🅴🅵🆄🅻 resources to not miss out for highschool & college students!", icon: "bolt", link: "/useful_resources" }]
|
||||
|
||||
function Layout({ children }: { children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
console.log('Layout rendering'); // Add this line
|
||||
|
||||
|
||||
const [marqueeSpeed, setMarqueeSpeed] = useState(15);
|
||||
|
||||
const uploadSyllabi = () => {
|
||||
navigate('/upload')
|
||||
@@ -24,37 +25,37 @@ function Layout({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`place-items-center justify-center border-b-2 ${isMobile() ? "pt-0 flex" : "pt-4 grid grid-cols-5"} border-purple-500`}>
|
||||
<div></div>
|
||||
<div className={`${isMobile() ? "w-full" : "col-span-3"} p-4`}>
|
||||
<div className="flex flex-row">
|
||||
<h3 onClick={() => { navigate("/") }} className="text-4xl hover:text-yellow-400 w-min cursor-pointer text-yellow-300 font-extrabold text-left flex-1">📚 Share Syllabus <text className="font-light text-purple-600">beta</text></h3>
|
||||
<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 sticky grid grid-cols-3">
|
||||
<text onClick={(e) => 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">
|
||||
<span className="material-symbols-rounded p-1 text-black">web_traffic</span>
|
||||
<button onClick={(e) => 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">
|
||||
<Marquee className="anim-enter-blur w-40" delay={1} gradient={true} gradientWidth="10%" gradientColor="black" speed={marqueeSpeed}>
|
||||
<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>
|
||||
<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>
|
||||
<span className="material-symbols-rounded pr-1 text-white">web_traffic</span>
|
||||
</div>
|
||||
</Marquee>
|
||||
</button>
|
||||
<span className="material-symbols-rounded p-1 text-black">add</span>
|
||||
</div>
|
||||
{!isMobile() && (
|
||||
<p className="text-left">
|
||||
A platform for sharing and exploring course syllabi, helping students compare class structures, assignments, and topics while earning community service hours by contributing syllabuses to the knowledge base.
|
||||
</p>
|
||||
)}
|
||||
<div className="pt-1 text-left flex flex-row space-x-2">
|
||||
<p className="text-xs font-bold">Contacts:</p>
|
||||
<Marquee className="text-xs flex-1" speed={25}>
|
||||
<p className="">Are you from a college outside of RCCD? Contact lwen3@student.rccd.edu to get your school listed</p>
|
||||
</Marquee>
|
||||
</div>
|
||||
|
||||
<div className={`w-full ${isMobile() ? "p-0" : "p-4"} flex place-items-center justify-between text-sm`}>
|
||||
<a href="https://shaded-yttrium-0dc.notion.site/Share-Syllabus-Blog-1a98dea271f580a4aa48fd791a8c83da?pvs=74">Blog</a>
|
||||
<a href="https://shaded-yttrium-0dc.notion.site/About-ShareSyllabus-me-1a98dea271f580cf9162f61b22e7454a">About Share Syllabus</a>
|
||||
<a href="https://assist.org">Assist.org</a>
|
||||
{!isMobile() && <button onClick={uploadSyllabi} className={`bg-blue-600 hover:bg-blue-700 border-yellow-300 border-2 flex place-items-center justify-center text-white rounded-lg font-bold p-2`}><span className="pr-1 material-symbols-rounded">add_circle</span>Upload a Syllabus</button>}
|
||||
|
||||
<div className="flex gap-4 font-light text-gray-600">
|
||||
<Link to="/moderator" className=" text-gray-600">Moderator Panel</Link>
|
||||
<Link to="/blog" className="text-gray-600">Blog</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { ClassListing } from "./syllabus_comp";
|
||||
import View from "./View";
|
||||
|
||||
|
||||
function Syllabus() {
|
||||
|
||||
const { id } = useParams();
|
||||
|
||||
const [syllabus, getSyllabus] = useState<ClassListing | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`https://api.sharesyllabus.me/syllabus/${id}`)
|
||||
.then(response => response.json())
|
||||
.then(data => getSyllabus(data))
|
||||
}, [id])
|
||||
|
||||
return (
|
||||
<div className="p-16 h-full text-left w-full justify-center flex">
|
||||
<div className="text-left place-items-center justify-center flex w-full">
|
||||
<div className="w-1/4 h-full flex flex-col">
|
||||
<div className="flex flex-col">
|
||||
<text className="text-3xl text-yellow-300 font-black">{syllabus?.className}</text>
|
||||
<text className="">{syllabus?.fullClassName}</text>
|
||||
<text className="italic underline">{syllabus?.professor}</text>
|
||||
</div>
|
||||
<text className="italic font-light pt-6 font-serif">"{syllabus?.description}"</text>
|
||||
<text className="text-blue-600">{syllabus?.classLength} weeked class</text>
|
||||
<text className="font-bold text-lime-500">{syllabus?.textbookCost} textbook cost</text>
|
||||
|
||||
</div>
|
||||
{syllabus ? <View className="flex-1" passedFile={syllabus?.fileName}></View> : <div></div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Syllabus;
|
||||
@@ -2,6 +2,7 @@ import { useParams } from "react-router-dom";
|
||||
import DocViewer, { DocViewerRenderers } from "react-doc-viewer";
|
||||
import {PDFViewer} from 'srpv-js'
|
||||
import { isMobile } from 'is-mobile';
|
||||
import server from "../main";
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +31,7 @@ function View({passedFile, className}: ViewParams) {
|
||||
const isPDF = checkIfPdf(file || "");
|
||||
|
||||
const docs = [
|
||||
{ uri: `https://api.sharesyllabus.me/files/${file}` }
|
||||
{ uri: `${server}/files/${file}` }
|
||||
]
|
||||
|
||||
|
||||
@@ -54,7 +55,7 @@ function View({passedFile, className}: ViewParams) {
|
||||
{isMobile() === false &&
|
||||
<>
|
||||
{isPDF
|
||||
? <iframe className="flex-1 w-full rounded-xl" src={`https://api.sharesyllabus.me/files/${file}`} />
|
||||
? <iframe className="flex-1 w-full rounded-xl" src={`${server}/files/${file}`} />
|
||||
: <DocViewer className="flex-1 w-full rounded-xl" pluginRenderers={DocViewerRenderers} documents={docs}></DocViewer>}
|
||||
</>
|
||||
|
||||
@@ -62,7 +63,7 @@ function View({passedFile, className}: ViewParams) {
|
||||
{isMobile() === true &&
|
||||
<>
|
||||
{isPDF
|
||||
? <div className="flex-1 w-full rounded-xl"><PDFViewer link={`https://api.sharesyllabus.me/files/${file}`}></PDFViewer></div>
|
||||
? <div className="flex-1 w-full rounded-xl"><PDFViewer link={`${server}/files/${file}`}></PDFViewer></div>
|
||||
: <DocViewer className="flex-1 w-full rounded-xl" pluginRenderers={DocViewerRenderers} documents={docs}></DocViewer>}
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { Accordion } from 'flowbite-react';
|
||||
|
||||
interface SyllabusData {
|
||||
header: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export function MarkdownView({ data }: { data: SyllabusData[] }) {
|
||||
return (
|
||||
<div className="markdown-body">
|
||||
<Accordion>
|
||||
{data.map((i, idx) => {
|
||||
return (
|
||||
<Accordion.Panel key={idx}>
|
||||
<Accordion.Title>{i.header}</Accordion.Title>
|
||||
<Accordion.Content>
|
||||
{/* @ts-ignore */}
|
||||
<Markdown remarkPlugins={[remarkGfm]} children={i.body} />
|
||||
</Accordion.Content>
|
||||
</Accordion.Panel>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./syllabuslisting.css";
|
||||
import "./App.tsx";
|
||||
import "../syllabuslisting.css";
|
||||
import "../pages/App.tsx";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -19,6 +19,7 @@ export interface ClassListing {
|
||||
professor: string;
|
||||
fileName: string;
|
||||
description: string;
|
||||
content?: string;
|
||||
classLength: number;
|
||||
reviewed?: boolean;
|
||||
createdByName?: string;
|
||||
@@ -98,11 +99,11 @@ function SyllabusListing({ className, school, professor, professorId, fileName,
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full border-zinc-800 border hover:bg-zinc-900 flex gap-4 cursor-pointer text-white rounded-lg p-3" onClick={handleView} >
|
||||
<div className="w-full border-gray-400 border flex gap-4 cursor-pointer text-[#051008] rounded-lg p-3" onClick={handleView} >
|
||||
<div className="flex text-left flex-col w-1/4">
|
||||
<span className="font-extrabold">{className}</span>
|
||||
<span className="text-xs text-gray-300">{fullClassName}</span>
|
||||
<div className="flex text-gray-300 text-xs">
|
||||
<span className="text-xs text-gray-800">{fullClassName}</span>
|
||||
<div className="flex text-gray-800 text-xs">
|
||||
<text>🏫 {school?.fullName}</text>
|
||||
</div>
|
||||
{textbookCostElement()}
|
||||
@@ -114,7 +115,7 @@ function SyllabusListing({ className, school, professor, professorId, fileName,
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="flex-1"></span>
|
||||
<button className="aspect-square flex items-center align-middle justify-center hover:bg-slate-200 rounded-sm text-white" onClick={() => setHideMore(!hideMore)}>
|
||||
<button className="aspect-square flex items-center align-middle justify-center rounded-sm text-[#051008]" onClick={() => setHideMore(!hideMore)}>
|
||||
<span className="material-symbols-rounded text-2xl">more_horiz</span>
|
||||
<div className="relative" hidden={hideMore}>
|
||||
<button ref={buttonRef} onClick={handleFlag} className="absolute border-black border-2 -left-14 top-2 drop-shadow-lg bg-white rounded-lg aspect-square p-4">
|
||||
+21
-18
@@ -1,16 +1,21 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Route, Routes, HashRouter } from 'react-router-dom';
|
||||
import './index.css';
|
||||
import App from './App.tsx';
|
||||
import View from './View.tsx';
|
||||
import Admin from './Admin.tsx';
|
||||
import Flag from './Flag.tsx';
|
||||
import Reports from './Reports.tsx';
|
||||
import Syllabus from './Syllabus.tsx';
|
||||
import App from './pages/App.tsx';
|
||||
import View from './components/FileView.tsx';
|
||||
import Admin from './pages/Admin.tsx';
|
||||
import Flag from './pages/Flag.tsx';
|
||||
import Reports from './pages/Reports.tsx';
|
||||
import Syllabus from './pages/Syllabus.tsx';
|
||||
import Layout from './Layout.tsx';
|
||||
import UploadSyllabi from './UploadSyllabi.tsx';
|
||||
import Professor from './Professor.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
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<HashRouter>
|
||||
@@ -19,16 +24,14 @@ createRoot(document.getElementById('root')!).render(
|
||||
<Route path="/admin" element={<Admin />} />
|
||||
<Route path="/admin/:highlightedSyllabusId" element={<Admin />} />
|
||||
<Route path="/reports" element={<Reports />} />
|
||||
|
||||
<Route path="/" element={<Layout><Welcome /></Layout>} />
|
||||
<Route path="/app/:college?/:professor?/:class?" element={<Layout><App /></Layout>} />
|
||||
<Route path="/flag/:id" element={<Layout><Flag /></Layout>} />
|
||||
<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>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/flag/:id" element={<Flag />} />
|
||||
<Route path="/syllabus/:id" element={<Syllabus />} />
|
||||
<Route path="/upload" element={<UploadSyllabi />} />
|
||||
<Route path="/professor/:id" element={<Professor />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</HashRouter>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { DataSheetGrid, textColumn, checkboxColumn, Column, keyColumn, intColumn } from 'react-datasheet-grid';
|
||||
import "react-datasheet-grid/dist/style.css";
|
||||
import { ClassListing } from "./syllabus_comp";
|
||||
import { ClassListing } from "../components/syllabus_comp";
|
||||
import { useParams } from "react-router-dom";
|
||||
import server from '../main'
|
||||
|
||||
|
||||
interface PreviewButtonProps {
|
||||
@@ -57,7 +58,7 @@ function Admin() {
|
||||
try {
|
||||
let formdata = new FormData();
|
||||
formdata.append("username", username);
|
||||
const response = await fetch(`https://api.sharesyllabus.me/admin/wholedatabase`, {
|
||||
const response = await fetch(`${server}/admin/wholedatabase`, {
|
||||
method: "POST",
|
||||
body: formdata,
|
||||
});
|
||||
@@ -71,7 +72,7 @@ function Admin() {
|
||||
} else {
|
||||
try {
|
||||
console.log(highlightedSyllabusId);
|
||||
const response = await fetch(`https://api.sharesyllabus.me/syllabus/${highlightedSyllabusId}`, {
|
||||
const response = await fetch(`${server}/syllabus/${highlightedSyllabusId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -92,7 +93,7 @@ function Admin() {
|
||||
for (const change of databaseChanges) {
|
||||
const { fileName, ...sendableChange } = change;
|
||||
|
||||
const response = await fetch(`https://api.sharesyllabus.me/admin/change/`, {
|
||||
const response = await fetch(`${server}/admin/change/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -4,6 +4,61 @@
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.anim-enter {
|
||||
opacity: 0;
|
||||
animation: enter-animation 1s forwards;
|
||||
}
|
||||
|
||||
.anim-enter-blur {
|
||||
opacity: 0;
|
||||
filter: blur(10px);
|
||||
animation: enter-animation-blur 1s forwards 1s cubic-bezier(.34,-0.01,.12,1.01);
|
||||
}
|
||||
|
||||
@keyframes enter-animation-blur {
|
||||
to {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes enter-animation {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.anim-enter-width {
|
||||
|
||||
width: 1rem;
|
||||
animation: enter-animation-width 1s cubic-bezier(.34,-0.01,.12,1.01) 1s forwards;
|
||||
}
|
||||
|
||||
@keyframes enter-animation-width {
|
||||
to {
|
||||
width: 40rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.animated-background {
|
||||
background-size: 400%;
|
||||
|
||||
-webkit-animation: animation 2s ease infinite;
|
||||
-moz-animation: animation 2s ease infinite;
|
||||
animation: animation 2s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes animation {
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes scroll-left {
|
||||
@@ -0,0 +1,377 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import './App.css'
|
||||
|
||||
import SyllabusListing, { ClassListing } from '../components/syllabus_comp';
|
||||
|
||||
import { Accordion, FileInput, } from "flowbite-react";
|
||||
|
||||
import server from '../main'
|
||||
|
||||
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 [querying, setQuerying] = useState(false);
|
||||
|
||||
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');
|
||||
}
|
||||
setQuerying(true);
|
||||
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(`${server}/search/?q=${debouncedQuery}${end}`);
|
||||
var data = await response.json();
|
||||
setQuerying(false);
|
||||
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(`${server}/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(`${server}/schools`, {
|
||||
method: "GET",
|
||||
})
|
||||
.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-2 border-black 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"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}>
|
||||
</input>
|
||||
<button className="aspect-square 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">
|
||||
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 rounded-md border-gray-400"
|
||||
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 className="z-auto" {...props.innerProps} onClick={() => {
|
||||
props.selectOption({ id: "", name: "No Filter", fullName: "No Filter" });
|
||||
setFilter({ ...filter, school: undefined });
|
||||
}} style={{ zIndex: 10 }}>
|
||||
<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 rounded-md border-gray-400"
|
||||
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(`${server}/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 flex">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
{querying && <div className="flex-1 flex flex-col place-items-center gap-4 p-4"><svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-black" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>Loading...</div>}
|
||||
{!querying && syllabi.length === 0 && <div className="flex-1 flex flex-col place-items-center gap-4 p-4">No results found</div>}
|
||||
{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
|
||||
@@ -0,0 +1,24 @@
|
||||
import { MDXEditor } from "@mdxeditor/editor";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function Blog () {
|
||||
|
||||
const { file } = useParams();
|
||||
const [blog, setBlog] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/blog/${file}.md`)
|
||||
.then(response => response.text())
|
||||
.then(data => setBlog(data))
|
||||
.then(() => console.log(blog))
|
||||
}, [file]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MDXEditor readOnly markdown={blog} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Blog
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import server from '../main'
|
||||
|
||||
|
||||
function Flag() {
|
||||
|
||||
@@ -11,7 +13,7 @@ function Flag() {
|
||||
const [reportBy, setReportBy] = useState("");
|
||||
|
||||
function submitReport() {
|
||||
fetch(`https://api.sharesyllabus.me/report/${syllabusId}`, {
|
||||
fetch(`${server}/report/${syllabusId}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import server from '../../main'
|
||||
|
||||
|
||||
function Moderator () {
|
||||
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('smsToken');
|
||||
if (token) {
|
||||
setToken(token);
|
||||
}
|
||||
}, [])
|
||||
|
||||
function submitFormElements() {
|
||||
fetch(`${server}/moderator/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username: username, password: password })
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.token === undefined) {
|
||||
alert('Invalid username or password');
|
||||
}
|
||||
console.log(data);
|
||||
setToken(data.token);
|
||||
localStorage.setItem('smsToken', data.token);
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col p-16 h-full bg-white">
|
||||
{!token &&
|
||||
<div className="flex-1 gap-2 flex flex-col justify-center place-items-center">
|
||||
<img src="https://severance.wiki/_media/lumon-logo.webp?cache=" className="w-1/2"></img>
|
||||
<text className="text-black font-bold text-4xl">Log In</text>
|
||||
<input value={username} onChange={(e) => setUsername(e.target.value)} className=" bg-white border border-gray-400 p-1 rounded-sm text-black placeholder:text-black" placeholder="username"></input>
|
||||
<input value={password} onChange={(e) => setPassword(e.target.value)} className=" bg-white border border-gray-400 p-1 rounded-sm text-black placeholder:text-black" placeholder="password"></input>
|
||||
<button onClick={submitFormElements}>Submit</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Moderator
|
||||
@@ -1,6 +1,6 @@
|
||||
import mime from "mime-types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ClassListing } from "./syllabus_comp";
|
||||
import { ClassListing } from "./components/syllabus_comp";
|
||||
|
||||
export interface SyllabusReport {
|
||||
id: string;
|
||||
@@ -17,7 +17,7 @@ function Reports() {
|
||||
const [reports, setReports] = useState<SyllabusReport[]>([]);
|
||||
|
||||
function obtainReports() {
|
||||
fetch("https://api.sharesyllabus.me/admin/getreports", {
|
||||
fetch(`${server}/admin/getreports`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -26,7 +26,6 @@ function Reports() {
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
setReports(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -45,7 +44,7 @@ function Reports() {
|
||||
|
||||
function viewReport(sId: string, report: SyllabusReport) {
|
||||
setCurrentReport(report);
|
||||
fetch(`https://api.sharesyllabus.me/admin/getsyllabus/${sId}`, {
|
||||
fetch(`${server}/admin/getsyllabus/${sId}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -54,7 +53,6 @@ function Reports() {
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
setCurrentSyllabus(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { ClassListing } from "../components/syllabus_comp";
|
||||
import FileView from "../components/FileView";
|
||||
import { MarkdownView } from "../components/MarkdownView";
|
||||
import { G4F } from "g4f"
|
||||
import server from '../main'
|
||||
|
||||
|
||||
import { MDXEditor } from '@mdxeditor/editor'
|
||||
import { headingsPlugin } from '@mdxeditor/editor'
|
||||
|
||||
function Syllabus() {
|
||||
const { id } = useParams();
|
||||
|
||||
|
||||
const [syllabus, getSyllabus] = useState<ClassListing | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${server}/syllabus/${id}`)
|
||||
.then(response => response.json())
|
||||
.then(data => getSyllabus(data))
|
||||
.then(data => console.log(data))
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div className="p-16 h-full text-left w-full justify-center flex">
|
||||
<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="">{syllabus?.fullClassName}</text>
|
||||
<text className="italic underline">{syllabus?.professor}</text>
|
||||
</div>
|
||||
<text className="italic font-light pt-6 font-serif">"{syllabus?.description}"</text>
|
||||
<text className="text-blue-600">{syllabus?.classLength} week class</text>
|
||||
<text className="font-bold text-lime-500">{syllabus?.textbookCost} textbook cost</text>
|
||||
{syllabus ? <FileView className="flex-1" passedFile={syllabus?.fileName}></FileView> : <div></div>}
|
||||
</div>
|
||||
<div className="relative flex-1 h-full">
|
||||
<div className="relative z-10 flex-1 h-full">
|
||||
<MDXEditor className="flex-1 h-full text-3xl text-black font-bold font-[Parkinsans] text-center" markdown={`${syllabus?.content}`} plugins={[headingsPlugin()]} readOnly></MDXEditor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Syllabus;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
|
||||
export interface Class {
|
||||
id: number;
|
||||
className: string;
|
||||
@@ -20,7 +19,6 @@ export interface Professor {
|
||||
}
|
||||
|
||||
function UploadSyllabi() {
|
||||
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [college, setCollege] = useState("");
|
||||
const [professor, setProfessor] = useState("");
|
||||
@@ -40,7 +38,7 @@ function UploadSyllabi() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("https://api.sharesyllabus.me/schools", {
|
||||
fetch(`https://api.sharesyllabus.me/schools`, {
|
||||
method: "GET",
|
||||
}).then((res) => res.json()).then((data) => {
|
||||
setCollegeOptions(data);
|
||||
@@ -56,19 +54,17 @@ function UploadSyllabi() {
|
||||
function searchProfessor(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const value = e.target.value;
|
||||
setProfessor(value);
|
||||
fetch(`https://api.sharesyllabus.me/search/professor/?q=${value}&s=${college}`, {
|
||||
fetch(`https:/search/professor/?q=${value}&s=${college}`, {
|
||||
method: "POST",
|
||||
}).then((res) => res.json()).then((data) => {
|
||||
setProfessorOptions(data);
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
function searchClassAbrv(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
setClassAbrvQuery(e.target.value);
|
||||
|
||||
|
||||
fetch(`https://api.sharesyllabus.me/search/class/?q=${classAbrvQuery}&s=${college}`, {
|
||||
fetch(`${server}/search/class/?q=${classAbrvQuery}&s=${college}`, {
|
||||
method: "POST",
|
||||
}).then((res) => res.json()).then((data) => {
|
||||
setClassAbrvOptions(data);
|
||||
@@ -103,9 +99,9 @@ function UploadSyllabi() {
|
||||
formData.append("createdByName", name);
|
||||
formData.append("createdByEmail", email);
|
||||
|
||||
console.log(formData)
|
||||
console.log(formData);
|
||||
|
||||
fetch("https://api.sharesyllabus.me/create", {
|
||||
fetch(`https://api.sharesyllabus.me/create`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
@@ -117,7 +113,7 @@ function UploadSyllabi() {
|
||||
setError(data.msg);
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -129,17 +125,16 @@ function UploadSyllabi() {
|
||||
console.log(classDescription);
|
||||
console.log(name);
|
||||
console.log(email);
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full flex place-items-center justify-center p-10">
|
||||
|
||||
<div className="w-1/2 text-left flex flex-col">
|
||||
<text className="text-3xl font-bold">Upload a Syllabus!</text>
|
||||
<text className="italic">If you complete all the information you can receive 2 hrs in community service. Just send us your email so we can give you an E-signature of your hours. If you dont want to do the community service hours, feel free to leave some fields blank.</text>
|
||||
<div className="flex flex-col pt-2 gap-2">
|
||||
<text>Syllabus (pdf, docx, etc.)</text>
|
||||
<input type="file" className="bg-zinc-800 w-min rounded-md" onChange={handleFileChange}></input>
|
||||
<input type="file" className="w-min rounded-md bg-white text-black" onChange={handleFileChange}></input>
|
||||
<text>College District (not school!):</text>
|
||||
<input
|
||||
list="collegeOptions"
|
||||
@@ -147,7 +142,7 @@ function UploadSyllabi() {
|
||||
onChange={(e) => setCollege(e.target.value)}
|
||||
placeholder="Search or select college district"
|
||||
required
|
||||
className="p-1 rounded-md"
|
||||
className="p-1 rounded-md bg-white text-black border"
|
||||
/>
|
||||
<datalist id="collegeOptions">
|
||||
{collegeOptions.map((college: School, index: number) => (
|
||||
@@ -155,34 +150,34 @@ function UploadSyllabi() {
|
||||
))}
|
||||
</datalist>
|
||||
<text>Class Name:</text>
|
||||
<input list="classAbrvOptions" className="p-1 rounded-md" placeholder="i.e.: ENG-1A" value={classAbrvQuery} disabled={!college} onChange={(e) => searchClassAbrv(e)} required onInput={(e) => setClassAbrvQuery(e.currentTarget.value)} />
|
||||
<input list="classAbrvOptions" className="p-1 rounded-md border bg-white text-black" placeholder="i.e.: ENG-1A" value={classAbrvQuery} disabled={!college} onChange={(e) => searchClassAbrv(e)} required onInput={(e) => setClassAbrvQuery(e.currentTarget.value)} />
|
||||
<datalist id="classAbrvOptions" className="w-full bg-white">
|
||||
{classAbrvOptions?.map((classOption, index) => (
|
||||
<option key={index} value={classOption.className} />
|
||||
))}
|
||||
</datalist>
|
||||
<text>Professor:</text>
|
||||
<input placeholder="enter or search professor" className="p-1 rounded-md" list="professorOptions" value={professor} disabled={!college} required onChange={searchProfessor}></input>
|
||||
<input placeholder="enter or search professor" className="p-1 rounded-md bg-white border text-black" list="professorOptions" value={professor} disabled={!college} required onChange={searchProfessor}></input>
|
||||
<datalist id="professorOptions">
|
||||
{professorOptions.map((professor, index) => (
|
||||
<option key={index} value={professor.name} />
|
||||
))}
|
||||
</datalist>
|
||||
<text>Duration of class in weeks: [OPTIONAL: community service hours]</text>
|
||||
<input type="number" className="bg-zinc-800 p-1 rounded-md" value={classLength} onChange={(e) => setClassLength(Number(e.target.value))}></input>
|
||||
<input type="number" className="p-1 rounded-md bg-white text-black" value={classLength} onChange={(e) => setClassLength(Number(e.target.value))}></input>
|
||||
<text>Textbook Cost: [OPTIONAL: community service hours]</text>
|
||||
<select className="bg-zinc-800 p-1 rounded-md" value={textbookCost} onChange={(e) => {setTextbookCost(e.target.value); console.log(e.target.value)}}>
|
||||
<select className="p-1 rounded-md bg-white text-black" value={textbookCost} onChange={(e) => {setTextbookCost(e.target.value); console.log(e.target.value)}}>
|
||||
<option>free</option>
|
||||
<option>cheap</option>
|
||||
<option>moderate</option>
|
||||
<option>expensive</option>
|
||||
</select>
|
||||
<text>Class description in at most 2 sentences (NOT a description of the teacher) [OPTIONAL: community service hours]:</text>
|
||||
<textarea className="bg-zinc-800 p-1 rounded-md" value={classDescription} onChange={(e) => setClassDescription(e.target.value)}></textarea>
|
||||
<textarea className="p-1 rounded-md bg-white text-black" value={classDescription} onChange={(e) => setClassDescription(e.target.value)}></textarea>
|
||||
<text>Your Name [OPTIONAL: community service hours]</text>
|
||||
<input placeholder="John Doe" className="bg-zinc-800 p-1 rounded-md" value={name} onChange={(e) => setName(e.target.value)}></input>
|
||||
<input placeholder="John Doe" className="p-1 rounded-md bg-white border text-black" value={name} onChange={(e) => setName(e.target.value)}></input>
|
||||
<text>Your Email [OPTIONAL: community service hours]</text>
|
||||
<input placeholder="example@company.com" className="bg-zinc-800 p-1 rounded-md" value={email} onChange={(e) => setEmail(e.target.value)}></input>
|
||||
<input placeholder="example@company.com" className="p-1 rounded-md border bg-white text-black" value={email} onChange={(e) => setEmail(e.target.value)}></input>
|
||||
<div className="text-right">
|
||||
<button
|
||||
className="bg-blue-600 rounded-lg p-2 disabled:opacity-50"
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface Class {
|
||||
id: number;
|
||||
className: string;
|
||||
fullClassName: string;
|
||||
disciplineId: string;
|
||||
}
|
||||
|
||||
export interface School {
|
||||
id: string;
|
||||
name: string;
|
||||
fullName: string;
|
||||
}
|
||||
|
||||
export interface Professor {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Marquee from "react-fast-marquee";
|
||||
import type { Class, Professor, School } from "./types";
|
||||
|
||||
import Select from "react-select";
|
||||
import AsyncSelect from "react-select/async";
|
||||
import server from "./main";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
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 [collegeOptions, setCollegeOptions] = React.useState<School[]>([]);
|
||||
const [defaultClassOptions, setDefaultClassOptions] = useState<AmbiguousClass[]>([]);
|
||||
const [defaultProfessorOptions, setDefaultProfessorOptions] = useState<Professor[]>([]);
|
||||
|
||||
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());
|
||||
};
|
||||
|
||||
if (collegeOptions.length > 0) {
|
||||
loadDefaultOptions();
|
||||
}
|
||||
}, [collegeOptions]);
|
||||
|
||||
// Navigation logic in useEffect to ensure we have the latest state
|
||||
useEffect(() => {
|
||||
if (!shouldNavigate) return;
|
||||
|
||||
// Start with the base path
|
||||
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);
|
||||
navigate(path);
|
||||
|
||||
// Reset the navigation flag
|
||||
setShouldNavigate(false);
|
||||
}, [selectedCollege, selectedProfessor, selectedClass, shouldNavigate, navigate]);
|
||||
|
||||
return (
|
||||
<div className="w-full text-left h-full bg-[#fafdfb] gap-2 text-[#051008] place-items-center p-32 flex flex-col">
|
||||
<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}
|
||||
/>
|
||||
|
||||
<AsyncSelect
|
||||
className="p-1 text-lg bg-[#fafdfb] w-2/3 rounded-md border-gray-300"
|
||||
placeholder="Course Name: ENG-1A, General Psych, etc."
|
||||
loadOptions={async (inputValue) => {
|
||||
if (!inputValue) {
|
||||
inputValue = "A";
|
||||
console.log('inputValue', inputValue);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}}
|
||||
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();
|
||||
}}
|
||||
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
|
||||
|
||||
/>
|
||||
|
||||
{/* Search button as fallback */}
|
||||
<div className="flex flex-col text-center gap-2">
|
||||
<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 className="flex-1"></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>
|
||||
)
|
||||
}
|
||||
|
||||
export default Welcome;
|
||||
Reference in New Issue
Block a user