Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix all major bugs, add about page #24

Merged
merged 8 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import NavBar from "@/components/navBar";

export default function Home() {
return (
<main>
<NavBar />
<div className="page-main">
<h1>About CRLSpen</h1>
<h2>Overview</h2>
<div className="about-text">
CRLSpen is a open-source web app made by and for CRLS students. It allows students to check their grades, assignments, schedule, and GPA.
<br />
<br />
The app is built using Next.js, a React framework, and is hosted on Vercel. The backend is written in TypeScript and scrapes data from Aspen, the student information system used by CRLS.
<br />
<br />
The app is not affiliated with CRLS or the Cambridge Public School District.
<br />
<br />
The source code for the app can be found on <a href="https://github.com/Aspine/crlspen">our GitHub repository</a>.
</div>
<h2>Deployment & Data</h2>
<div className="about-text">
CRLSpen is currently deployed on Vercel, a deployment which can be found <a href="https://crlspen-deploy.vercel.app/">here</a>.
<br />
<br />
The app is hosted on a free plan, which means that it may be slow to load and may have downtime.
<br />
<br />
Due to the nature of scraping Aspen, your username and password are transferred to Vercel servers. We do not store your credentials, but we cannot guarantee that they are not stored by Vercel.
<br />
<br />
If you are concerned about privacy, you can host the app yourself. Instructions for doing so can be found below.
</div>
<h2>Self-Hosting</h2>
<div className="about-text">
If you would like to host the app yourself, you can do so by following these steps:
<ol>
<li>Clone the repository from <a href="https://github.com/Aspine/crlspen">here</a></li>
<li>Install Node.js and npm</li>
<li>Run <code>npm install</code> in the root directory of the repository</li>
<li>Run <code>npm run dev</code> to start the development server</li>
<li>Access the app at <code>http://localhost:3000</code> in your browser</li>
</ol>
</div>
</div>
</main>
)
}
5 changes: 2 additions & 3 deletions src/app/api/get_assignments_current/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ export async function GET(req: NextRequest, res: NextResponse) {

const sessionId = cookies().get("sessionId")?.value;
var apacheToken = cookies().get("apacheToken")?.value;
const classesListUnparsed = cookies().get("classDataQ3")?.value;
const classesList = classesListUnparsed ? JSON.parse(classesListUnparsed) : [];
const classes = classesList.length;
const classesUnparsed = cookies().get("classDataLength")?.value;
const classes: number = classesUnparsed ? +classesUnparsed | 0 : 0;
var assingmentsList: Assignment[][] = [];

await fetch("https://aspen.cpsd.us/aspen/portalAssignmentList.do?navkey=academics.classes.list.gcd", {
Expand Down
62 changes: 62 additions & 0 deletions src/app/api/get_class_info_current/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import cheerio from "cheerio";
import { Assignment } from "@/types";
import { getCurrentQuarterOid } from "@/utils/getCurrentQuarter";
import { processAssignments } from "@/utils/processData";

export async function GET(req: NextRequest, res: NextResponse) {
try {
const startTime = new Date();

const sessionId = cookies().get("sessionId")?.value;
var apacheToken = cookies().get("apacheToken")?.value;
const classesListUnparsed = cookies().get("classDataQ3")?.value;
const classesList = classesListUnparsed ? JSON.parse(classesListUnparsed) : [];
const classes = classesList.length;
var assingmentsList: Assignment[][] = [];

await fetch("https://aspen.cpsd.us/aspen/portalClassDetail.do?navkey=academics.classes.list.detail", {
headers: {
Cookie: `JSESSIONID=${sessionId}`
},
}).then(res => res.text()).then(html => {
});

for (let i = 0; i < classes - 1; i++) {
await fetch("https://aspen.cpsd.us/aspen/portalClassDetail.do?navkey=academics.classes.list.detail", {
headers: {
Cookie: `JSESSIONID=${sessionId}`
},
method: "POST",
body: new URLSearchParams({
"org.apache.struts.taglib.html.TOKEN": apacheToken ? apacheToken : "",
"userEvent": "60",
"gradeTermOid": getCurrentQuarterOid(),
}),
}).then(res => res.text()).then(async html => {
});
}

await fetch("https://aspen.cpsd.us/aspen/portalClassDetail.do?navkey=academics.classes.list.detail", {
headers: {
Cookie: `JSESSIONID=${sessionId}`
},
method: "POST",
body: new URLSearchParams({
"org.apache.struts.taglib.html.TOKEN": apacheToken ? apacheToken : "",
"userEvent": "80",
"gradeTermOid": getCurrentQuarterOid(),
}),
})

console.log(assingmentsList);

const elapsedTime = new Date().getTime() - startTime.getTime();
console.log("\x1b[32m ✓\x1b[0m scraped assignments in", elapsedTime, "ms");
return new Response(JSON.stringify(await assingmentsList), { status: 200 });
} catch (e) {
console.error(e);
return new Response("An error occurred.", { status: 500 });
}
}
24 changes: 9 additions & 15 deletions src/app/api/get_grade_data_current/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export async function GET(req: NextRequest, res: NextResponse) {
).then((res) => res.text()).then((html) => {
const $ = cheerio.load(html);

if (html.includes("You are not logged on or your session has expired.")) {
throw new Error("Session expired");
}

const apacheInput = $("input");
apacheToken = apacheInput.attr("value");

Expand Down Expand Up @@ -64,7 +68,7 @@ export async function GET(req: NextRequest, res: NextResponse) {
});

return classes;
})
});

cookies().set("apacheToken", apacheToken ? apacheToken : "");

Expand All @@ -73,23 +77,13 @@ export async function GET(req: NextRequest, res: NextResponse) {
endTimeClasses.getTime() - startTimeClasses.getTime();
console.log("\x1b[32m ✓\x1b[0m scraped class data in", elapsedTimeClasses, "ms");

cookies().set("classDataQ3", JSON.stringify(classesList));
// cookies().set("classDataQ3", JSON.stringify(classesList));
cookies().set("classDataLength", String(classesList.length));

return NextResponse.json({ text: classesList }, { status: 200 });
} catch (error) {
console.error("Error during scraping:", error);
if (res.status) {
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
} else {
console.error("res object does not have a status function");
}

return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);

return NextResponse.json({ text: "An error occurred" }, { status: 500 });
}
}
33 changes: 33 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ body {
box-shadow: 0 0 2px 1px white;
background-color: var(--background-color);
color: var(--text-color);
max-width: 250px;
width: 100%;
}

.login-box button {
Expand Down Expand Up @@ -323,4 +325,35 @@ a {

.class-row {
cursor: not-allowed;
}

.fraction-grade {
width: 1%;
white-space: nowrap;
padding-left: 20px;
}

.about-text {
font-size: 16px;
}

code {
background-color: var(--alt-background-color);
color: var(--text-color);
padding: 2.5px;
border-radius: 2.5px;
font-size: 14px;
}

li {
margin-bottom: 10px;
}

.about-link {
color: var(--text-color);
padding: 10px;
background: var(--background-color);
position: fixed;
bottom: 0;
right: 0;
}
118 changes: 66 additions & 52 deletions src/app/gradebook/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,26 @@ export default function Home() {
const [loading, setLoading] = useState(true);
const [loadingAssignment, setLoadingAssignment] = useState(true);
const [assignmentsTableContent, setAssignmentsTableContent] = useState<JSX.Element[]>([
<p className="placeholder-text" key={0}>click on a class to show assignments</p>
<tbody key={0}><tr><td><p className="placeholder-text">click on a class to show assignments</p></td></tr></tbody>
]);

useEffect(() => {
setClassData(JSON.parse(
decodeURIComponent(document.cookie.split(';').find(cookie => cookie.trim().startsWith("classDataQ3="))?.split('=')[1] || "[]")
));

setLoading(false);
}, [setClassData]);

const gpaInput = classData.map((data) => {
return {
grade: data.grade,
credits: getCredits(data.name),
ap: data.name.startsWith("AP"),
};
});
async function getData() {
const gradesResponse = await fetch("/api/get_grade_data_current/", {
method: "GET",
headers: {
"Content-Type": "application/json",
}
});

const hUnweightedGpa = calculateGpa(gpaInput, "hUnweighted");
const fUnweightedGpa = calculateGpa(gpaInput, "fUnweighted");
const fWeightedGpa = calculateGpa(gpaInput, "fWeighted");
if (gradesResponse.ok) {
setClassData((await gradesResponse.json()).text);
setLoading(false);
} else {
console.error("Failed to fetch grades data");
window.location.href = "/login";
}

useEffect(() => {
async function backgroundScrape() {
await fetch("/api/get_schedule_data", {
method: "GET",
});
Expand All @@ -46,39 +41,58 @@ export default function Home() {
method: "GET",
}).then(res => res.json());

// const classInfoDataQ3 = await fetch("/api/get_class_info_current", {
// method: "GET",
// }).then(res => res.json());

setAssignmentData(assignmentDataQ3);
setLoadingAssignment(false);
}

backgroundScrape();
}, ["/api/get_schedule_data/", "/api/get_assignments_current/", setAssignmentData])
getData();

function handleRowClick(index: number) {
const assignments = assignmentData[index] || [];
}, [setClassData, "/api/get_grade_data_current/", "/api/get_schedule_data/", "/api/get_assignments_current/", setAssignmentData]);

if (assignments) {
setAssignmentsTableContent(
[
<table className="assignments-table" key={index}>
<tbody key={index}>
{assignments.map((assignment, index) => (
<tr key={index}>
<td>{assignment.name}</td>
<td>{assignment.gradeCategory}</td>
<td>{assignment.earned} / {assignment.points}</td>
</tr>
))}
</tbody>
</table>
]
);
} else {
<p className="placeholder-text">no assignments in this class</p>
}
const gpaInput = classData.map((data) => {
return {
grade: data.grade,
credits: getCredits(data.name),
ap: data.name.startsWith("AP"),
};
});

const hUnweightedGpa = calculateGpa(gpaInput, "hUnweighted");
const fUnweightedGpa = calculateGpa(gpaInput, "fUnweighted");
const fWeightedGpa = calculateGpa(gpaInput, "fWeighted");

function handleRowClick(classIndex: number) {
const assignments = assignmentData[classIndex] || [];

if (assignments && assignments.length != 0) {
setAssignmentsTableContent(
[
<tbody key={classIndex}>
{assignments.map((assignment, index) => (
<tr key={index}>
<td>{assignment.name}</td>
<td>{assignment.gradeCategory}</td>
<td className="fraction-grade">{`${assignment.earned || 0} / ${assignment.points || 0}`}</td>
</tr>
))}
</tbody>
]
);
} else {
setAssignmentsTableContent(
[
<p className="placeholder-text" key={classIndex}>no assignments for this class</p>
]
);
}
}

return (
loading ? <LoadingScreen loadText="Parsing Grades..." /> :
loading ? <LoadingScreen loadText="Loading..." /> :
<main>
<NavBar />
<div className="page-main">
Expand All @@ -95,15 +109,15 @@ export default function Home() {
</div>
<table className="grades-table">
<tbody>
<tr>
<th>TEACHERS</th>
<th>CLASS</th>
<th>GRADE</th>
<th>RM.</th>
<tr key={0}>
<th key={0}>TEACHERS</th>
<th key={1}>CLASS</th>
<th key={2}>GRADE</th>
<th key={3}>RM.</th>
</tr>
{classData.map((data, index) => (
<>
<tr key={index} className="class-row" id={`c${index}`} onClick={() => handleRowClick(index)}
<React.Fragment key={index+1}>
<tr className="class-row" id={`c${index}`} onClick={() => handleRowClick(index)}
style={!loadingAssignment ? {
cursor: "pointer"
} : {}}
Expand All @@ -115,7 +129,7 @@ export default function Home() {
</td>
<td>{data.room}</td>
</tr>
</>
</React.Fragment>
))}
</tbody>
</table>
Expand Down
1 change: 1 addition & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function RootLayout({
<html lang="en">
<body suppressHydrationWarning={true} >
{children}
<a href="/about" className="about-link">About CRLSpen</a>
</body>
</html>
);
Expand Down
Loading