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:
@@ -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
@@ -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!" />
|
||||||
|
|||||||
Generated
+7918
File diff suppressed because it is too large
Load Diff
+12
-4
@@ -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": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
### Hello WORLD
|
||||||
|
hello
|
||||||
-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
|
|
||||||
+28
-27
@@ -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 {
|
||||||
@@ -9,14 +10,14 @@ interface BlogPosts {
|
|||||||
link: string;
|
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 }) {
|
function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 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>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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 {
|
||||||
@@ -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 { 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",
|
||||||
@@ -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 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) => {
|
||||||
@@ -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"
|
||||||
@@ -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;
|
||||||
+17
-4
@@ -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
@@ -22,5 +22,5 @@
|
|||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src", "../new/src/components/welcome.tsx"]
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -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,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user