textbook cost & class name

This commit is contained in:
Leo Wen
2025-02-17 21:44:48 -08:00
parent e344b2b371
commit 07f8895b91
9 changed files with 417 additions and 84 deletions
BIN
View File
Binary file not shown.
+4
View File
@@ -16,17 +16,21 @@
"@apollo/client": "^3.12.6",
"@nextui-org/select": "^2.4.9",
"@react-pdf/renderer": "^4.2.1",
"@types/mime-types": "^2.1.4",
"autoprefixer": "^10.4.20",
"flowbite": "^2.5.2",
"flowbite-react": "^0.10.2",
"gh-pages": "^6.3.0",
"graphql": "^16.10.0",
"mime-types": "^2.1.35",
"react": "^18.3.1",
"react-datasheet-grid": "^4.11.5",
"react-doc-viewer": "^0.1.14",
"react-dom": "^18.3.1",
"react-fast-marquee": "^1.6.5",
"react-pro-sidebar": "^1.1.0",
"react-router-dom": "^7.1.3",
"react-select": "^5.10.0",
"tailwindcss": "^3.4.17"
},
"devDependencies": {
+51 -26
View File
@@ -1,16 +1,17 @@
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, TextbookCost } from "./syllabus_comp";
import { useParams } from "react-router-dom";
import Select from 'react-select';
interface PreviewButtonProps {
fileName: string;
}
const PreviewButton = ({ fileName }: PreviewButtonProps) => {
const handlePreview = () => {
const url = `/#/view/${encodeURIComponent(fileName)}`;
const url = `#/view/${encodeURIComponent(fileName)}`;
window.open(url, '_blank');
};
@@ -21,7 +22,11 @@ const PreviewButton = ({ fileName }: PreviewButtonProps) => {
);
};
function Admin() {
const { highlightedSyllabusId } = useParams<{ highlightedSyllabusId?: string }>();
const [username, setUsername] = useState("");
const [referenceDatabase, setReferenceDatabase] = useState<ClassListing[]>([]);
const [wholeDatabase, setWholeDatabase] = useState<ClassListing[]>([]);
@@ -32,39 +37,62 @@ function Admin() {
title: "Preview",
},
{ ...keyColumn("reviewed", checkboxColumn), title: "Approved" },
{ ...keyColumn("className", textColumn), title: "Class Name" },
{ ...keyColumn("className", textColumn), title: "Class Abbrev" },
{ ...keyColumn("fullClassName", textColumn), title: "Class Name" },
{ ...keyColumn("textbookCost", textColumn), title: "free, cheap, moderate, expensive" },
{ ...keyColumn("description", textColumn), title: "Description" },
{ ...keyColumn("professor", textColumn), title: "Instructor" },
{ ...keyColumn("length", intColumn), title: "Length" },
{ ...keyColumn("classLength", intColumn), title: "Length" },
{ ...keyColumn("createdByName", textColumn), title: "Submitted By" },
{ ...keyColumn("createdByEmail", textColumn), title: "Email" },
];
useEffect(() => {
console.log(`highlightedSyllabusId is: ${highlightedSyllabusId}`);
if (highlightedSyllabusId != null) fetchWholeDatabase();
}, [highlightedSyllabusId]);
const fetchWholeDatabase = async () => {
if (!highlightedSyllabusId) {
try {
let formdata = new FormData();
formdata.append("username", username);
var response = await fetch(`https://api.sharesyllabus.me/admin/wholedatabase`, {
const response = await fetch(`https://api.sharesyllabus.me/admin/wholedatabase`, {
method: "POST",
body: formdata,
});
var data = await response.json();
const data = await response.json();
console.log(data);
setWholeDatabase(data);
setReferenceDatabase(data);
} catch {
console.log("error");
} catch (error) {
console.log("error", error);
}
} else {
try {
console.log(highlightedSyllabusId);
const response = await fetch(`https://api.sharesyllabus.me/syllabus/${highlightedSyllabusId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
}
});
const data = await response.json();
console.log(`data is: ${data}`);
setWholeDatabase([...wholeDatabase, data]);
setReferenceDatabase([...referenceDatabase, data]);
} catch (error) {
console.log("error", error);
}
}
};
const changeServerDatabase = async () => {
try {
databaseChanges.forEach(async (change) => {
for (const change of databaseChanges) {
const { fileName, ...sendableChange } = change;
var response = await fetch(`https://api.sharesyllabus.me/admin/change/`, {
const response = await fetch(`https://api.sharesyllabus.me/admin/change/`, {
method: "POST",
headers: {
'Content-Type': 'application/json'
@@ -74,22 +102,18 @@ function Admin() {
id: sendableChange.id,
data: sendableChange,
})
}).then((res) => {
if (res.ok) {
console.log("success");
});
if (!response.ok) {
console.log("error");
}
}
fetchWholeDatabase();
alert("success!");
} else {
console.log("error");
}
})
console.log(response);
});
} catch {
console.log("error");
}
} catch (error) {
console.log("error", error);
}
};
useEffect(() => {
const changes = wholeDatabase.filter((item, index) => {
@@ -101,6 +125,7 @@ function Admin() {
return (
<div className="flex flex-col items-center justify-center w-full min-h-screen p-4">
<div>{highlightedSyllabusId}</div>
<div className="text-black mb-4 w-full flex">
<input
type="text"
+5 -2
View File
@@ -16,6 +16,9 @@ export const classOptions = [ // TODO REMOVE
{ key: "physics", label: "Physics" },
];
//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
var searchQuery = "";
function findIfMobile() {
@@ -179,7 +182,7 @@ function App() {
</div>
<div className="listing">
{syllabi.map((syllabus, i) => {
return <SyllabusListing key={i} fileName={syllabus.fileName} className={syllabus.className} description={syllabus.description} classLength={syllabus.classLength} professor={syllabus.professor} />
return <SyllabusListing key={i} fileName={syllabus.fileName} className={syllabus.className} description={syllabus.description} fullClassName={syllabus.fullClassName} textbookCost={syllabus.textbookCost} classLength={syllabus.classLength} professor={syllabus.professor} />
})}
</div>
@@ -209,7 +212,7 @@ function App() {
<div className="listing">
{syllabi.map((syllabus, i) => {
return <SyllabusListing key={i} fileName={syllabus.fileName} className={syllabus.className} description={syllabus.description} classLength={syllabus.classLength} professor={syllabus.professor} />
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>
+65
View File
@@ -0,0 +1,65 @@
import { useState } from "react";
import { useParams } from "react-router-dom";
function Flag() {
const params = useParams();
const syllabusId = params.id;
const [reportTitle, setReportTitle] = useState("");
const [reportBody, setReportBody] = useState("");
const [reportBy, setReportBy] = useState("");
function submitReport() {
fetch(`https://api.sharesyllabus.me/report/${syllabusId}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
reportType: "syllabus",
reportTitle,
reportBody,
reportBy,
syllabusId
}),
})
.then((res) => res.json())
.then((data) => {
if (data.msg) {
alert(data.msg);
} else {
alert("Report submitted successfully");
}
})
.catch((e) => {
console.error(e);
alert("An error occurred while submitting the report");
});
}
return (
<div className="w-full h-full p-8 ">
<div className="w-full h-full space-y-6 bg-zinc-950 shadow-lg flex flex-col shadow-black p-8 rounded-lg">
<nav>
<text className="text-4xl font-semibold flex items-center justify-center align-middle">🚩 File a report on a Syllabus</text>
<text className="text-gray-400 text-xs">syllabus: {syllabusId}</text>
</nav>
<div className="space-y-6 flex-1 flex flex-col text-left">
<div className="flex justify-center items-center">
<text className="flex-1">Title of Report:</text>
<button onClick={submitReport} className="justify-center rounded-md p-2 flex font-bold bg-blue-700">Submit Report<span className="material-symbols-rounded pl-2">mail</span></button>
</div>
<input className="w-full p-2 rounded-lg" value={reportTitle} placeholder="Enter the title of your report" onChange={(e) => {setReportTitle(e.target.value)}}></input>
<text className="pt-1 pb-1">Email (OPTIONAL: to get in touch):</text>
<input className="w-full p-2 rounded-lg" value={reportBy} placeholder="example@company.com" onChange={(e) => setReportBy(e.target.value)}></input>
<text className="pt-1 pb-1">Details:</text>
<textarea className="w-full p-2 flex-1 rounded-lg text-black" value={reportBody} placeholder="What is wrong with this syllabus?: incorrect class for syllabus, bad description...." onChange={(e) => setReportBody(e.target.value)}></textarea>
</div>
</div>
</div>
)
}
export default Flag;
+171
View File
@@ -0,0 +1,171 @@
import mime from "mime-types";
import { useEffect, useState } from "react";
import { ClassListing } from "./syllabus_comp";
export interface SyllabusReport {
id: string;
reportTitle: string;
reportBody: string;
reportBy: string;
reportDate: string;
syllabusId: string;
}
function Reports() {
const [username, setUsername] = useState("");
const [reports, setReports] = useState<SyllabusReport[]>([]);
function obtainReports() {
fetch("https://api.sharesyllabus.me/admin/getreports", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username }),
})
.then((res) => res.json())
.then((data) => {
console.log(data);
setReports(data);
})
.catch((e) => {
console.error(e);
});
}
const [currentReport, setCurrentReport] = useState<SyllabusReport | null>(null);
const [currentSyllabus, setCurrentSyllabus] = useState<ClassListing | null>(null);
const [currentFileName, setCurrentFileName] = useState("");
useEffect(() => {
if (currentSyllabus == null || currentSyllabus?.mimeType == null) return;
setCurrentFileName(`${currentSyllabus?.id}.${mime.extension(currentSyllabus.mimeType)}`)
}, [currentSyllabus])
function viewReport(sId: string, report: SyllabusReport) {
setCurrentReport(report);
fetch(`https://api.sharesyllabus.me/admin/getsyllabus/${sId}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username }),
})
.then((res) => res.json())
.then((data) => {
console.log(data);
setCurrentSyllabus(data);
})
.catch((e) => {
console.error(e);
});
}
async function previewSyllabus() {
const url = `#/view/${encodeURIComponent(currentFileName)}`;
window.open(url, '_blank');
}
async function editSyllabusEntry() {
const url = `#/admin/${currentSyllabus?.id}`;
window.open(url, '_blank');
}
return (
<div className="flex flex-col items-center justify-center w-full min-h-screen p-4">
<div className="text-black mb-4 w-full flex">
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
obtainReports()
}
}}
className="p-2 border border-gray-300 rounded"
/>
<button
onClick={obtainReports}
className="ml-2 p-2 bg-blue-500 text-white rounded"
>
Obtain Reports
</button>
<div className="flex-1 flex items-center justify-end">
<span className="text-white">
</span>
</div>
</div>
<div className="flex-1 w-full h-full">
<div className=" w-full h-full grid grid-cols-4 gap-4">
{reports.map((report, i) => <ReportComponent report={report} viewReport={viewReport} key={i} />)}
</div>
</div>
<div className="w-full bg-zinc-950 grid grid-cols-2 p-4 rounded-md">
<div className="flex flex-col gap-2 text-left">
<text className="font-bold">
{currentReport?.reportTitle}
</text>
<text>
{currentReport?.reportBody}
</text>
</div>
<div className="bg-white p-4 rounded-md text-black flex">
<div className="flex flex-1 flex-col text-left">
<text className="font-bold">
{currentSyllabus?.className}
</text>
<text>
{currentSyllabus?.professor}
</text>
<text>
{currentSyllabus?.description}{currentSyllabus?.fileName}
</text>
<text>
classLength: {currentSyllabus?.classLength} wk
</text>
</div>
<div className="flex-col flex gap-2">
<button className="aspect-square bg-blue-600 p-2 rounded-md text-yellow-300" onClick={previewSyllabus}><span className="material-symbols-rounded">open_in_new</span></button>
<button className="flex-1 bg-yellow-300 p-2 rounded-md flex items-center" onClick={editSyllabusEntry}><span className="material-symbols-rounded">edit</span></button>
</div>
</div>
</div>
</div >
)
}
interface ReportComponentProps {
report: SyllabusReport;
viewReport: (sId: string, report: SyllabusReport) => void;
}
function ReportComponent({ report, viewReport }: ReportComponentProps) {
const date = new Date(report.reportDate).toLocaleDateString("en-US");
const view = (e: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
e.stopPropagation();
viewReport(report.syllabusId, report);
}
return (
<div className="bg-white text-black p-2 rounded-md text-left flex flex-col hover:bg-gray-200" onClick={view}>
<div className="flex items-center space-x-2">
<text className="font-bold text-lg">{report.reportTitle}</text>
<text className="font-light text-md text-gray-500 flex-1">{date}</text>
<button className="shadow-lg rounded-md aspect-square text-white bg-black p-1 items-center flex hover:bg-red-600"><span className="material-symbols-rounded text-md">delete</span></button>
</div>
<text>{report.reportBody}</text>
<text className="text-xs font-light">{report.reportBy}</text>
</div>
)
}
export default Reports;
+1
View File
@@ -16,6 +16,7 @@ function View() {
const { file } = useParams();
// TODO: make it work on mobile (pdf viewer doesen't work)
const isPDF = checkIfPdf(file || "");
+11 -12
View File
@@ -1,22 +1,21 @@
import { createRoot } from 'react-dom/client'
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 './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';
createRoot(document.getElementById('root')!).render(
<HashRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/view/:file" element={<View />} />
<Route path="/admin" element={<Admin />} />
<Route path="/admin/:highlightedSyllabusId" element={<Admin />} />
<Route path="/flag/:id" element={<Flag />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</HashRouter>
)
);
+69 -4
View File
@@ -2,6 +2,16 @@ import "./syllabuslisting.css";
import "./App.tsx";
import { useNavigate } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
export enum TextbookCost {
free,
cheap,
moderate,
expensive
}
// TODO: implement fullClassName and textbook cost in mobile
export interface ClassListing {
className: string;
@@ -12,13 +22,35 @@ export interface ClassListing {
reviewed?: boolean;
createdByName?: string;
createdByEmail?: string;
mimeType?: string;
textbookCost?: string;
fullClassName?: string;
id?: string;
}
function findIfMobile() {
//return true;
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
function SyllabusListing({ className, professor, fileName, description, classLength }: ClassListing) {
function SyllabusListing({ className, professor, fileName, description, classLength, fullClassName, textbookCost }: ClassListing) {
const [hideMore, setHideMore] = useState(true);
const buttonRef = useRef<HTMLButtonElement>(null);
const id = fileName.split(".")[0];
const handleClickOutside = (event: MouseEvent) => {
if (buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
setHideMore(true);
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const navigate = useNavigate();
@@ -26,6 +58,22 @@ function SyllabusListing({ className, professor, fileName, description, classLen
navigate(`/view/${encodeURIComponent(`${fileName}`)}`);
}
async function handleFlag() {
navigate(`/flag/${encodeURIComponent(`${id}`)}`);
}
const textbookCostElement = () => {
if (textbookCost === "free") {
return <span className="text-xs text-green-500 font-bold">🤑 Free</span>;
} else if (textbookCost === "cheap") {
return <span className="text-xs text-yellow-400 font-bold">💸 Cheap</span>;
} else if (textbookCost === "moderate") {
return <span className="text-xs text-orange-600 font-bold">💵 Moderate</span>;
} else {
return <span className="text-xs text-red-600 font-bold">💰 Expensive</span>;
}
}
if (!findIfMobile()) {
@@ -34,13 +82,30 @@ function SyllabusListing({ className, professor, fileName, description, classLen
<div className="w-full flex space-x-4 bg-white text-black rounded-lg p-3" >
<div className="flex text-left flex-col w-1/4">
<span className="font-extrabold">{className}</span>
<span className="text-xs text-gray-600">{fullClassName}</span>
{textbookCostElement()}
<span className="flex-1 italic">{professor}</span>
{classLength > 7 ? <span className="text-xs font-light text-green-600">{classLength} weeks course (normal)</span> : <span className="text-xs font-light text-red-600">{classLength} weeks course</span>}
{classLength > 7 ? <span className="text-xs font-light text-green-600">{classLength} weeks course</span> : <span className="text-xs font-light text-red-600">{classLength} weeks course</span>}
</div>
<div className="flex-1 flex flex-col text-center">
<span className="w-3/4 font-serif">"{description}"</span>
</div>
<button className="aspect-square flex items-center align-middle justify-center rounded-lg hover:bg-slate-200 text-black" onClick={handleView}><span className="material-symbols-rounded text-2xl">open_in_new</span></button>
<div className="flex flex-col">
<button className="aspect-square flex items-center align-middle justify-center hover:bg-slate-200 rounded-sm text-black" onClick={handleView}>
<span className="material-symbols-rounded flex-1 text-2xl">open_in_new</span>
</button>
<span className="flex-1"></span>
<button className="aspect-square flex items-center align-middle justify-center hover:bg-slate-200 rounded-sm text-black" 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">
<span className="material-symbols-rounded text-red-600 text-2xl">flag</span>
</button>
</div>
</button>
</div>
</div>
</>
)
@@ -52,7 +117,7 @@ function SyllabusListing({ className, professor, fileName, description, classLen
<span className=" font-extrabold">{className}</span>
<span className="flex-1 italic">{professor}</span>
<span className="w-4/5 font-serif">"{description}"</span>
{classLength > 7 ? <span className="text-xs font-light text-green-600">{classLength} weeks course (normal)</span> : <span className="text-xs font-light text-red-600">{classLength} weeks course</span>}
{classLength > 7 ? <span className="text-xs font-light text-green-600">{classLength} weeks course</span> : <span className="text-xs font-light text-red-600">{classLength} weeks course</span>}
</div>
<button className="flex items-center h-full p-2 rounded-lg bg-slate-200 text-black" onClick={handleView}>
<span className="material-symbols-rounded text-2xl">open_in_new</span>