├── .gitignore ├── .npmrc ├── README.md ├── app.config.ts ├── components └── AssignmentCard.vue ├── layouts └── default.vue ├── nuxt.config.ts ├── package.json ├── pages └── index.vue ├── pnpm-lock.yaml ├── repo └── screenshot.png ├── server ├── api │ └── assignments.get.ts └── parsers │ ├── cadmo.js │ ├── eprog.js │ └── linalg.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Assignment Viewer 2 | 3 | ### Problem 4 | 5 | During my first year at ETH Zürich, I encountered a challenge where I had to individually access and visit every lecture website to obtain the latest assignment or homework files. This required clicking an average of four links, which was three more than I desired. 6 | 7 | ### Solution 8 | 9 | I have developed a website that consolidates various assignments, providing a centralized platform where you can access them, along with solutions and bonus questions. 10 | 11 | ### TODO's 12 | 13 | - [ ] Don't hardcode the lectures, allow for dynamic selection / addition of lectures. 14 | - [ ] Get rid of different parsing logic for each lecture website, maybe get GPT-3.5 to do the parsing for me. 15 | 16 | ### Screenshot 17 | 18 | ![Screenshot](./repo/screenshot.png) 19 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Ziad Malik 2 | 3 | export default defineAppConfig({ 4 | ui: { 5 | primary: "sky", 6 | gray: "neutral", 7 | } 8 | }); -------------------------------------------------------------------------------- /components/AssignmentCard.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47 | 48 | 77 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Ziad Malik 2 | export default defineNuxtConfig({ 3 | devtools: { enabled: true }, 4 | modules: ["@nuxt/ui"], 5 | // supabase: { 6 | // redirectOptions: { 7 | // login: "/", 8 | // callback: "/", 9 | // exclude: ["/"] 10 | // }, 11 | // }, 12 | }); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assignments-me", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "devDependencies": { 13 | "@nuxt/devtools": "latest", 14 | "@nuxtjs/supabase": "^1.1.2", 15 | "nuxt": "^3.7.4" 16 | }, 17 | "dependencies": { 18 | "@nuxt/ui": "^2.8.1", 19 | "cheerio": "1.0.0-rc.12" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33 | 34 | 48 | -------------------------------------------------------------------------------- /repo/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misc-box/ethz-assignment-viewer/2bf5ef26057ad9f9fbfb686c3b4263d41f62f913/repo/screenshot.png -------------------------------------------------------------------------------- /server/api/assignments.get.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Ziad Malik 2 | 3 | import linalg from "../parsers/linalg"; 4 | import cadmo from "../parsers/cadmo"; 5 | import eprog from "../parsers/eprog"; 6 | 7 | export default defineEventHandler(async event => { 8 | return { 9 | "Lineare Algebra": await linalg(), 10 | "Algorithmen und Datenstrukturen": await cadmo(), 11 | "Einführung in die Programmierung": await eprog(), 12 | }; 13 | }); -------------------------------------------------------------------------------- /server/parsers/cadmo.js: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Ziad Malik 2 | 3 | import * as cheerio from "cheerio"; 4 | import url from "url"; 5 | 6 | export default async function parse() { 7 | const lectureStart = new Date(new Date().getFullYear(), 8, 25); 8 | 9 | const baseUrl = "https://moodle-app2.let.ethz.ch/course/view.php?id=23362"; 10 | const res = await fetch(baseUrl) 11 | 12 | if (!res.ok) { 13 | console.log('Response not OK'); 14 | return; 15 | } 16 | 17 | const html = await res.text(); 18 | 19 | const $ = cheerio.load(html); 20 | const exercises = []; 21 | 22 | // Iterate through each table row in the tbody 23 | const fileBaseUrl = "https://cadmo.ethz.ch/education/lectures/HS23/DA/" 24 | $('table#uebungen > tbody > tr').each((index, element) => { 25 | const exerciseName = $(element).find('td:first-child a').text().trim(); 26 | const exercisePDF = $(element).find('td:first-child a').attr('href'); 27 | const solutionPDF = $(element).find('td:last-child a').attr('href'); 28 | const dueDate = lectureStart.setDate(lectureStart.getDate() + (index ? 7 : 0)); 29 | 30 | // Create an exercise object and push it to the array 31 | exercises.push({ 32 | exerciseName, 33 | exercisePDF: url.resolve(fileBaseUrl, exercisePDF), 34 | solutionPDF: solutionPDF ? url.resolve(fileBaseUrl, solutionPDF) : null, 35 | bonusLink: null, 36 | dueDate, 37 | }); 38 | }); 39 | 40 | return { 41 | exercises, 42 | website: baseUrl, 43 | video: "https://video.ethz.ch/lectures/d-infk/2024/autumn/252-0026-00L.html", 44 | }; 45 | }; -------------------------------------------------------------------------------- /server/parsers/eprog.js: -------------------------------------------------------------------------------- 1 | import * as cheerio from "cheerio"; 2 | 3 | export default async function parse() { 4 | // Set the start date of the assignments to September 17, 2024, as the first release date 5 | const lectureStart = new Date(2024, 8, 17); // September is month 8 because months are 0-based in JavaScript Date 6 | 7 | // Base URL for the assignments root 8 | const websiteBaseUrl = "https://lec.inf.ethz.ch/infk/eprog/2024/"; 9 | const assignmentBaseUrl = `${websiteBaseUrl}exercises/sheets/`; 10 | 11 | const currentDate = new Date(); // Get the current date 12 | const exercises = []; 13 | 14 | // Calculate how many assignments have been released so far 15 | const timeDiff = currentDate - lectureStart; // Time difference between the current date and the start date 16 | const weeksPassed = Math.floor(timeDiff / (7 * 24 * 60 * 60 * 1000)); // Number of full weeks that have passed 17 | 18 | // Iterate through all released assignments 19 | for (let i = 0; i <= weeksPassed; i++) { 20 | const assignmentName = `uebungsblatt${i}`; 21 | const exercisePDF = `${assignmentBaseUrl}${assignmentName}.pdf`; 22 | 23 | // Calculate the release date of each assignment 24 | const assignmentDate = new Date(lectureStart); 25 | assignmentDate.setDate(lectureStart.getDate() + i * 7); // Released every Tuesday 26 | 27 | // Set the due date to 7 days after the release date 28 | const dueDate = new Date(assignmentDate); 29 | dueDate.setDate(assignmentDate.getDate() + 7); 30 | 31 | // Create the assignment object and add it to the array 32 | exercises.push({ 33 | exerciseName: assignmentName, 34 | exercisePDF, 35 | solutionPDF: null, 36 | bonusLink: null, 37 | dueDate, 38 | openLink: exercisePDF, 39 | }); 40 | } 41 | 42 | return { 43 | exercises, // List of all the exercises generated 44 | website: websiteBaseUrl, // Root URL that redirects to the website 45 | video: "https://video.ethz.ch/lectures/d-infk/2024/autumn/252-0027-00L.html", // Assuming this is the 2024 video link 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /server/parsers/linalg.js: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Ziad Malik 2 | 3 | import * as cheerio from "cheerio"; 4 | 5 | function getLastDateFromString(inputString) { 6 | const monthAbbreviations = { 7 | Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, 8 | Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 9 | }; 10 | 11 | // Split the input string by spaces to get individual date parts 12 | const dateParts = inputString.trim().split(' '); 13 | 14 | // Parse the month and day from the last date part 15 | const [month, day] = dateParts.slice(-2); 16 | 17 | if (!Object.keys(monthAbbreviations).includes(month)) { 18 | return undefined; 19 | } 20 | 21 | // Get the current year 22 | const currentYear = new Date().getFullYear(); 23 | 24 | // Create a new Date object with the current year, parsed month, and day 25 | const lastDate = new Date(currentYear, monthAbbreviations[month], day); 26 | 27 | if (!lastDate.getTime()) { 28 | return undefined; 29 | } 30 | 31 | console.log('linalg last date: ') 32 | console.log(lastDate) 33 | 34 | // LinAlg gives you 6 days time for some reason? 35 | return lastDate.setDate(lastDate.getDate() + 6); 36 | } 37 | 38 | export default async function parse() { 39 | const baseUrl = "https://ti.inf.ethz.ch/ew/courses/LA24/index.html"; 40 | const res = await fetch(baseUrl) 41 | 42 | if (!res.ok) { 43 | console.log('Response not OK'); 44 | return; 45 | } 46 | 47 | const html = await res.text(); 48 | 49 | const $ = cheerio.load(html); 50 | // const exercises = []; 51 | // Initialize an array to store the data for each row 52 | const rows = []; 53 | 54 | // Iterate through each table row in the tbody 55 | $('table tbody tr').each((index, element) => { 56 | if (element.children.length < 2) { 57 | return; 58 | } 59 | 60 | const columns = $(element).find('td'); 61 | 62 | const exerciseName = $(columns[5]).find('a').text(); 63 | const exercisePDF = $(columns[5]).find('a').attr('href'); 64 | const solutionPDF = $(columns[5]).find('a').attr('href'); 65 | const bonusLink = $(columns[6]).find('a').attr('href'); 66 | const dueDate = getLastDateFromString($(columns[1]).text()); 67 | console.log(dueDate) 68 | 69 | // Create an object for the row and push it to the array 70 | rows.push({ 71 | exerciseName, 72 | exercisePDF, 73 | solutionPDF: null, 74 | bonusLink, 75 | dueDate, 76 | }); 77 | }); 78 | 79 | const exercises = rows.filter(e => e.exerciseName && e.exercisePDF); 80 | 81 | return { 82 | exercises, 83 | website: baseUrl, 84 | video: "https://video.ethz.ch/lectures/d-math/2024/autumn/401-0131-00L.html", 85 | }; 86 | }; 87 | 88 | // console.log(await parse()) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | --------------------------------------------------------------------------------