added new feature

This commit is contained in:
Leo Wen
2025-03-13 20:22:50 -07:00
parent 66c74c185b
commit 9f4d0e7239
30 changed files with 8870 additions and 414 deletions
+2
View File
@@ -0,0 +1,2 @@
VITE_SERVER_ADDRESS = https://api.sharesyllabus.me
#VITE_SERVER_ADDRESS = http://localhost:4000
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.
+3
View File
@@ -1,6 +1,8 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-1666992508674643"
crossorigin="anonymous"></script>
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-5YD59QXYF5"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=G-5YD59QXYF5"></script>
<script> <script>
@@ -16,6 +18,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Parkinsans:wght@300..800&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Share Syllabus helps students preview course materials before enrolling—no biased reviews, just real syllabi. Get insights into class content, expectations, and workload before you commit!" /> <meta name="description" content="Share Syllabus helps students preview course materials before enrolling—no biased reviews, just real syllabi. Get insights into class content, expectations, and workload before you commit!" />
+7918
View File
File diff suppressed because it is too large Load Diff
+12 -4
View File
@@ -14,11 +14,13 @@
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.12.6", "@apollo/client": "^3.12.6",
"@mdxeditor/editor": "^3.25.1",
"@million/lint": "latest",
"@nextui-org/select": "^2.4.9", "@nextui-org/select": "^2.4.9",
"@react-pdf/renderer": "^4.2.1", "@react-pdf/renderer": "^4.2.1",
"@syncfusion/ej2-react-pdfviewer": "^28.2.9",
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
"@types/pdf-viewer-reactjs": "^2.2.5", "@types/pdf-viewer-reactjs": "^2.2.5",
"@types/react-autocomplete": "^1.8.11",
"@types/react-autosuggest": "^10.1.11", "@types/react-autosuggest": "^10.1.11",
"accessible-autocomplete": "^3.0.1", "accessible-autocomplete": "^3.0.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
@@ -26,22 +28,28 @@
"document-viewer-ts": "^1.0.0-legacy", "document-viewer-ts": "^1.0.0-legacy",
"flowbite": "^2.5.2", "flowbite": "^2.5.2",
"flowbite-react": "^0.10.2", "flowbite-react": "^0.10.2",
"g4f": "^1.4.6",
"gh-pages": "^6.3.0", "gh-pages": "^6.3.0",
"graphql": "^16.10.0", "graphql": "^16.10.0",
"is-mobile": "^5.0.0", "is-mobile": "^5.0.0",
"million": "^3.1.11",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"pdf-viewer-reactjs": "^2.2.3", "pdf-viewer-reactjs": "^2.2.3",
"pdf2json": "^3.1.5",
"quill-markdown": "^2.1.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-autocomplete": "^1.8.1",
"react-autosuggest": "^10.1.0", "react-autosuggest": "^10.1.0",
"react-datasheet-grid": "^4.11.5", "react-datasheet-grid": "^4.11.5",
"react-doc-viewer": "^0.1.14", "react-doc-viewer": "^0.1.14",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-fast-marquee": "^1.6.5", "react-fast-marquee": "^1.6.5",
"react-markdown": "^9.*",
"react-pro-sidebar": "^1.1.0", "react-pro-sidebar": "^1.1.0",
"react-quilljs": "^2.0.5",
"react-router-dom": "^7.1.3", "react-router-dom": "^7.1.3",
"react-select": "^5.10.0", "react-select": "^5.10.0",
"react-tabs": "^6.1.0", "react-tabs": "^6.1.0",
"remark-gfm": "^4.0.1",
"srpv-js": "^1.0.16", "srpv-js": "^1.0.16",
"tailwindcss": "^3.4.17" "tailwindcss": "^3.4.17"
}, },
@@ -49,13 +57,13 @@
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0", "eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0", "globals": "^15.14.0",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"typescript-eslint": "^8.18.2", "typescript-eslint": "^8.18.2",
"vite": "^6.0.5" "vite": "^6.0.5",
"@vitejs/plugin-react": "*"
} }
} }
+2
View File
@@ -0,0 +1,2 @@
### Hello WORLD
hello
-1
View File
@@ -1 +0,0 @@
VITE_SERVER_ADDRESS = http://192.168.1.202:4000
-275
View File
@@ -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
+27 -26
View File
@@ -1,6 +1,7 @@
import Marquee from "react-fast-marquee"; 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 { isMobile } from "is-mobile";
import { useState } from "react";
interface BlogPosts { interface BlogPosts {
@@ -16,7 +17,7 @@ function Layout({ children }: { children: React.ReactNode }) {
console.log('Layout rendering'); // Add this line console.log('Layout rendering'); // Add this line
const [marqueeSpeed, setMarqueeSpeed] = useState(15);
const uploadSyllabi = () => { const uploadSyllabi = () => {
navigate('/upload') navigate('/upload')
@@ -24,37 +25,37 @@ function Layout({ children }: { children: React.ReactNode }) {
} }
return ( return (
<> <div className="bg-[#fcfffd] text-[#051008] w-full h-full">
<div className={`place-items-center justify-center border-b-2 ${isMobile() ? "pt-0 flex" : "pt-4 grid grid-cols-5"} border-purple-500`}> <div className={`w-full text-left p-1 flex place-items-center justify-center border-b border-gray-300 `}>
<div></div> <div className="flex-1 gap-4 place-items-center sticky grid grid-cols-3">
<div className={`${isMobile() ? "w-full" : "col-span-3"} p-4`}> <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 flex-row"> <div className="flex-1 flex place-items-center text-center">
<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> <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> </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> </Marquee>
</button>
<span className="material-symbols-rounded p-1 text-black">add</span>
</div> </div>
<div className="flex gap-4 font-light text-gray-600">
<div className={`w-full ${isMobile() ? "p-0" : "p-4"} flex place-items-center justify-between text-sm`}> <Link to="/moderator" className=" text-gray-600">Moderator Panel</Link>
<a href="https://shaded-yttrium-0dc.notion.site/Share-Syllabus-Blog-1a98dea271f580a4aa48fd791a8c83da?pvs=74">Blog</a> <Link to="/blog" className="text-gray-600">Blog</Link>
<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> </div>
</div> </div>
<div></div>
</div> </div>
{children} {children}
</> </div>
) )
} }
-39
View File
@@ -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;
+4 -3
View File
@@ -2,6 +2,7 @@ import { useParams } from "react-router-dom";
import DocViewer, { DocViewerRenderers } from "react-doc-viewer"; import DocViewer, { DocViewerRenderers } from "react-doc-viewer";
import {PDFViewer} from 'srpv-js' import {PDFViewer} from 'srpv-js'
import { isMobile } from 'is-mobile'; import { isMobile } from 'is-mobile';
import server from "../main";
@@ -30,7 +31,7 @@ function View({passedFile, className}: ViewParams) {
const isPDF = checkIfPdf(file || ""); const isPDF = checkIfPdf(file || "");
const docs = [ const docs = [
{ uri: `https://api.sharesyllabus.me/files/${file}` } { uri: `${server}/files/${file}` }
] ]
@@ -54,7 +55,7 @@ function View({passedFile, className}: ViewParams) {
{isMobile() === false && {isMobile() === false &&
<> <>
{isPDF {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>} : <DocViewer className="flex-1 w-full rounded-xl" pluginRenderers={DocViewerRenderers} documents={docs}></DocViewer>}
</> </>
@@ -62,7 +63,7 @@ function View({passedFile, className}: ViewParams) {
{isMobile() === true && {isMobile() === true &&
<> <>
{isPDF {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>} : <DocViewer className="flex-1 w-full rounded-xl" pluginRenderers={DocViewerRenderers} documents={docs}></DocViewer>}
</> </>
} }
+28
View File
@@ -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 "../syllabuslisting.css";
import "./App.tsx"; import "../pages/App.tsx";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
@@ -19,6 +19,7 @@ export interface ClassListing {
professor: string; professor: string;
fileName: string; fileName: string;
description: string; description: string;
content?: string;
classLength: number; classLength: number;
reviewed?: boolean; reviewed?: boolean;
createdByName?: string; createdByName?: string;
@@ -98,11 +99,11 @@ function SyllabusListing({ className, school, professor, professorId, fileName,
return ( 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"> <div className="flex text-left flex-col w-1/4">
<span className="font-extrabold">{className}</span> <span className="font-extrabold">{className}</span>
<span className="text-xs text-gray-300">{fullClassName}</span> <span className="text-xs text-gray-800">{fullClassName}</span>
<div className="flex text-gray-300 text-xs"> <div className="flex text-gray-800 text-xs">
<text>🏫 {school?.fullName}</text> <text>🏫 {school?.fullName}</text>
</div> </div>
{textbookCostElement()} {textbookCostElement()}
@@ -114,7 +115,7 @@ function SyllabusListing({ className, school, professor, professorId, fileName,
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="flex-1"></span> <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> <span className="material-symbols-rounded text-2xl">more_horiz</span>
<div className="relative" hidden={hideMore}> <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"> <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
View File
@@ -1,16 +1,21 @@
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { Route, Routes, HashRouter } from 'react-router-dom'; import { Route, Routes, HashRouter } from 'react-router-dom';
import './index.css'; import './index.css';
import App from './App.tsx'; import App from './pages/App.tsx';
import View from './View.tsx'; import View from './components/FileView.tsx';
import Admin from './Admin.tsx'; import Admin from './pages/Admin.tsx';
import Flag from './Flag.tsx'; import Flag from './pages/Flag.tsx';
import Reports from './Reports.tsx'; import Reports from './pages/Reports.tsx';
import Syllabus from './Syllabus.tsx'; import Syllabus from './pages/Syllabus.tsx';
import Layout from './Layout.tsx'; import Layout from './Layout.tsx';
import UploadSyllabi from './UploadSyllabi.tsx'; import UploadSyllabi from './pages/UploadSyllabi.tsx';
import Professor from './Professor.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( createRoot(document.getElementById('root')!).render(
<HashRouter> <HashRouter>
@@ -19,16 +24,14 @@ createRoot(document.getElementById('root')!).render(
<Route path="/admin" element={<Admin />} /> <Route path="/admin" element={<Admin />} />
<Route path="/admin/:highlightedSyllabusId" element={<Admin />} /> <Route path="/admin/:highlightedSyllabusId" element={<Admin />} />
<Route path="/reports" element={<Reports />} /> <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> </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> </HashRouter>
); );
+5 -4
View File
@@ -1,8 +1,9 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { DataSheetGrid, textColumn, checkboxColumn, Column, keyColumn, intColumn } from 'react-datasheet-grid'; import { DataSheetGrid, textColumn, checkboxColumn, Column, keyColumn, intColumn } from 'react-datasheet-grid';
import "react-datasheet-grid/dist/style.css"; 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 { useParams } from "react-router-dom";
import server from '../main'
interface PreviewButtonProps { interface PreviewButtonProps {
@@ -57,7 +58,7 @@ function Admin() {
try { try {
let formdata = new FormData(); let formdata = new FormData();
formdata.append("username", username); formdata.append("username", username);
const response = await fetch(`https://api.sharesyllabus.me/admin/wholedatabase`, { const response = await fetch(`${server}/admin/wholedatabase`, {
method: "POST", method: "POST",
body: formdata, body: formdata,
}); });
@@ -71,7 +72,7 @@ function Admin() {
} else { } else {
try { try {
console.log(highlightedSyllabusId); console.log(highlightedSyllabusId);
const response = await fetch(`https://api.sharesyllabus.me/syllabus/${highlightedSyllabusId}`, { const response = await fetch(`${server}/syllabus/${highlightedSyllabusId}`, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -92,7 +93,7 @@ function Admin() {
for (const change of databaseChanges) { for (const change of databaseChanges) {
const { fileName, ...sendableChange } = change; const { fileName, ...sendableChange } = change;
const response = await fetch(`https://api.sharesyllabus.me/admin/change/`, { const response = await fetch(`${server}/admin/change/`, {
method: "POST", method: "POST",
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
+55
View File
@@ -4,6 +4,61 @@
place-items: center; 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 { @keyframes scroll-left {
+377
View File
@@ -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
+24
View File
@@ -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
+3 -1
View File
@@ -1,5 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import server from '../main'
function Flag() { function Flag() {
@@ -11,7 +13,7 @@ function Flag() {
const [reportBy, setReportBy] = useState(""); const [reportBy, setReportBy] = useState("");
function submitReport() { function submitReport() {
fetch(`https://api.sharesyllabus.me/report/${syllabusId}`, { fetch(`${server}/report/${syllabusId}`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
+51
View File
@@ -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
+3 -5
View File
@@ -1,6 +1,6 @@
import mime from "mime-types"; import mime from "mime-types";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ClassListing } from "./syllabus_comp"; import { ClassListing } from "./components/syllabus_comp";
export interface SyllabusReport { export interface SyllabusReport {
id: string; id: string;
@@ -17,7 +17,7 @@ function Reports() {
const [reports, setReports] = useState<SyllabusReport[]>([]); const [reports, setReports] = useState<SyllabusReport[]>([]);
function obtainReports() { function obtainReports() {
fetch("https://api.sharesyllabus.me/admin/getreports", { fetch(`${server}/admin/getreports`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -26,7 +26,6 @@ function Reports() {
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
console.log(data);
setReports(data); setReports(data);
}) })
.catch((e) => { .catch((e) => {
@@ -45,7 +44,7 @@ function Reports() {
function viewReport(sId: string, report: SyllabusReport) { function viewReport(sId: string, report: SyllabusReport) {
setCurrentReport(report); setCurrentReport(report);
fetch(`https://api.sharesyllabus.me/admin/getsyllabus/${sId}`, { fetch(`${server}/admin/getsyllabus/${sId}`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -54,7 +53,6 @@ function Reports() {
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
console.log(data);
setCurrentSyllabus(data); setCurrentSyllabus(data);
}) })
.catch((e) => { .catch((e) => {
+50
View File
@@ -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"; import React, { useEffect, useState } from "react";
export interface Class { export interface Class {
id: number; id: number;
className: string; className: string;
@@ -20,7 +19,6 @@ export interface Professor {
} }
function UploadSyllabi() { function UploadSyllabi() {
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [college, setCollege] = useState(""); const [college, setCollege] = useState("");
const [professor, setProfessor] = useState(""); const [professor, setProfessor] = useState("");
@@ -40,7 +38,7 @@ function UploadSyllabi() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
fetch("https://api.sharesyllabus.me/schools", { fetch(`https://api.sharesyllabus.me/schools`, {
method: "GET", method: "GET",
}).then((res) => res.json()).then((data) => { }).then((res) => res.json()).then((data) => {
setCollegeOptions(data); setCollegeOptions(data);
@@ -56,19 +54,17 @@ function UploadSyllabi() {
function searchProfessor(e: React.ChangeEvent<HTMLInputElement>) { function searchProfessor(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value; const value = e.target.value;
setProfessor(value); setProfessor(value);
fetch(`https://api.sharesyllabus.me/search/professor/?q=${value}&s=${college}`, { fetch(`https:/search/professor/?q=${value}&s=${college}`, {
method: "POST", method: "POST",
}).then((res) => res.json()).then((data) => { }).then((res) => res.json()).then((data) => {
setProfessorOptions(data); setProfessorOptions(data);
} });
)
} }
function searchClassAbrv(e: React.ChangeEvent<HTMLInputElement>) { function searchClassAbrv(e: React.ChangeEvent<HTMLInputElement>) {
setClassAbrvQuery(e.target.value); setClassAbrvQuery(e.target.value);
fetch(`${server}/search/class/?q=${classAbrvQuery}&s=${college}`, {
fetch(`https://api.sharesyllabus.me/search/class/?q=${classAbrvQuery}&s=${college}`, {
method: "POST", method: "POST",
}).then((res) => res.json()).then((data) => { }).then((res) => res.json()).then((data) => {
setClassAbrvOptions(data); setClassAbrvOptions(data);
@@ -103,9 +99,9 @@ function UploadSyllabi() {
formData.append("createdByName", name); formData.append("createdByName", name);
formData.append("createdByEmail", email); formData.append("createdByEmail", email);
console.log(formData) console.log(formData);
fetch("https://api.sharesyllabus.me/create", { fetch(`https://api.sharesyllabus.me/create`, {
method: "POST", method: "POST",
body: formData, body: formData,
}) })
@@ -117,7 +113,7 @@ function UploadSyllabi() {
setError(data.msg); setError(data.msg);
} }
setIsLoading(false); setIsLoading(false);
}) });
} }
useEffect(() => { useEffect(() => {
@@ -129,17 +125,16 @@ function UploadSyllabi() {
console.log(classDescription); console.log(classDescription);
console.log(name); console.log(name);
console.log(email); console.log(email);
}, []) }, []);
return ( return (
<div className="w-full flex place-items-center justify-center p-10"> <div className="w-full flex place-items-center justify-center p-10">
<div className="w-1/2 text-left flex flex-col"> <div className="w-1/2 text-left flex flex-col">
<text className="text-3xl font-bold">Upload a Syllabus!</text> <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> <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"> <div className="flex flex-col pt-2 gap-2">
<text>Syllabus (pdf, docx, etc.)</text> <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> <text>College District (not school!):</text>
<input <input
list="collegeOptions" list="collegeOptions"
@@ -147,7 +142,7 @@ function UploadSyllabi() {
onChange={(e) => setCollege(e.target.value)} onChange={(e) => setCollege(e.target.value)}
placeholder="Search or select college district" placeholder="Search or select college district"
required required
className="p-1 rounded-md" className="p-1 rounded-md bg-white text-black border"
/> />
<datalist id="collegeOptions"> <datalist id="collegeOptions">
{collegeOptions.map((college: School, index: number) => ( {collegeOptions.map((college: School, index: number) => (
@@ -155,34 +150,34 @@ function UploadSyllabi() {
))} ))}
</datalist> </datalist>
<text>Class Name:</text> <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"> <datalist id="classAbrvOptions" className="w-full bg-white">
{classAbrvOptions?.map((classOption, index) => ( {classAbrvOptions?.map((classOption, index) => (
<option key={index} value={classOption.className} /> <option key={index} value={classOption.className} />
))} ))}
</datalist> </datalist>
<text>Professor:</text> <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"> <datalist id="professorOptions">
{professorOptions.map((professor, index) => ( {professorOptions.map((professor, index) => (
<option key={index} value={professor.name} /> <option key={index} value={professor.name} />
))} ))}
</datalist> </datalist>
<text>Duration of class in weeks: [OPTIONAL: community service hours]</text> <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> <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>free</option>
<option>cheap</option> <option>cheap</option>
<option>moderate</option> <option>moderate</option>
<option>expensive</option> <option>expensive</option>
</select> </select>
<text>Class description in at most 2 sentences (NOT a description of the teacher) [OPTIONAL: community service hours]:</text> <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> <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> <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"> <div className="text-right">
<button <button
className="bg-blue-600 rounded-lg p-2 disabled:opacity-50" className="bg-blue-600 rounded-lg p-2 disabled:opacity-50"
+17
View File
@@ -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
View File
@@ -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. &nbsp;</text>
</Marquee>
</div>
</div>
)
}
export default Welcome;
+17 -4
View File
@@ -1,15 +1,28 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
const flowbite = require("flowbite-react/tailwind");
export default { export default {
content: ["./src/**/*.{html,js,tsx}", flowbite.content()], content: ["./src/**/*.{html,js,tsx}"],
theme: { theme: {
extend: { extend: {
fontFamily: { fontFamily: {
sserif: ["Source Serif 4", "serif"], sserif: ["Source Serif 4", "serif"],
} },
backgroundImage: {
'gradient-stripes': 'linear-gradient(135deg, #e2e8f0 25%, #f8fafc 25%, #f8fafc 50%, #e2e8f0 50%, #e2e8f0 75%, #f8fafc 75%, #f8fafc)',
},
backgroundSize: {
'stripe-size': '200% 200%',
},
animation: {
'parallax-flash': 'parallax 1.5s linear infinite',
},
keyframes: {
parallax: {
'0%': { backgroundPosition: '0% 0%' },
'100%': { backgroundPosition: '200% 200%' },
},
},
}, },
}, },
plugins: [flowbite.plugin()],
} }
+1 -1
View File
@@ -22,5 +22,5 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["src"] "include": ["src", "../new/src/components/welcome.tsx"]
} }
+5 -1
View File
@@ -1,8 +1,12 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import MillionLint from "@million/lint"
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
base: '/' base: '/',
server: {
port: 3000,
}
}) })