From c1c90a025dbea8ca6e2e57ebfd72dea796e24d3c Mon Sep 17 00:00:00 2001
From: Rei <38581479+Rei-x@users.noreply.github.com>
Date: Mon, 9 Sep 2024 18:13:51 +0200
Subject: [PATCH] feat: add api for courses (#44)
* feat:add plan sharing
* feat:add plan copying
* fix: types
* fix: fix scrollbars
* fix: fix styling on share plan page
* feat: add responsive share link dialog
* fix: fix input props component error
* fix: fix drawer styling
* fix: maybe fix for olo
* fix: umami can be undefined
* feat: add link to helpform
* feat: add api for courses
* feat: inform about project deploy
* fix: add padding to mobile ending quote
* feat: add new darker background
* feat: change design for plan view
* fix: disable hover on mai nbutton
---------
Co-authored-by: unewMe <56974648+unewMe@users.noreply.github.com>
---
package-lock.json | 41 ++++++++++++++++--
package.json | 1 +
public/copy.svg | 1 +
src/app/api/data/[facultyId]/route.ts | 36 ++++++++++++++++
src/app/api/profile/route.ts | 10 -----
src/services/usos/getRegistrations.ts | 44 +++++++++++++++++++
src/services/usos/index.ts | 61 +++++++++++++++++++++++++--
src/services/usos/usosClient.ts | 19 ++++++++-
8 files changed, 195 insertions(+), 18 deletions(-)
create mode 100644 public/copy.svg
create mode 100644 src/app/api/data/[facultyId]/route.ts
delete mode 100644 src/app/api/profile/route.ts
create mode 100644 src/services/usos/getRegistrations.ts
diff --git a/package-lock.json b/package-lock.json
index d70f0be..95dbfc5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0",
@@ -22,6 +23,7 @@
"fetch-cookie": "^3.0.1",
"framer-motion": "^11.3.28",
"jotai": "^2.9.3",
+ "lru-cache": "^11.0.1",
"lucide-react": "^0.426.0",
"next": "^14.2.3",
"next-sitemap": "^4.2.3",
@@ -33,6 +35,7 @@
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.2",
+ "vaul": "^0.9.2",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -3101,6 +3104,30 @@
}
}
},
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz",
+ "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-presence": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz",
@@ -8385,12 +8412,12 @@
}
},
"node_modules/lru-cache": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz",
- "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==",
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz",
+ "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==",
"license": "ISC",
"engines": {
- "node": "14 || >=16.14"
+ "node": "20 || >=22"
}
},
"node_modules/lucide-react": {
@@ -9051,6 +9078,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
diff --git a/package.json b/package.json
index d5833e2..ace0124 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"fetch-cookie": "^3.0.1",
"framer-motion": "^11.3.28",
"jotai": "^2.9.3",
+ "lru-cache": "^11.0.1",
"lucide-react": "^0.426.0",
"next": "^14.2.3",
"next-sitemap": "^4.2.3",
diff --git a/public/copy.svg b/public/copy.svg
new file mode 100644
index 0000000..f3b629c
--- /dev/null
+++ b/public/copy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/app/api/data/[facultyId]/route.ts b/src/app/api/data/[facultyId]/route.ts
new file mode 100644
index 0000000..8e66af3
--- /dev/null
+++ b/src/app/api/data/[facultyId]/route.ts
@@ -0,0 +1,36 @@
+import { NextResponse } from "next/server";
+
+import type { ApiResponse } from "@/lib/types";
+import { createUsosService } from "@/lib/usos";
+
+export const revalidate = 3600;
+
+export async function GET(
+ _request: Request,
+ { params }: { params: { facultyId: string } },
+) {
+ const service = createUsosService();
+
+ return NextResponse.json(
+ {
+ registrations: await Promise.all(
+ (await service.getRegistrations(params.facultyId)).map(async (r) => ({
+ registration: r,
+ courses: await Promise.all(
+ r.related_courses.flatMap(async (c) => ({
+ course: await service.getCourse(c.course_id),
+ groups: await service.getGroups(c.course_id, c.term_id),
+ })),
+ ),
+ })),
+ ),
+ },
+ {
+ headers: {
+ "Cache-Control": "public, max-age=3600",
+ },
+ },
+ );
+}
+
+export type ApiProfileGet = ApiResponse;
diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts
deleted file mode 100644
index f804ece..0000000
--- a/src/app/api/profile/route.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { NextResponse } from "next/server";
-
-import type { ApiResponse } from "@/lib/types";
-import { createUsosService } from "@/lib/usos";
-
-export async function GET() {
- return NextResponse.json(await createUsosService().getProfile());
-}
-
-export type ApiProfileGet = ApiResponse;
diff --git a/src/services/usos/getRegistrations.ts b/src/services/usos/getRegistrations.ts
new file mode 100644
index 0000000..9d9243e
--- /dev/null
+++ b/src/services/usos/getRegistrations.ts
@@ -0,0 +1,44 @@
+export type GetRegistrations = Registration[];
+export interface Registration {
+ id: string;
+ description: Description;
+ message: Description;
+ type: Type;
+ status: RegistrationStatus;
+ is_linkage_required: boolean;
+ www_instance: WWWInstance;
+ related_courses: RelatedCourse[];
+}
+
+export interface Description {
+ pl: string;
+ en: string;
+}
+
+export interface RelatedCourse {
+ course_id: string;
+ term_id: TermID;
+ status: RelatedCourseStatus;
+ limits: null;
+}
+
+export enum RelatedCourseStatus {
+ RegisterAndUnregister = "register_and_unregister",
+}
+
+export enum TermID {
+ The202425Z = "2024/25-Z",
+}
+
+export enum RegistrationStatus {
+ Active = "active",
+ Prepared = "prepared",
+}
+
+export enum Type {
+ NotToken = "not_token",
+}
+
+export enum WWWInstance {
+ PwrWWW = "PWR_WWW",
+}
diff --git a/src/services/usos/index.ts b/src/services/usos/index.ts
index d8a2d79..64d15d3 100644
--- a/src/services/usos/index.ts
+++ b/src/services/usos/index.ts
@@ -1,10 +1,12 @@
import * as cheerio from "cheerio";
import makeFetchCookie from "fetch-cookie";
+import { LRUCache } from "lru-cache";
import type { GetCoursesCart } from "./getCoursesCart";
import type { GetCoursesEditions } from "./getCoursesEditions";
import type { GetProfile } from "./getProfile";
import type { GetRegistrationRoundCourses } from "./getRegistrationRoundCourses";
+import type { GetRegistrations } from "./getRegistrations";
import { type GetTerms } from "./getTerms";
import type { GetUserRegistrations } from "./getUserRegistrations";
import { Day, Frequency, LessonType } from "./types";
@@ -40,6 +42,11 @@ const calculateDifference = (start: Time, end: Time) => {
};
};
+const cache = new LRUCache({
+ ttl: 60 * 1000,
+ ttlAutopurge: true,
+});
+
export const usosService = (usosClient: UsosClient) => {
return {
getProfile: async () => {
@@ -57,7 +64,7 @@ export const usosService = (usosClient: UsosClient) => {
getUserRegistrations: async () => {
const data = await usosClient.get(
- "registrations/user_registrations?fields=id|description|message|type|status|is_linkage_required|www_instance|faculty|rounds|related_courses",
+ "registrations/user_registrations?fields=id|description|message|type|status|is_linkage_required|www_instance|faculty|rounds|related_courses&active=false",
);
return data;
@@ -88,6 +95,16 @@ export const usosService = (usosClient: UsosClient) => {
return data;
},
+ getRegistrations: async (facultyId: string) => {
+ return usosClient.get(
+ `registrations/faculty_registrations?faculty_id=${facultyId}&fields=id|description|message|type|status|is_linkage_required|www_instance|related_courses`,
+ );
+ },
+ getCourses: async (coursesIds: string[]) => {
+ return usosClient.get(
+ `courses/courses&course_ids=${coursesIds.join("|")}`,
+ );
+ },
getClassGroupTimetable: async (
courseUnitId: string,
groupNumber: string,
@@ -111,6 +128,15 @@ export const usosService = (usosClient: UsosClient) => {
return data;
},
getCourse: async (courseId: string) => {
+ const cacheKey = `course-${courseId}`;
+
+ if (cache.has(cacheKey)) {
+ return cache.get(cacheKey) as Promise<{
+ id: string;
+ name: string;
+ }>;
+ }
+
const data = await fetchWithCookie(
`https://web.usos.pwr.edu.pl/kontroler.php?_action=katalog2/przedmioty/pokazPrzedmiot&prz_kod=${courseId}`,
);
@@ -119,13 +145,40 @@ export const usosService = (usosClient: UsosClient) => {
const name = $("h1").text();
- return {
+ cache.set(cacheKey, {
id: courseId,
name,
- };
+ });
+
+ return cache.get(cacheKey) as Promise<{
+ id: string;
+ name: string;
+ }>;
},
getGroups: async (courseId: string, term?: string) => {
+ const cacheKey = `groups-${courseId}-${term}`;
+
+ if (cache.has(cacheKey)) {
+ return cache.get(cacheKey) as Promise<
+ Array<{
+ hourStartTime: Time;
+ hourEndTime: Time;
+ duration: Time;
+ person: string;
+ personLink: string;
+ groupLink: string;
+ day: Day;
+ courseId: string;
+ type: LessonType;
+ nameExtended: string;
+ groupNumber: number;
+ frequency: Frequency;
+ name: string;
+ }>
+ >;
+ }
+
const data = await fetchWithCookie(
`https://web.usos.pwr.edu.pl/kontroler.php?_action=katalog2/przedmioty/pokazPlanZajecPrzedmiotu&prz_kod=${courseId}&plan_division=semester&plan_format=new-ui${
typeof term === "string" ? `&cdyd_kod=${term}` : ""
@@ -239,6 +292,8 @@ export const usosService = (usosClient: UsosClient) => {
};
});
+ cache.set(cacheKey, groups);
+
return groups;
},
};
diff --git a/src/services/usos/usosClient.ts b/src/services/usos/usosClient.ts
index 992e316..9f02862 100644
--- a/src/services/usos/usosClient.ts
+++ b/src/services/usos/usosClient.ts
@@ -1,8 +1,16 @@
+import { LRUCache } from "lru-cache";
+
import { USOS_APPS_URL } from "@/env.mjs";
import { oauth } from "@/lib/auth";
const baseUrl = `${USOS_APPS_URL}/services`;
+const cache = new LRUCache({
+ ttl: 60 * 60 * 1000,
+ ttlAutopurge: false,
+ max: 10000,
+});
+
export const createClient = ({
token,
secret,
@@ -19,6 +27,10 @@ export const createClient = ({
async get(endpoint: string): Promise {
const url = `${baseUrl}/${endpoint}`;
+ if (cache.has(url)) {
+ return cache.get(url) as R;
+ }
+
const data = oauth.authorize(
{
url,
@@ -46,7 +58,12 @@ export const createClient = ({
throw new Error("Unauthorized");
}
- return response.json() as Promise;
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const json: object = await response.json();
+
+ cache.set(url, json);
+
+ return json as Promise;
},
async post(endpoint: string, body: unknown): Promise {
const url = `${baseUrl}/${endpoint}`;