├── .all-contributorsrc ├── .dockerignore ├── .env.example ├── .github ├── CODE_OF_CONDUCT.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── url-request.md ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── Boundary.tsx ├── academia │ ├── calendar │ │ ├── components │ │ │ ├── DayCell.tsx │ │ │ └── Grid.tsx │ │ └── page.tsx │ ├── components │ │ ├── Attendance │ │ │ ├── Card │ │ │ │ ├── AttendancePill.tsx │ │ │ │ ├── Margin.tsx │ │ │ │ ├── Title.tsx │ │ │ │ └── index.tsx │ │ │ ├── List.tsx │ │ │ ├── Prediction │ │ │ │ ├── Predictor.tsx │ │ │ │ ├── RangeCalendar.tsx │ │ │ │ ├── computePrediction.ts │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── Marks │ │ │ ├── Card │ │ │ │ ├── MarkElement.tsx │ │ │ │ ├── MarkList.tsx │ │ │ │ ├── Total.tsx │ │ │ │ └── index.tsx │ │ │ ├── List.tsx │ │ │ └── index.tsx │ │ ├── NavBar.tsx │ │ └── Timetable │ │ │ ├── EditTimetable.tsx │ │ │ ├── Editor.tsx │ │ │ ├── OptionalEditor.tsx │ │ │ ├── components │ │ │ ├── TableCell.tsx │ │ │ └── TimetableStack.tsx │ │ │ └── index.tsx │ ├── courses │ │ ├── components │ │ │ └── Card │ │ │ │ ├── Class.tsx │ │ │ │ ├── CourseCode.tsx │ │ │ │ ├── CourseTitle.tsx │ │ │ │ ├── Credit.tsx │ │ │ │ └── index.tsx │ │ └── page.tsx │ ├── faculties │ │ ├── Faculties.tsx │ │ ├── components │ │ │ └── SearchBar.tsx │ │ └── page.tsx │ ├── gradex │ │ ├── components │ │ │ ├── GradeCalculator.tsx │ │ │ ├── GradeCard.tsx │ │ │ └── Medals.tsx │ │ └── page.tsx │ ├── layout.tsx │ ├── library │ │ ├── LibraryHome.tsx │ │ ├── components │ │ │ ├── Card.tsx │ │ │ ├── Code.tsx │ │ │ ├── PaperLink.tsx │ │ │ └── SearchBar.tsx │ │ ├── page.tsx │ │ ├── pyq │ │ │ ├── PYQ.tsx │ │ │ └── page.tsx │ │ ├── render │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── resources │ │ │ ├── Resources.tsx │ │ │ └── page.tsx │ ├── links │ │ ├── components │ │ │ ├── LinkList.tsx │ │ │ ├── SearchBar.tsx │ │ │ └── URLSection.tsx │ │ └── page.tsx │ └── page.tsx ├── api │ ├── calendar │ │ └── route.tsx │ ├── logout │ │ └── route.ts │ ├── ophours │ │ └── route.ts │ └── timetable │ │ └── route.tsx ├── auth │ ├── login │ │ ├── components │ │ │ ├── Form.tsx │ │ │ └── form │ │ │ │ ├── PasswordInput.tsx │ │ │ │ └── UidInput.tsx │ │ └── page.tsx │ └── logout │ │ └── page.tsx ├── builder │ └── page.tsx ├── error.tsx ├── global-error.tsx ├── globals.css ├── home │ └── page.tsx ├── invalid │ └── page.tsx ├── layout.tsx ├── loading.tsx ├── manifest.ts ├── not-found.tsx ├── offline │ └── page.tsx ├── page.tsx ├── payment │ └── page.tsx ├── privacy │ └── page.tsx ├── ratelimit │ └── page.tsx ├── sleeping │ └── page.tsx ├── suspended │ └── page.tsx ├── sw.ts └── view │ └── page.tsx ├── biome.json ├── bun.lock ├── components ├── Button.tsx ├── Error.tsx ├── Indicator.tsx ├── Sidebar │ ├── Badges │ │ └── DayOrder.tsx │ ├── Buttons │ │ ├── InstallButton.tsx │ │ ├── MiniButtons.tsx │ │ └── OpenButton.tsx │ ├── Popup.tsx │ ├── ProfileBadge.tsx │ ├── SidebarLink.tsx │ ├── UserDialog.tsx │ └── index.tsx ├── States │ └── Loading.tsx ├── themes │ ├── ColorPicker.tsx │ └── ThemeToggle.tsx └── ui │ ├── button.tsx │ ├── calendar.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ └── slider.tsx ├── compose.yaml ├── eslint.config.mjs ├── hooks ├── fetchCalendar.tsx ├── fetchFiles.tsx ├── fetchResources.tsx ├── fetchUserData.tsx ├── useGesture.tsx └── useSearch.tsx ├── lib └── utils.ts ├── library ├── files.ts └── resources.ts ├── middleware.ts ├── misc ├── encode.ts ├── faculties.ts ├── links.ts ├── theme.ts └── users.ts ├── netlify.toml ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── provider └── ThemeProvider.tsx ├── public ├── fonts │ └── Geist.ttf ├── icons │ ├── apple-icon.png │ ├── favicon.ico │ ├── icon.png │ ├── icon.svg │ ├── maskable_icon_x192.png │ └── maskable_icon_x512.png ├── images │ ├── GradeX.tsx │ ├── batman.svg │ ├── database.png │ └── og.png ├── library │ ├── resources.png │ └── sem.png └── screenshots │ ├── phone │ ├── academia.webp │ ├── calendar.webp │ ├── faculties.webp │ └── predict.webp │ └── wide │ ├── academia.webp │ ├── calendar.jpeg │ ├── calendar.webp │ ├── faculties.webp │ ├── links.webp │ └── predict.webp ├── robots.txt ├── stubs └── use-effect-event.js ├── tsconfig.json ├── types ├── Attendance.ts ├── Calendar.ts ├── CalendarData.ts ├── Course.ts ├── CoursePapers.ts ├── DayOrder.ts ├── Error.ts ├── Folders.ts ├── Grade.ts ├── Marks.ts ├── Response.ts ├── Timetable.ts ├── User.ts └── UserData.ts ├── utils ├── Cookies.ts ├── Database │ ├── index.ts │ └── supabase.ts ├── Date.ts ├── Grade.ts ├── Interval.ts ├── Margin.ts ├── ProfileColor.ts ├── Range.ts ├── Times.ts ├── Tokenize.ts ├── URL.ts ├── color.ts └── encrypt.ts └── vercel.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "ClassPro", 3 | "projectOwner": "Rahuletto", 4 | "commitConvention": "eslint", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "commitType": "docs", 9 | "contributorsPerLine": 7, 10 | "contributors": [ 11 | { 12 | "login": "Rahuletto", 13 | "name": "Rahul Marban", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/71836991?v=4", 15 | "profile": "http://marban.is-a.dev", 16 | "contributions": [ 17 | "code", 18 | "design", 19 | "bug", 20 | "a11y", 21 | "infra", 22 | "maintenance", 23 | "projectManagement", 24 | "review", 25 | "security", 26 | "tool" 27 | ] 28 | }, 29 | { 30 | "login": "root-daemon", 31 | "name": "Srivishal Sivasubramanian", 32 | "avatar_url": "https://avatars.githubusercontent.com/u/47695678?v=4", 33 | "profile": "https://github.com/root-daemon", 34 | "contributions": [ 35 | "code", 36 | "bug", 37 | "maintenance", 38 | "review" 39 | ] 40 | }, 41 | { 42 | "login": "DebadityaMalakar", 43 | "name": "Debaditya Malakar", 44 | "avatar_url": "https://avatars.githubusercontent.com/u/123065261?v=4", 45 | "profile": "https://portfolio-debaditya.vercel.app/", 46 | "contributions": [ 47 | "design" 48 | ] 49 | }, 50 | { 51 | "login": "Aakarsh-Kumar", 52 | "name": "Aakarsh Kumar", 53 | "avatar_url": "https://avatars.githubusercontent.com/u/72206467?v=4", 54 | "profile": "https://github.com/Aakarsh-Kumar", 55 | "contributions": [ 56 | "code", 57 | "bug" 58 | ] 59 | }, 60 | { 61 | "login": "harsshhan", 62 | "name": "HARSHAN A M", 63 | "avatar_url": "https://avatars.githubusercontent.com/u/146644928?v=4", 64 | "profile": "https://github.com/harsshhan", 65 | "contributions": [ 66 | "data" 67 | ] 68 | } 69 | ], 70 | "repoType": "github" 71 | } 72 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/go/build-context-dockerignore/ 6 | 7 | **/.classpath 8 | **/.dockerignore 9 | **/.env 10 | **/.git 11 | **/.gitignore 12 | **/.project 13 | **/.settings 14 | **/.toolstarget 15 | **/.vs 16 | **/.vscode 17 | **/.next 18 | **/.cache 19 | **/*.*proj.user 20 | **/*.dbmdl 21 | **/*.jfm 22 | **/charts 23 | **/docker-compose* 24 | **/compose.y*ml 25 | **/Dockerfile* 26 | **/node_modules 27 | **/npm-debug.log 28 | **/obj 29 | **/secrets.dev.yaml 30 | **/values.dev.yaml 31 | **/build 32 | **/dist 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_URL="" 2 | NEXT_PUBLIC_VALIDATION_KEY="" 3 | NEXT_PUBLIC_SERVICE_KEY="" 4 | NEXT_PUBLIC_SUPABASE_URL="" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[ISSUE] Bug report" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEAT]: Requesting new feature" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/url-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: URL Request 3 | about: Requesting URL to be added in "Useful Links" 4 | title: "[URL]: Request new URL" 5 | labels: URL 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] Is this relatable to SRM students 11 | - [ ] is this safe and valid 12 | 13 | URL: `` 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | pnpm-lock.yaml 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # Million Lint 44 | .million 45 | 46 | # Service Workers 47 | public/sw* 48 | public/swe-worker* 49 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 2, 6 | "semi": true, 7 | "jsxSingleQuote": true, 8 | "printWidth": 80, 9 | "arrowParens": "always", 10 | 11 | "plugins": ["prettier-plugin-tailwindcss"] 12 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | ## Reporting Issues 4 | 5 | 1. Use the GitHub issue tracker 6 | 2. Include a clear description of the issue 7 | 3. Add steps to reproduce the bug 8 | 4. Include system information and environment details 9 | 5. Add screenshots if applicable 10 | 11 | ## Feature Requests 12 | 13 | 1. Check existing issues to avoid duplicates 14 | 2. Describe the feature clearly 15 | 3. Explain why this feature would be useful 16 | 4. Add examples of how it would work 17 | 18 | ## Adding URLs 19 | 20 | When adding new URLs to the collection: 21 | 22 | 1. Follow the existing structure in `misc/links.ts` 23 | 2. Ensure the URL is active and accessible 24 | 3. Type should be "unofficial" to indicate its not from SRM or use "official" 25 | 4. Include a brief description 26 | ```ts 27 | { 28 | site: "Better-Lab", 29 | url: "https://better-lab.vercel.app", 30 | type: "unofficial", 31 | description: 32 | "A better alternative to SRM-Elab. Efficient, Fast, Zippy as hecc", 33 | } 34 | ``` 35 | 36 | ## Theming 37 | 38 | When creating or modifying themes: 39 | 40 | 1. Theme Structure 41 | ```ts 42 | { 43 | title: "Theme Name", 44 | mode: "light" | "dark", // specify theme mode 45 | mono?: boolean, // optional: for monospace themes 46 | properties: { 47 | // all colors in RGB format 48 | metacolor: "#hexcode", // only metacolor allows hex 49 | "background-normal": "R G B", 50 | "background-light": "R G B", 51 | "background-dark": "R G B", 52 | "background-darker": "R G B", 53 | input: "R G B/opacity", 54 | button: "R G B", 55 | side: "R G B", 56 | accent: "R G B", 57 | color: "R G B", 58 | "error-background": "R G B", 59 | "error-color": "R G B", 60 | "warn-background": "R G B", 61 | "warn-color": "R G B", 62 | "success-background": "R G B", 63 | "success-color": "R G B", 64 | "info-background": "R G B", 65 | "info-color": "R G B", 66 | } 67 | } 68 | ``` 69 | 70 | 2. Color Format Rules: 71 | - Use space-separated RGB values (e.g., "255 128 0") 72 | - For transparency, use the "/" notation (e.g., "255 255 255/0.03") 73 | - Only metacolor property uses hex code format 74 | 75 | 3. Required Color Categories: 76 | - Background variants (normal, light, dark, darker) 77 | - Interactive elements (input, button) 78 | - Status colors (error, warn, success, info) 79 | - Content colors (accent, color) 80 | - Please use the default colors as possible for theory, practical 81 | 82 | ## Pull Request Process 83 | 84 | 1. Create a new branch for your changes 85 | 2. Follow the coding style of the project 86 | 3. Follow the commit lint naming conventions 87 | 4. Update documentation as needed 88 | 5. Request review from maintainers 89 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Comments are provided throughout this file to help you get started. 4 | # If you need more help, visit the Dockerfile reference guide at 5 | # https://docs.docker.com/go/dockerfile-reference/ 6 | 7 | # Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 8 | 9 | ARG NODE_VERSION=22.11.0 10 | 11 | ################################################################################ 12 | FROM node:${NODE_VERSION}-alpine AS base 13 | 14 | # Install pnpm 15 | RUN corepack enable && corepack prepare pnpm@9.15.4 --activate 16 | 17 | WORKDIR /usr/src/app 18 | 19 | ################################################################################ 20 | FROM base as deps 21 | 22 | RUN --mount=type=bind,source=package.json,target=package.json \ 23 | --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ 24 | pnpm install --frozen-lockfile --prod 25 | 26 | ################################################################################ 27 | FROM base as build 28 | 29 | RUN --mount=type=bind,source=package.json,target=package.json \ 30 | --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ 31 | pnpm install --frozen-lockfile 32 | 33 | COPY . . 34 | 35 | RUN pnpm run build 36 | 37 | ################################################################################ 38 | FROM base as final 39 | 40 | ENV NODE_ENV production 41 | USER node 42 | 43 | COPY package.json . 44 | COPY --from=deps /usr/src/app/node_modules ./node_modules 45 | COPY --from=build /usr/src/app/.next ./.next 46 | 47 | EXPOSE 243 48 | 49 | CMD ["pnpm", "start", "--port", "243"] -------------------------------------------------------------------------------- /app/Boundary.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { Component, type ErrorInfo, type ReactNode } from "react"; 3 | import ErrorComponent from "@/components/Error"; 4 | 5 | interface ErrorBoundaryProps { 6 | children: ReactNode; 7 | fallback?: ReactNode; 8 | } 9 | 10 | interface ErrorBoundaryState { 11 | hasError: boolean; 12 | error?: Error; 13 | } 14 | 15 | class ErrorBoundary extends Component { 16 | constructor(props: ErrorBoundaryProps) { 17 | super(props); 18 | this.state = { hasError: false }; 19 | } 20 | 21 | static getDerivedStateFromError(error: Error): ErrorBoundaryState { 22 | return { hasError: true, error }; 23 | } 24 | 25 | componentDidCatch(error: Error, errorInfo: ErrorInfo): void { 26 | console.error("ErrorBoundary caught an error:", error, errorInfo); 27 | } 28 | 29 | render(): ReactNode { 30 | if (this.state.hasError) { 31 | if (this.props.fallback) { 32 | return this.props.fallback; 33 | } 34 | 35 | return ( 36 | 43 | ); 44 | } 45 | 46 | return this.props.children; 47 | } 48 | } 49 | 50 | export default ErrorBoundary; 51 | -------------------------------------------------------------------------------- /app/academia/calendar/components/DayCell.tsx: -------------------------------------------------------------------------------- 1 | import type { Day } from "@/types/Calendar"; 2 | import { forwardRef } from "react"; 3 | 4 | interface DayCellProps { 5 | day: Day; 6 | isToday: boolean; 7 | isTomorrow: boolean; 8 | } 9 | 10 | export default forwardRef(function DayCell( 11 | { day, isToday, isTomorrow }, 12 | ref, 13 | ) { 14 | const isErrorDay = day?.dayOrder === "-"; 15 | 16 | const cellClasses = ` 17 | flex min-h-48 flex-col items-start justify-between gap-3 border border-dark-background-light p-4 xl:max-h-none xl:min-h-64 xl:items-end 18 | ${ 19 | isToday && isErrorDay 20 | ? "border-light-error-color bg-light-error-background dark:border-dark-error-color dark:bg-dark-error-background" 21 | : isErrorDay 22 | ? "bg-light-error-background dark:bg-dark-error-background" 23 | : isToday 24 | ? "opacity-100 border-light-success-color bg-light-success-background text-light-success-color dark:border-dark-success-color dark:bg-dark-success-background dark:text-dark-success-color" 25 | : isTomorrow 26 | ? "bg-light-warn-background text-light-warn-color dark:bg-dark-warn-background opacity-50 dark:text-dark-warn-color" 27 | : "dark:opacity-60 bg-light-background-light dark:bg-dark-background-normal" 28 | } 29 | `; 30 | 31 | return ( 32 |
38 | 44 | 45 | 46 |
47 | ); 48 | }); 49 | 50 | interface DateDisplayProps { 51 | date: string; 52 | day: string; 53 | isToday: boolean; 54 | isErrorDay: boolean; 55 | } 56 | 57 | const DateDisplay: React.FC = ({ 58 | date, 59 | day, 60 | isToday, 61 | isErrorDay, 62 | }) => ( 63 |
70 |

{day}

71 |

74 | {date} 75 |

76 |
77 | ); 78 | 79 | interface HolidayDisplayProps { 80 | holiday: string | null; 81 | isErrorDay: boolean; 82 | } 83 | 84 | const HolidayDisplay: React.FC = ({ 85 | holiday, 86 | isErrorDay, 87 | }) => { 88 | if (!holiday) return null; 89 | return ( 90 |

93 | {holiday.replaceAll(",", ", ")} 94 |

95 | ); 96 | }; 97 | 98 | interface DayOrderDisplayProps { 99 | dayOrder: string; 100 | isToday: boolean; 101 | } 102 | 103 | const DayOrderDisplay: React.FC = ({ 104 | dayOrder, 105 | isToday, 106 | }) => { 107 | if (dayOrder === "-") return null; 108 | return ( 109 |

110 | Day Order{" "} 111 | 112 | {dayOrder} 113 | 114 |

115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /app/academia/calendar/page.tsx: -------------------------------------------------------------------------------- 1 | import { fetchCalendar } from "@/hooks/fetchCalendar"; 2 | import React, { Suspense } from "react"; 3 | 4 | import Loading from "@/components/States/Loading"; 5 | import dynamic from "next/dynamic"; 6 | import { fetchUserData } from "@/hooks/fetchUserData"; 7 | import { supabase } from "@/utils/Database/supabase"; 8 | 9 | const CalendarGrid = dynamic(() => import("./components/Grid")); 10 | 11 | export default async function Calendar() { 12 | const { calendar, index } = await fetchCalendar(); 13 | const { user } = await fetchUserData(); 14 | 15 | const subscribed = true; 16 | const subscribedSince = Date.now(); 17 | 18 | 19 | return ( 20 |
21 | }> 22 | 23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/academia/components/Attendance/Card/AttendancePill.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type AttendancePillProps = { 4 | present: number; 5 | absent: number; 6 | total: number; 7 | }; 8 | 9 | export default function AttendanceDetails({ 10 | present, 11 | absent, 12 | total, 13 | }: AttendancePillProps) { 14 | return ( 15 |
19 |
20 | 21 | {present} 22 | 23 | 24 | {absent} 25 | 26 |
27 | 28 | {total} 29 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/academia/components/Attendance/Card/Margin.tsx: -------------------------------------------------------------------------------- 1 | import { calculateMargin } from "@/utils/Margin"; 2 | import React from "react"; 3 | 4 | type AttendanceMarginProps = { 5 | present: number; 6 | total: number; 7 | }; 8 | 9 | export default function AttendanceMargin({ 10 | present, 11 | total, 12 | }: AttendanceMarginProps) { 13 | const margin = calculateMargin(present, total); 14 | 15 | return ( 16 |
22 | 26 | 27 | {margin >= 0 ? "Margin: " : "Required: "} 28 | 29 | 30 | 33 | {Math.abs(margin)} 34 | 35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/academia/components/Attendance/Card/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import dynamic from "next/dynamic"; 3 | 4 | const Indicator = dynamic( 5 | () => import("@/components/Indicator").then((a) => a.default), 6 | { ssr: true }, 7 | ); 8 | 9 | type CourseTitleProps = { 10 | courseTitle: string; 11 | category: string; 12 | }; 13 | 14 | export default function CourseTitle({ 15 | courseTitle, 16 | category, 17 | }: CourseTitleProps) { 18 | return ( 19 |
24 | 25 | 26 | {courseTitle} 27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/academia/components/Attendance/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import type { AttendanceCourse } from "@/types/Attendance"; 2 | import React from "react"; 3 | import CourseTitle from "./Title"; 4 | import AttendanceMargin from "./Margin"; 5 | import AttendanceDetails from "./AttendancePill"; 6 | 7 | export default function AttendanceCard({ 8 | attendance, 9 | continuous, 10 | }: { attendance: AttendanceCourse; continuous?: boolean }) { 11 | const { 12 | courseTitle, 13 | category, 14 | hoursConducted, 15 | hoursAbsent, 16 | attendancePercentage, 17 | } = attendance; 18 | 19 | const present = 20 | Number.parseInt(hoursConducted, 10) - Number.parseInt(hoursAbsent, 10); 21 | const total = Number.parseInt(hoursConducted, 10); 22 | const absent = Number.parseInt(hoursAbsent, 10); 23 | 24 | return ( 25 |
29 | 33 |
34 | 35 |
36 | 37 | 48 | {attendancePercentage.replace(".00", "").replace("%", "")}% 49 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/academia/components/Attendance/List.tsx: -------------------------------------------------------------------------------- 1 | import type { AttendanceCourse } from "@/types/Attendance"; 2 | import React, { Suspense } from "react"; 3 | import AttendanceCard from "./Card"; 4 | import Indicator from "@/components/Indicator"; 5 | import Loading from "@/components/States/Loading"; 6 | 7 | export default function List({ 8 | list, 9 | continuous, 10 | }: { list: AttendanceCourse[]; continuous?: boolean }) { 11 | return ( 12 | }> 13 |
a.category === "Practical")[0] ? "mb-4" : "" 16 | } 17 | > 18 | {list 19 | ?.filter((a) => a.category === "Theory") 20 | .map((course, index) => ( 21 | 26 | ))} 27 |
28 | 29 | {list?.filter((a) => a.category === "Practical")?.[0] && ( 30 | 31 | )} 32 | 33 | {list?.filter((a) => a.category === "Practical")[0] && ( 34 |
35 | {list 36 | ?.filter((a) => a.category === "Practical") 37 | .map((course, index) => ( 38 | 43 | ))} 44 |
45 | )} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /app/academia/components/Attendance/Prediction/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import type { AllResponse } from "@/types/Response"; 3 | import React, { Suspense, useState } from "react"; 4 | import { PiBrainBold } from "react-icons/pi"; 5 | import Predictor from "./Predictor"; 6 | import type { Calendar } from "@/types/Calendar"; 7 | import Loading from "@/components/States/Loading"; 8 | 9 | export default function Prediction({ 10 | data, 11 | cal, 12 | calendar, 13 | subscribed 14 | }: { 15 | data: AllResponse; 16 | cal: { 17 | date: string; 18 | month: number; 19 | order: string; 20 | dateObj: Date; 21 | }[]; 22 | calendar: Calendar[]; 23 | subscribed: boolean; 24 | }) { 25 | const [isOpen, setIsOpen] = useState(false); 26 | 27 | return ( 28 | <> 29 | 36 | }> 37 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /app/academia/components/Attendance/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import Prediction from "./Prediction"; 3 | import type { AllResponse } from "@/types/Response"; 4 | import { fetchCalendar } from "@/hooks/fetchCalendar"; 5 | import Loading from "@/components/States/Loading"; 6 | import { supabase } from "@/utils/Database/supabase"; 7 | 8 | export const months = [ 9 | "Jan", 10 | "Feb", 11 | "Mar", 12 | "Apr", 13 | "May", 14 | "Jun", 15 | "Jul", 16 | "Aug", 17 | "Sep", 18 | "Oct", 19 | "Nov", 20 | "Dec", 21 | ]; 22 | 23 | export default async function Attendance({ data }: { data: AllResponse }) { 24 | const cal = await fetchCalendar(); 25 | 26 | if (!cal || !cal?.calendar) return ( 27 | <> 28 |
29 |
30 |

Attendance

31 |
32 |
33 |
34 | 35 | ); 36 | 37 | const subscribed = true 38 | 39 | const mappedCal = cal.calendar?.flatMap((day) => { 40 | const month = months.findIndex( 41 | (month) => month.trim() === day.month.split("'")[0].trim(), 42 | ); 43 | 44 | return day.days.map((date) => ({ 45 | date: date.date, 46 | month: month, 47 | order: date?.dayOrder, 48 | dateObj: new Date( 49 | new Date().getFullYear(), 50 | month, 51 | Number.parseInt(date.date), 52 | ), 53 | })); 54 | }); 55 | 56 | return ( 57 | <> 58 |
59 |
60 |
61 |

Attendance

62 |
63 |
64 | }> 65 | 71 | 72 |
73 |
74 |
75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /app/academia/components/Marks/Card/MarkElement.tsx: -------------------------------------------------------------------------------- 1 | import type { Marks, TestPerformance } from "@/types/Marks"; 2 | import React from "react"; 3 | 4 | export default function MarkElement({ test }: { test: TestPerformance }) { 5 | return ( 6 |
7 | {test.test} 8 | 9 |
10 | ); 11 | } 12 | 13 | export function MarkDisplay({ marks }: { marks: Marks }) { 14 | const scored = marks.scored 15 | return ( 16 |
19 | 22 | {scored} 23 | 24 | 27 | {scored ? marks.total.split(".")[0] : marks.total} 28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/academia/components/Marks/Card/MarkList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { TestPerformance } from "@/types/Marks"; 3 | import dynamic from "next/dynamic"; 4 | 5 | const MarkElement = dynamic( 6 | () => import("./MarkElement").then((a) => a.default), 7 | { ssr: false }, 8 | ); 9 | 10 | export default function MarkList({ 11 | testPerformance, 12 | }: { 13 | testPerformance?: TestPerformance[]; 14 | }) { 15 | if (testPerformance?.[0]) { 16 | return ( 17 |
18 | {testPerformance.map((test, i) => ( 19 | 20 | ))} 21 |
22 | ); 23 | } 24 | 25 | return ( 26 |
30 |

34 | No tests conducted. 35 |

36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/academia/components/Marks/Card/Total.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Marks } from "@/types/Marks"; 3 | 4 | import { MarkDisplay } from "./MarkElement"; 5 | 6 | interface TotalProps { 7 | overall?: Marks; 8 | graph?: boolean; 9 | } 10 | 11 | export default function TotalSection({ overall, graph }: TotalProps) { 12 | if (!overall) { 13 | const percent = "0.00"; 14 | return ( 15 |
16 |
22 |
23 |

Total

24 | 25 | {graph && ( 26 |
29 |

{percent}%

30 |
31 | )} 32 |
33 | 39 |
40 |
41 | ); 42 | } 43 | 44 | const percent = ( 45 | (Number.parseFloat(overall.scored ?? "0") / Number.parseFloat(overall.total ?? "1")) * 46 | 100 47 | ).toFixed(1); 48 | 49 | return ( 50 |
51 |
57 |
58 |

Total

59 | 60 | {graph && ( 61 |
64 |

{percent}%

65 |
66 | )} 67 |
68 | 69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/academia/components/Marks/Card/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import type { Mark } from "@/types/Marks"; 4 | import TotalSection from "./Total"; 5 | import MarkList from "./MarkList"; 6 | 7 | import dynamic from "next/dynamic"; 8 | import type { Course } from "@/types/Course"; 9 | // import PerformanceChart from "./PerfGraph"; 10 | 11 | const Indicator = dynamic( 12 | () => import("@/components/Indicator").then((a) => a.default), 13 | { ssr: false }, 14 | ); 15 | 16 | export default function MarkCard({ 17 | mark, 18 | course, 19 | // graph = false, 20 | }: { 21 | mark: Mark | undefined; 22 | course: Course; 23 | // graph: boolean; 24 | }) { 25 | // if (!mark) return null; 26 | return ( 27 |
28 |
32 |
33 |
34 |

39 | {course.title} 40 |

41 | 42 |
43 |
44 | {/* {graph ? ( 45 | 46 | ) : null} */} 47 | 48 | 49 |
50 |
51 | 52 | 56 |
57 | {course?.credit === "0" && ( 58 |
59 |

60 | Zero Credit 61 |

62 |
63 | )} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /app/academia/components/Marks/List.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MarkCard from "./Card"; 3 | import Indicator from "@/components/Indicator"; 4 | import type { Mark } from "@/types/Marks"; 5 | import type { Course } from "@/types/Course"; 6 | 7 | export default function List({ 8 | list, 9 | courses, 10 | }: { 11 | list: Mark[]; 12 | courses: Course[]; 13 | }) { 14 | return ( 15 | <> 16 |
a.slotType === "Practical")[0] ? "mb-4" : "" 19 | } grid animate-fadeIn grid-cols-marks gap-2 transition-all duration-200`} 20 | > 21 | {courses 22 | ?.filter((a) => a.slotType === "Theory") 23 | .map((course, index) => ( 24 | a.courseCode === course.code)} 27 | course={course} 28 | /> 29 | ))} 30 |
31 | 32 | {courses?.filter((a) => a.slotType === "Practical")?.[0] && ( 33 | 34 | )} 35 | 36 | {courses?.filter((a) => a.slotType === "Practical")[0] && ( 37 |
38 | {courses 39 | ?.filter((a) => a.slotType === "Practical") 40 | .map((course, index) => ( 41 | a.courseCode === course.code)} 44 | course={course} 45 | /> 46 | ))} 47 |
48 | )} 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/academia/components/Marks/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Mark } from "@/types/Marks"; 3 | import type { Course } from "@/types/Course"; 4 | import List from "./List"; 5 | 6 | export default function Marks({ 7 | marks, 8 | courses, 9 | }: { marks: Mark[]; courses: Course[] }) { 10 | return ( 11 |
12 |

Marks

13 |
14 |
15 | 16 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/academia/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { AiOutlineClockCircle } from "react-icons/ai"; 6 | import { FiPercent } from "react-icons/fi"; 7 | import { BsFillPersonCheckFill } from "react-icons/bs"; 8 | 9 | export default function NavBar() { 10 | const [currentView, setCurrentView] = useState('timetable'); 11 | const views = [ 12 | { id: 'timetable', label: }, 13 | { id: 'attendance', label: }, 14 | { id: 'marks', label: } 15 | ]; 16 | 17 | useEffect(() => { 18 | const getCurrentView = () => { 19 | return window.location.hash.slice(1) || 'timetable'; 20 | }; 21 | 22 | setCurrentView(getCurrentView()); 23 | 24 | const handleHashChange = () => { 25 | setCurrentView(getCurrentView()); 26 | }; 27 | 28 | window.addEventListener('hashchange', handleHashChange); 29 | return () => window.removeEventListener('hashchange', handleHashChange); 30 | }, []); 31 | 32 | 33 | 34 | return ( 35 | 51 | ); 52 | } -------------------------------------------------------------------------------- /app/academia/components/Timetable/EditTimetable.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useRef, useState } from "react"; 3 | import type { Schedule } from "@/types/Timetable"; 4 | import { TbPencil } from "react-icons/tb"; 5 | import { createPortal } from "react-dom"; 6 | import OptionalEditor from "./OptionalEditor"; 7 | 8 | export default function EditTimetable({ 9 | timetable, 10 | ophours, 11 | }: { timetable: Schedule[]; ophours: string[] }) { 12 | const [isOpen, setIsOpen] = useState(false); 13 | const editBox = useRef(null); 14 | 15 | useEffect(() => { 16 | editBox.current = document.getElementById( 17 | "edit-timetable", 18 | ) as HTMLDivElement; 19 | 20 | return () => { 21 | editBox.current = null; 22 | }; 23 | }, []); 24 | 25 | useEffect(() => { 26 | if (isOpen && editBox.current) { 27 | editBox.current.scrollIntoView({ behavior: "smooth" }); 28 | } 29 | }, [isOpen, editBox]); 30 | 31 | return ( 32 | <> 33 | 40 | {editBox.current && 41 | isOpen && 42 | createPortal( 43 | , 47 | editBox.current, 48 | )} 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/academia/components/Timetable/Editor.tsx: -------------------------------------------------------------------------------- 1 | import type { Schedule } from "@/types/Timetable"; 2 | import type React from "react"; 3 | import { useState } from "react"; 4 | import { FaTrashAlt } from "react-icons/fa"; 5 | 6 | function OptionalHours({ 7 | optionals, 8 | setOptionals, 9 | timetable, 10 | }: { 11 | optionals: string[]; 12 | setOptionals: React.Dispatch>; 13 | timetable: Schedule[]; 14 | }) { 15 | const handleDayChange = (index: number, day: string) => { 16 | const [, hourSlot] = optionals[index].split("-"); 17 | const newOptionals = [...optionals]; 18 | newOptionals[index] = `${day}-${hourSlot}`; 19 | setOptionals(newOptionals); 20 | }; 21 | 22 | const handleHourChange = (index: number, hour: string) => { 23 | const [dayorder, _] = optionals[index].split("-"); 24 | const newOptionals = [...optionals]; 25 | newOptionals[index] = `${dayorder}-${hour}`; 26 | setOptionals(newOptionals); 27 | }; 28 | 29 | return ( 30 |
31 | {optionals.map((hour, i) => { 32 | const [dayorder, hourSlot] = hour.split("-"); 33 | 34 | return ( 35 |
39 |
40 | 52 | 53 | 65 |
66 | 87 |
88 | ); 89 | })} 90 | 97 |
98 | ); 99 | } 100 | 101 | export default function Editor({ 102 | timetable, 103 | ophours, 104 | }: { timetable: Schedule[]; ophours: string[] }) { 105 | const [optionals, setOptionals] = useState(ophours); 106 | return ( 107 |
108 |
109 |

Optional hours

110 |

111 | Set optional hours so you don't have to confuse yourself for 112 | timetable 113 |

114 | 119 |
120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /app/academia/components/Timetable/components/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import type { ScheduleSlot } from "@/types/Timetable"; 2 | import { timeRange } from "@/utils/Range"; 3 | import { Time, timeConvert } from "@/utils/Times"; 4 | import React from "react"; 5 | 6 | export default function TableCell({ 7 | cell, 8 | first, 9 | last, 10 | index, 11 | currentTime, 12 | isClassGoing, 13 | }: { 14 | cell: ScheduleSlot | null; 15 | first?: boolean; 16 | index: number; 17 | last?: boolean; 18 | currentTime: Date; 19 | isClassGoing: boolean; 20 | }) { 21 | const start = Time.start[last ? index + 5 : index]; 22 | const end = Time.end[last ? index + 5 : index]; 23 | 24 | const inRange = timeRange(currentTime, `${start}-${end}`); 25 | 26 | return ( 27 |
30 |
33 |

34 | {cell?.name.split(":")[0]} 35 |

36 |
37 |

{cell?.roomNo}

38 | {cell?.isOptional && ( 39 |

40 | (Optional) 41 |

42 | )} 43 |
44 |
45 |

46 | {timeConvert(start)}-{timeConvert(end)} 47 |

48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/academia/components/Timetable/index.tsx: -------------------------------------------------------------------------------- 1 | import { fetchCalendar } from "@/hooks/fetchCalendar"; 2 | import type { Schedule } from "@/types/Timetable"; 3 | import React from "react"; 4 | import TimetableStack from "./components/TimetableStack"; 5 | import { FiDownload } from "react-icons/fi"; 6 | import EditTimetable from "./EditTimetable"; 7 | import { supabase } from "@/utils/Database/supabase"; 8 | import { UserInfo } from "@/types/User"; 9 | 10 | export default async function Timetable({ 11 | schedule, 12 | ophours, 13 | user, 14 | }: { 15 | schedule: Schedule[]; 16 | ophours: string[]; 17 | user: UserInfo 18 | }) { 19 | const { today, tomorrow } = await fetchCalendar(); 20 | 21 | const subscribed = true 22 | const subscribedSince = Date.now() 23 | 24 | 25 | return ( 26 |
27 |
28 |

Timetable

29 |
30 | {((subscribed && subscribedSince && (new Date().getTime() - subscribedSince) < (30 * 24 * 60 * 60 * 1000)) )? ( 31 | 36 | 37 | 38 | ) : ( 39 | 43 | 44 | 45 | )} 46 | 47 |
48 |
49 |
50 | 51 |
52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /app/academia/courses/components/Card/Class.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface ClassProps { 4 | classroom: string; 5 | category: "Practical" | "Theory"; 6 | className?: string; 7 | } 8 | 9 | export default function Class({ classroom, category, className }: ClassProps) { 10 | return category === "Practical" ? ( 11 | 18 | {classroom} 19 | 20 | ) : ( 21 | 28 | {classroom} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/academia/courses/components/Card/CourseCode.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function CourseCode({ 4 | code, 5 | className = "", 6 | }: { code: string; className?: string }) { 7 | return ( 8 | 13 | {code} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/academia/courses/components/Card/CourseTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import dynamic from "next/dynamic"; 3 | 4 | const Indicator = dynamic( 5 | () => import("@/components/Indicator").then((a) => a.default), 6 | { ssr: true }, 7 | ); 8 | 9 | type CourseTitleProps = { 10 | courseTitle: string; 11 | category: string; 12 | }; 13 | 14 | export default function CourseTitle({ 15 | courseTitle, 16 | category, 17 | }: CourseTitleProps) { 18 | return ( 19 |
24 | 25 | 26 | {courseTitle} 27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/academia/courses/components/Card/Credit.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Credit({ 4 | credit, 5 | className, 6 | }: { credit: number; className?: string }) { 7 | return ( 8 |

15 | Credit:{" "} 16 | 19 | {credit} 20 | 21 |

22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/academia/courses/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Course } from "@/types/Course"; 2 | import React from "react"; 3 | import CourseCode from "./CourseCode"; 4 | import { Link } from "next-view-transitions"; 5 | import Class from "./Class"; 6 | import { searchUrl } from "@/misc/faculties"; 7 | import Credit from "./Credit"; 8 | import CourseTitle from "./CourseTitle"; 9 | 10 | export default function CourseCard({ course, subscribed }: { course: Course; subscribed: boolean; }) { 11 | const urls = searchUrl(course.faculty.split("(")[0]); 12 | const url = urls[0]?.url; 13 | 14 | return ( 15 |
20 |
21 | 22 | 27 | 28 |
29 | 34 | 38 | 39 | {subscribed ? 46 | {course.faculty.split("(")[0]} 47 | :

{course.faculty.split("(")[0]}

} 48 | 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/academia/courses/page.tsx: -------------------------------------------------------------------------------- 1 | import { fetchUserData } from "@/hooks/fetchUserData"; 2 | import React, { Suspense } from "react"; 3 | import CourseCard from "./components/Card"; 4 | import Indicator from "@/components/Indicator"; 5 | import Loading from "@/components/States/Loading"; 6 | 7 | export default async function Courses() { 8 | const json = await fetchUserData(); 9 | 10 | const subscribed = true 11 | 12 | 13 | return ( 14 |
15 | 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/academia/faculties/Faculties.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Link } from "next-view-transitions"; 3 | import React, { useEffect, useState, useRef } from "react"; 4 | import { SearchBar } from "./components/SearchBar"; 5 | import { searchUrl, urls, type UrlSearchResult } from "@/misc/faculties"; 6 | 7 | export default function FacultyPage() { 8 | const [searchQuery, setSearchQuery] = useState(""); 9 | const [results, setResults] = useState([]); 10 | const [visibleItems, setVisibleItems] = useState(20); 11 | const observer = useRef(null); 12 | 13 | useEffect(() => { 14 | setResults(searchUrl(searchQuery)); 15 | }, [searchQuery]); 16 | 17 | useEffect(() => { 18 | if (observer.current) observer.current.disconnect(); 19 | 20 | observer.current = new IntersectionObserver((entries) => { 21 | if (entries[0].isIntersecting) { 22 | setVisibleItems((prevVisibleItems) => prevVisibleItems + 20); 23 | } 24 | }); 25 | 26 | const load = document.querySelector("#load-more"); 27 | if (load) { 28 | observer.current.observe(load); 29 | } 30 | 31 | return () => observer.current?.disconnect(); 32 | }, [results]); 33 | 34 | const displayedResults = results?.[0] ? results.slice(0, visibleItems) : []; 35 | const defUrl = urls.slice(0, visibleItems); 36 | 37 | return ( 38 |
39 | 85 | 86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /app/academia/faculties/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useRef } from "react"; 3 | import { LuSquareSlash } from "react-icons/lu"; 4 | 5 | interface SearchBarProps { 6 | searchQuery: string; 7 | setSearchQuery: (query: string) => void; 8 | } 9 | 10 | export function SearchBar({ searchQuery, setSearchQuery }: SearchBarProps) { 11 | const searchbox = useRef(null); 12 | const [isMounted, setIsMounted] = React.useState(false); 13 | 14 | useEffect(() => { 15 | function keyHandler(e: KeyboardEvent) { 16 | if (e.metaKey && e.key === "k") { 17 | e.preventDefault(); 18 | searchbox.current?.focus(); 19 | } else if (e.key === "/") { 20 | e.preventDefault(); 21 | searchbox.current?.focus(); 22 | } 23 | if (e.key === "Escape" && searchbox.current) { 24 | searchbox.current.blur(); 25 | } 26 | } 27 | 28 | setIsMounted(true); 29 | window.addEventListener("keydown", keyHandler); 30 | 31 | return () => { 32 | window.removeEventListener("keydown", keyHandler); 33 | }; 34 | }, []); 35 | 36 | if (!isMounted) return null; 37 | return ( 38 |
39 | setSearchQuery(e.target.value)} 46 | className="relative z-10 w-[250px] animate-fastfade rounded-xl bg-light-button px-4 py-2 text-lg font-medium shadow-lg outline-hidden backdrop-blur-lg dark:backdrop-blur-lg transition-all duration-200 md:w-[350px] dark:bg-dark-button" 47 | /> 48 |
49 | 50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/academia/faculties/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FacultyPage from "./Faculties"; 3 | 4 | export default async function Faculties() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /app/academia/gradex/components/Medals.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { medalStyles } from "./GradeCard"; 3 | 4 | type MedalProps = { 5 | grade: "O" | "A+" | "A" | "B+" | "B" | "C"; 6 | edit: boolean; 7 | setEdit: React.Dispatch>; 8 | }; 9 | 10 | const Medal: React.FC = ({ grade, edit, setEdit }) => { 11 | const { text, bg, border } = medalStyles[grade]; 12 | 13 | return ( 14 | 22 | ); 23 | }; 24 | 25 | export default Medal; -------------------------------------------------------------------------------- /app/academia/gradex/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { fetchUserData } from "@/hooks/fetchUserData"; 3 | import GradeCalculator from "./components/GradeCalculator"; 4 | 5 | export default async function GradeX() { 6 | const json = await fetchUserData(); 7 | const marks = json.marks?.marks 8 | const courses = json.courses?.courses 9 | 10 | return
11 | 12 |
; 13 | } 14 | -------------------------------------------------------------------------------- /app/academia/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Sidebar } from "@/components/Sidebar"; 2 | import DayOrder from "@/components/Sidebar/Badges/DayOrder"; 3 | import ProfileBadge from "@/components/Sidebar/ProfileBadge"; 4 | import { fetchUserData } from "@/hooks/fetchUserData"; 5 | import type { UserInfo } from "@/types/User"; 6 | import { Link } from "next-view-transitions"; 7 | import type { ReactNode } from "react"; 8 | import { BiLogInCircle } from "react-icons/bi"; 9 | 10 | export default async function RootLayout({ 11 | children, 12 | }: Readonly<{ 13 | children: ReactNode; 14 | }>) { 15 | const json = await fetchUserData(); 16 | 17 | const subscribed = true; 18 | return ( 19 |
20 |
21 | } 23 | mini={ 24 | 28 | } 29 | profile={ 30 | json?.error ? ( 31 | 35 | Login 36 | 37 | ) : ( 38 | 42 | ) 43 | } 44 | /> 45 |
52 |
57 |
{children}
58 |
59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /app/academia/library/LibraryHome.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'next-view-transitions' 2 | import Image from 'next/image' 3 | import React from 'react' 4 | 5 | export default function Library() { 6 | return ( 7 |
13 |
14 |
18 |

Library.

19 | 20 |

21 | Get access to Semester previous year papers or resources, all from ClassPro. 22 |

23 |
24 |
25 |
26 | 27 |
28 |
29 | Sem papers 37 |
38 |
39 |
40 |

Sem PYQs.

41 |

42 | Get your hands on all the semester papers you need. 43 |

44 |
45 |
46 | 47 | 48 |
49 |
50 | Resource papers 57 |
58 |
59 |
60 |

Resources.

61 |

62 | A dumpster fire of available files from the web. 63 |

64 |
65 |
66 | 67 |
68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /app/academia/library/components/Card.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { CoursePapers } from "@/types/CoursePapers"; 3 | import CourseCode from "./Code"; 4 | import PaperLink from "./PaperLink"; 5 | 6 | export default function Card({ result }: { result: CoursePapers }) { 7 | return ( 8 |
9 |

10 | {result.name} 11 |

12 | 13 |
14 | {result.urls.map((urlGroup) => ( 15 |
19 |
20 | 21 | 22 | Semester {urlGroup.semester} 23 | 24 |
25 |
26 | {urlGroup.urls.map((url) => ( 27 | 32 | ))} 33 |
34 |
35 | ))} 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/academia/library/components/Code.tsx: -------------------------------------------------------------------------------- 1 | export default function CourseCode({ code }: { code: string }) { 2 | return ( 3 | 4 | {code} 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /app/academia/library/components/PaperLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { AiOutlinePaperClip } from "react-icons/ai"; 3 | 4 | export default function PaperLink({ 5 | link, 6 | period, 7 | }: { 8 | link: string; 9 | period: string; 10 | }) { 11 | return ( 12 | 13 | 20 |
21 |
22 | 23 |
24 | 25 | {period} 26 | 27 |
28 | Open 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/academia/library/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useRef } from "react"; 3 | import { LuSquareSlash } from "react-icons/lu"; 4 | 5 | interface SearchBarProps { 6 | searchQuery: string; 7 | setSearchQuery: (query: string) => void; 8 | } 9 | 10 | export function SearchBar({ searchQuery, setSearchQuery }: SearchBarProps) { 11 | const searchbox = useRef(null); 12 | const [isMounted, setIsMounted] = React.useState(false); 13 | 14 | useEffect(() => { 15 | function keyHandler(e: KeyboardEvent) { 16 | if (e.metaKey && e.key === "k") { 17 | e.preventDefault(); 18 | searchbox.current?.focus(); 19 | } else if (e.key === "/") { 20 | e.preventDefault(); 21 | searchbox.current?.focus(); 22 | } 23 | if (e.key === "Escape" && searchbox.current) { 24 | searchbox.current.blur(); 25 | } 26 | } 27 | 28 | setIsMounted(true); 29 | window.addEventListener("keydown", keyHandler); 30 | 31 | return () => { 32 | window.removeEventListener("keydown", keyHandler); 33 | }; 34 | }, []); 35 | 36 | if (!isMounted) return null; 37 | return ( 38 |
39 | setSearchQuery(e.target.value)} 46 | className="relative z-10 w-[250px] animate-fastfade rounded-xl bg-light-button px-4 py-2 text-lg font-medium shadow-lg outline-hidden backdrop-blur-lg dark:backdrop-blur-lg transition-all duration-200 md:w-[350px] dark:bg-dark-button" 47 | /> 48 |
49 | 50 |
51 |
52 | ); 53 | } -------------------------------------------------------------------------------- /app/academia/library/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Library from "@/app/academia/library/LibraryHome"; 3 | 4 | export default async function Docupro() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /app/academia/library/pyq/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Library from "./PYQ"; 3 | import { fetchUserData } from "@/hooks/fetchUserData"; 4 | import { fetchFileArray } from "@/hooks/fetchFiles"; 5 | 6 | export default async function Docupro() { 7 | const { courses } = await fetchUserData(); 8 | 9 | const files = await fetchFileArray(); 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /app/academia/library/render/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { decodeString } from "@/misc/encode"; 2 | 3 | export async function GET(req: Request) { 4 | const { searchParams } = new URL(req.url); 5 | const id = searchParams.get("id"); 6 | const decodedId = decodeString(id as string); 7 | return Response.redirect(`${decodedId}`); 8 | } 9 | 10 | export const config = { 11 | runtime: "edge", 12 | }; 13 | -------------------------------------------------------------------------------- /app/academia/library/render/route.ts: -------------------------------------------------------------------------------- 1 | import { decodeString } from "@/misc/encode"; 2 | 3 | export async function GET(req: Request) { 4 | const agent = req.headers.get("user-agent"); 5 | if ( 6 | !agent?.includes("Google AppsViewer;") || 7 | !agent?.includes("drive.google.com") 8 | ) 9 | return new Response("NUH UH", { status: 404 }); 10 | const { searchParams } = new URL(req.url); 11 | const id = searchParams.get("id"); 12 | const decodedId = decodeString(id as string); 13 | return Response.redirect(`${decodedId}`); 14 | } 15 | 16 | export const config = { 17 | runtime: "edge", 18 | }; 19 | -------------------------------------------------------------------------------- /app/academia/library/resources/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Resources from "./Resources"; 3 | import { fetchResourcesArray } from "@/hooks/fetchResources"; 4 | 5 | export default async function Docupro() { 6 | const folders = await fetchResourcesArray(); 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /app/academia/links/components/LinkList.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "next-view-transitions"; 2 | import React from "react"; 3 | import type { DirLink } from "@/misc/links"; 4 | 5 | export default function LinkList({ url }: { url: DirLink }) { 6 | return ( 7 | 8 | 9 |
16 |
17 | 20 | {url.site} 21 | 22 | 27 | {url.url} 28 | 29 |
30 |

{url.description}

31 |
32 | 33 | ); 34 | } 35 | 36 | function Svg() { 37 | return ( 38 | 47 | Decorative Line 48 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /app/academia/links/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useRef } from "react"; 3 | import { LuSquareSlash } from "react-icons/lu"; 4 | 5 | interface SearchBarProps { 6 | searchQuery: string; 7 | setSearchQuery: (query: string) => void; 8 | } 9 | 10 | export function SearchBar({ searchQuery, setSearchQuery }: SearchBarProps) { 11 | const searchbox = useRef(null); 12 | const [isMounted, setIsMounted] = React.useState(false); 13 | 14 | useEffect(() => { 15 | function keyHandler(e: KeyboardEvent) { 16 | if (e.metaKey && e.key === "k") { 17 | e.preventDefault(); 18 | searchbox.current?.focus(); 19 | } else if (e.key === "/") { 20 | e.preventDefault(); 21 | searchbox.current?.focus(); 22 | } 23 | if (e.key === "Escape" && searchbox.current) { 24 | searchbox.current.blur(); 25 | } 26 | } 27 | 28 | setIsMounted(true); 29 | window.addEventListener("keydown", keyHandler); 30 | 31 | return () => { 32 | window.removeEventListener("keydown", keyHandler); 33 | }; 34 | }, []); 35 | 36 | if (!isMounted) return null; 37 | return ( 38 |
39 | setSearchQuery(e.target.value)} 46 | className="relative z-10 w-[250px] animate-fastfade rounded-xl bg-light-button px-4 py-2 text-lg font-medium shadow-lg outline-hidden backdrop-blur-lg dark:backdrop-blur-lg transition-all duration-200 md:w-[350px] dark:bg-dark-button" 47 | /> 48 |
49 | 50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/academia/links/components/URLSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import LinkList from "./LinkList"; 3 | import type { DirLink } from "@/misc/links"; 4 | 5 | interface UrlSectionProps { 6 | title: string; 7 | urls: DirLink[]; 8 | className?: string; 9 | special?: boolean; 10 | } 11 | 12 | export function UrlSection({ 13 | title, 14 | urls, 15 | className, 16 | special, 17 | }: UrlSectionProps) { 18 | return ( 19 |
20 |

25 | {title} 26 |

27 |
30 | {urls.map((url, i) => ( 31 | 32 | ))} 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/academia/links/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Link } from "next-view-transitions"; 3 | import React, { useState } from "react"; 4 | import { SearchBar } from "./components/SearchBar"; 5 | import { UrlSection } from "./components/URLSection"; 6 | import useSearch from "@/hooks/useSearch"; 7 | 8 | export default function LinksPage() { 9 | const [searchQuery, setSearchQuery] = useState(""); 10 | 11 | const { priority, officials, others } = useSearch({ searchQuery }); 12 | return ( 13 |
14 | 35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/academia/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import dynamic from "next/dynamic"; 3 | import Loading from "@/components/States/Loading"; 4 | import { fetchUserData } from "@/hooks/fetchUserData"; 5 | import { supabase } from "@/utils/Database/supabase"; 6 | import { cookies } from "next/headers"; 7 | import { encode } from "@/utils/Cookies"; 8 | 9 | const Attendance = dynamic(() => import("./components/Attendance"), { 10 | ssr: true, 11 | }); 12 | 13 | const Marks = dynamic(() => import("./components/Marks"), { ssr: true }); 14 | const Timetable = dynamic(() => import("./components/Timetable"), { 15 | ssr: true, 16 | }); 17 | 18 | const NavBar = dynamic(() => import("./components/NavBar"), { ssr: true }); 19 | 20 | export default async function Academia() { 21 | const json = await fetchUserData(); 22 | const cookie = (await cookies()).get("key"); 23 | let ophours: string[] = []; 24 | 25 | const { data, error } = await supabase 26 | .from("goscrape") 27 | .select("ophour") 28 | .eq("token", encode(cookie?.value ?? "")) 29 | .single(); 30 | 31 | if (error) { 32 | if (error.code === "PGRST116") ophours = json.ophour?.split(",") ?? []; 33 | else console.error("Error fetching ophours:", error); 34 | } else { 35 | ophours = data?.ophour?.split(","); 36 | } 37 | 38 | return ( 39 | <> 40 |
41 | }> 42 | 47 | 48 | }> 49 | 50 | 51 | }> 52 | 53 | 54 |
55 | 56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /app/api/logout/route.ts: -------------------------------------------------------------------------------- 1 | import { token } from "@/utils/Tokenize"; 2 | import rotateUrl from "@/utils/URL"; 3 | import { cookies } from "next/headers"; 4 | 5 | export async function DELETE() { 6 | const cookie = await cookies(); 7 | 8 | const a = await fetch(`${rotateUrl()}/logout`, { 9 | method: "DELETE", 10 | headers: { 11 | cookie: `${cookie.get("key")?.value ?? ""}`, 12 | Authorization: `Bearer ${token()}`, 13 | "X-CSRF-Token": cookie.get("key")?.value ?? "", 14 | Origin: "https://class-pro.vercel.app", 15 | }, 16 | }); 17 | 18 | if (a.ok) { 19 | for (const c of cookie.getAll()) { 20 | cookie.delete(c); 21 | } 22 | return Response.json({ message: "Logged out" }); 23 | } 24 | return Response.json({ message: "Failed to log out" }); 25 | } 26 | -------------------------------------------------------------------------------- /app/api/ophours/route.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from "@/utils/Database/supabase"; 2 | import { cookies } from "next/headers"; 3 | import { encode } from "@/utils/Cookies"; 4 | 5 | export async function POST(req: Request) { 6 | const cookie = await cookies(); 7 | const key = cookie.get("key")?.value ?? ""; 8 | const body = await req.json(); 9 | const { ophours } = body; 10 | 11 | if ( 12 | !Array.isArray(ophours) || 13 | !ophours.every((item) => typeof item === "string") 14 | ) { 15 | return Response.json( 16 | { error: "Invalid input" }, 17 | { 18 | status: 400, 19 | }, 20 | ); 21 | } 22 | 23 | const ophoursString = ophours.join(","); 24 | 25 | try { 26 | const { error } = await supabase 27 | .from("goscrape") 28 | .update({ ophour: ophoursString }) 29 | .eq("token", encode(key)); 30 | 31 | if (error) { 32 | return Response.json( 33 | { error: error.message }, 34 | { 35 | status: 400, 36 | }, 37 | ); 38 | } 39 | 40 | return Response.json( 41 | { success: true }, 42 | { 43 | status: 200, 44 | }, 45 | ); 46 | } catch (error) { 47 | return Response.json( 48 | { error: (error as any).message }, 49 | { 50 | status: 500, 51 | }, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/auth/login/components/Form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useCallback, useState } from "react"; 3 | import UidInput from "./form/UidInput"; 4 | import PasswordInput from "./form/PasswordInput"; 5 | import rotateUrl from "@/utils/URL"; 6 | import Button from "@/components/Button"; 7 | import { token } from "@/utils/Tokenize"; 8 | import { useTransitionRouter } from "next-view-transitions"; 9 | import Link from "next/link"; 10 | import { setCookie } from "@/utils/Cookies"; 11 | 12 | export default function Form() { 13 | const router = useTransitionRouter(); 14 | const [uid, setUid] = useState(""); 15 | const [pass, setPass] = useState(""); 16 | 17 | const [status, setStatus] = useState(0); 18 | const [statusMessage, setMessage] = useState(""); 19 | 20 | const handleLogin = useCallback(async (account: string, password: string) => { 21 | setStatus(1); 22 | const login = await fetch(`${rotateUrl()}/login`, { 23 | method: "POST", 24 | headers: { 25 | Authorization: `Bearer ${token()}`, 26 | "content-type": "application/json", 27 | }, 28 | body: JSON.stringify({ 29 | account: account.replaceAll(" ", "").replace("@srmist.edu.in", ""), 30 | password: password, 31 | }), 32 | }); 33 | 34 | if (!login.ok) { 35 | setStatus(-1); 36 | setMessage("Server down."); 37 | } 38 | 39 | const loginResponse = await login.json(); 40 | 41 | if (loginResponse.authenticated) { 42 | setStatus(2); 43 | setMessage("Loading data..."); 44 | if(!loginResponse.cookies) { 45 | setStatus(-1); 46 | setMessage("No cookies received. Wrong password."); 47 | return; 48 | } 49 | setCookie("key", loginResponse.cookies); 50 | 51 | router.push("/academia"); 52 | } else if (loginResponse?.message) { 53 | setStatus(-1); 54 | if (loginResponse.message?.includes("Digest")) 55 | setMessage( 56 | "Seems like this is your first time. Go to academia.srmist.edu.in and setup password!", 57 | ); 58 | else setMessage(loginResponse?.message); 59 | } 60 | }, []); 61 | 62 | return ( 63 |
{ 66 | e.preventDefault(); 67 | }} 68 | > 69 | {status === -1 && ( 70 |

71 | {statusMessage?.includes(">_") ? "" : "SRM:"} 72 | {statusMessage?.replace(">_", "")} 73 |

74 | )} 75 | 76 | {status === 2 && statusMessage && ( 77 |

78 | {statusMessage} 79 |

80 | )} 81 |
82 | 83 | 84 |
85 | 86 |
87 | 103 | 107 | Forgot 108 | 109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /app/auth/login/components/form/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { 4 | useState, 5 | type Dispatch, 6 | type SetStateAction, 7 | type KeyboardEvent, 8 | } from "react"; 9 | import { BsEyeSlashFill, BsEyeFill } from "react-icons/bs"; 10 | 11 | export default function PasswordInput({ 12 | password, 13 | setPassword, 14 | }: { 15 | password: string; 16 | setPassword: Dispatch>; 17 | }) { 18 | const [visible, setVisible] = useState(false); 19 | 20 | const handleKeyDown = (e: KeyboardEvent) => { 21 | if (e.key === "Enter") return setVisible(false); 22 | if (e.altKey) { 23 | setVisible((prev) => !prev); 24 | } 25 | }; 26 | 27 | return ( 28 | <> 29 | setPassword(e.target.value)} 35 | placeholder="Passw*rd" 36 | /> 37 | {password && ( 38 | 47 | )} 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /app/auth/login/components/form/UidInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { type Dispatch, type SetStateAction } from "react"; 4 | 5 | export default function UidInput({ 6 | uid, 7 | setUid, 8 | }: { 9 | uid: string; 10 | setUid: Dispatch>; 11 | }) { 12 | return ( 13 | setUid(e.target.value)} 18 | placeholder="User ID" 19 | /> 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Form from "./components/Form"; 3 | 4 | import { FaBookOpen } from "react-icons/fa"; 5 | import { redirect } from "next/navigation"; 6 | import { IoLockClosed } from "react-icons/io5"; 7 | import { Link as Alink } from "next-view-transitions"; 8 | import ThemeToggle from "@/components/themes/ThemeToggle"; 9 | import { fetchUserData } from "@/hooks/fetchUserData"; 10 | 11 | export default async function Login() { 12 | const d = await fetchUserData(); 13 | if (d.user) redirect("/academia"); 14 | 15 | return ( 16 |
17 | 25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |

ClassPro

34 | 35 |
36 |

37 | University data, beautifully presented at your fingertips 38 |

39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 |

47 | Transparent, Open-source and secure. 48 |

49 |
50 |
51 | 55 | Help 56 | 57 | 61 | Privacy 62 | 63 |
64 |
65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /app/auth/logout/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import React, { useEffect } from "react"; 4 | import { RiLoader3Fill } from "react-icons/ri"; 5 | 6 | export default function Logout() { 7 | const router = useRouter(); 8 | 9 | useEffect(() => { 10 | (async () => { 11 | const a = await fetch("/api/logout", { 12 | method: "DELETE", 13 | }); 14 | 15 | const body = await a.json(); 16 | console.info(body); 17 | if (a.ok) router.push("/"); 18 | })(); 19 | }, []); 20 | 21 | return ( 22 |
23 | 27 |

Logging out...

28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Error components must be Client Components 2 | 3 | import { useEffect } from "react"; 4 | import { BiError } from "react-icons/bi"; 5 | 6 | export default function ErrorComponent({ error }: { error: Error | string }) { 7 | useEffect(() => { 8 | console.group('Error Details'); 9 | console.error('Error object:', error); 10 | if (error instanceof Error) { 11 | console.error('Stack trace:', error.stack); 12 | console.error('Error name:', error.name); 13 | console.error('Component stack:', (error as any).componentStack); 14 | } 15 | console.groupEnd(); 16 | }, [error]); 17 | 18 | // Format error message for display 19 | const getErrorMessage = () => { 20 | if (typeof error === 'string') return error; 21 | if (error instanceof Error) { 22 | return `${error.name}: ${error.message}`; 23 | } 24 | return 'An unknown error occurred'; 25 | }; 26 | 27 | // Get additional debug info 28 | const getDebugInfo = () => { 29 | if (!(error instanceof Error)) return ''; 30 | const debugInfo = []; 31 | 32 | if (error.stack) { 33 | const stackLines = error.stack.split('\n'); 34 | // Extract the most relevant stack frames 35 | debugInfo.push('Stack Trace (most recent call first):'); 36 | debugInfo.push(...stackLines.slice(0, 5)); 37 | } 38 | 39 | if ((error as any).componentStack) { 40 | debugInfo.push('\nComponent Hierarchy:'); 41 | debugInfo.push((error as any).componentStack); 42 | } 43 | 44 | return debugInfo.join('\n'); 45 | }; 46 | 47 | return ( 48 | 49 |
50 | 51 |

52 | Error. 53 |

54 |

55 | *intense crash sound* 56 |

57 | 58 |
59 | 						
60 | 							
{getErrorMessage()}
61 |
{getDebugInfo()}
62 |
63 |
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Error components must be Client Components 2 | 3 | import { useEffect } from "react"; 4 | import { BiError } from "react-icons/bi"; 5 | 6 | export default function ErrorComponent({ error }: { error: Error | string }) { 7 | useEffect(() => { 8 | console.group('Error Details'); 9 | console.error('Error object:', error); 10 | if (error instanceof Error) { 11 | console.error('Stack trace:', error.stack); 12 | console.error('Error name:', error.name); 13 | console.error('Component stack:', (error as any).componentStack); 14 | } 15 | console.groupEnd(); 16 | }, [error]); 17 | 18 | // Format error message for display 19 | const getErrorMessage = () => { 20 | if (typeof error === 'string') return error; 21 | if (error instanceof Error) { 22 | return `${error.name}: ${error.message}`; 23 | } 24 | return 'An unknown error occurred'; 25 | }; 26 | 27 | // Get additional debug info 28 | const getDebugInfo = () => { 29 | if (!(error instanceof Error)) return ''; 30 | const debugInfo = []; 31 | 32 | if (error.stack) { 33 | const stackLines = error.stack.split('\n'); 34 | // Extract the most relevant stack frames 35 | debugInfo.push('Stack Trace (most recent call first):'); 36 | debugInfo.push(...stackLines.slice(0, 5)); 37 | } 38 | 39 | if ((error as any).componentStack) { 40 | debugInfo.push('\nComponent Hierarchy:'); 41 | debugInfo.push((error as any).componentStack); 42 | } 43 | 44 | return debugInfo.join('\n'); 45 | }; 46 | 47 | return ( 48 |
49 |
50 |
51 | 52 |

53 | Error. 54 |

55 |

56 | *intense crash sound* 57 |

58 | 59 |
60 | 						
61 | 							
{getErrorMessage()}
62 |
{getDebugInfo()}
63 |
64 |
65 |
66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /app/home/page.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "next-view-transitions"; 2 | import { FaBookOpen } from "react-icons/fa"; 3 | import ThemeToggle from "@/components/themes/ThemeToggle"; 4 | import { cookies } from "next/headers"; 5 | import { VscMegaphone } from "react-icons/vsc"; 6 | import Image from "next/image"; 7 | 8 | export default async function Academia() { 9 | const cookie = (await cookies()).get("key"); 10 | return ( 11 |
12 |
13 |
14 | 15 |

16 | ClassPro. 17 |

18 |
19 | 20 |
21 | 22 |
23 | {Array.from({ length: 20 }).map((_, index) => ( 24 |
26 | key={index} 27 | className="absolute w-1 h-1 bg-light-accent dark:bg-dark-accent rounded-full" 28 | style={{ 29 | top: `${Math.random() * 100}%`, 30 | left: `${Math.random() * 100}%`, 31 | opacity: Math.random(), 32 | animation: `twinkle ${Math.random() * 2 + 2}s infinite, move ${Math.random() * 2 + 2}s infinite alternate`, 33 | }} 34 | /> 35 | ))} 36 |
37 | 38 | 46 | 50 | Read about ClassPro v3 51 | 52 | 53 |

54 | Better way to manage 55 |
56 | your academics. 57 |

58 |

59 | View, predict, and strategize your success. 60 |

61 |
62 | {cookie?.value ? ( 63 | 67 | Dashboard 68 | 69 | ) : ( 70 | 74 | Login 75 | 76 | )} 77 | 81 | SRM Academia 82 | 83 |
84 | 85 | Hero 92 | Hero 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /app/invalid/page.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "next-view-transitions"; 2 | import React from "react"; 3 | import { FaQuestion } from "react-icons/fa"; 4 | import { VscSquirrel } from "react-icons/vsc"; 5 | 6 | export default function invalid() { 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 |
14 |

Session expired/invalid

15 |

16 | Our squirrels (delulu) found out you have an invalid or expired 17 | session token, Because SRM didn't return us the page with your 18 | token, we want you to reset and login again 19 |

20 | 21 | 25 | Reset 26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import { GeistSans } from "geist/font/sans"; 3 | import { GeistMono } from "geist/font/mono"; 4 | import { Analytics } from "@vercel/analytics/react"; 5 | 6 | import "./globals.css"; 7 | import { ThemeProvider } from "@/provider/ThemeProvider"; 8 | import { Themes } from "@/misc/theme"; 9 | import { ViewTransitions } from "next-view-transitions"; 10 | import ErrorBoundary from "./Boundary"; 11 | import { SpeedInsights } from "@vercel/speed-insights/next"; 12 | import type { ReactNode } from "react"; 13 | import Script from "next/script"; 14 | 15 | const APP_NAME = "ClassPro"; 16 | const APP_DEFAULT_TITLE = "ClassPro"; 17 | const APP_TITLE_TEMPLATE = "%s - PWA App"; 18 | const APP_DESCRIPTION = "Better way to manage your academics."; 19 | const PRODUCTION_URL = "https://class-pro.vercel.app"; 20 | 21 | export const metadata: Metadata = { 22 | metadataBase: new URL( 23 | process.env.NODE_ENV === "production" 24 | ? PRODUCTION_URL 25 | : "http://localhost:0243", 26 | ), 27 | applicationName: APP_NAME, 28 | title: { 29 | default: APP_DEFAULT_TITLE, 30 | template: APP_TITLE_TEMPLATE, 31 | }, 32 | description: APP_DESCRIPTION, 33 | manifest: "/manifest.json", 34 | appleWebApp: { 35 | capable: true, 36 | statusBarStyle: "black-translucent", 37 | title: APP_DEFAULT_TITLE, 38 | }, 39 | formatDetection: { 40 | telephone: false, 41 | }, 42 | openGraph: { 43 | type: "website", 44 | siteName: APP_NAME, 45 | title: { 46 | default: APP_DEFAULT_TITLE, 47 | template: APP_TITLE_TEMPLATE, 48 | }, 49 | description: APP_DESCRIPTION, 50 | images: [ 51 | { 52 | url: "/images/og.png", 53 | width: 1280, 54 | height: 720, 55 | alt: APP_NAME, 56 | }, 57 | ], 58 | }, 59 | twitter: { 60 | card: "summary_large_image", 61 | title: { 62 | default: APP_DEFAULT_TITLE, 63 | template: APP_TITLE_TEMPLATE, 64 | }, 65 | description: APP_DESCRIPTION, 66 | images: ["/images/og.png"], 67 | }, 68 | icons: { 69 | icon: [ 70 | { 71 | url: "/icons/icon.svg", 72 | sizes: "192x192", 73 | type: "image/svg+xml", 74 | }, 75 | ], 76 | apple: [ 77 | { 78 | url: "/icons/icon.svg", 79 | sizes: "192x192", 80 | type: "image/svg+xml", 81 | }, 82 | ], 83 | }, 84 | }; 85 | 86 | export const viewport: Viewport = { 87 | themeColor: "#11151b", 88 | }; 89 | 90 | export default async function RootLayout({ 91 | children, 92 | }: Readonly<{ 93 | children: ReactNode; 94 | }>) { 95 | return ( 96 | 97 | 101 | 102 | 103 | 104 | t.title === "Dark")?.properties.metacolor 109 | } 110 | /> 111 | t.title === "Light")?.properties.metacolor 116 | } 117 | /> 118 | 119 | t.title === "Dark")?.properties.metacolor 123 | } 124 | /> 125 | 126 | 127 | 128 | 129 | 130 | {children} 131 | 132 | 133 | 134 | 135 | 136 | ); 137 | } 138 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FaHourglassStart } from 'react-icons/fa6' 3 | 4 | export default function loading() { 5 | return ( 6 |
7 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/manifest.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from "next"; 2 | 3 | export default function manifest(): MetadataRoute.Manifest { 4 | return { 5 | name: "ClassPro", 6 | short_name: "ClassPro", 7 | description: "Better way to manage your academics.", 8 | theme_color: "#11151b", 9 | lang: "en", 10 | background_color: "#11151b", 11 | start_url: "https://class-pro.vercel.app/", 12 | scope: "https://class-pro.vercel.app/", 13 | launch_handler: { 14 | client_mode: "focus-existing", 15 | }, 16 | display_override: ["fullscreen", "window-controls-overlay"], 17 | display: "standalone", 18 | id: "7272005", 19 | dir: "ltr", 20 | orientation: "portrait", 21 | categories: [ 22 | "education", 23 | "lifestyle", 24 | "navigation", 25 | "personalization", 26 | "productivity", 27 | ], 28 | icons: [ 29 | { 30 | src: "/icons/maskable_icon_x192.png", 31 | sizes: "192x192", 32 | type: "image/png", 33 | purpose: "maskable", 34 | }, 35 | { 36 | src: "/icons/maskable_icon_x512.png", 37 | sizes: "512x512", 38 | type: "image/png", 39 | purpose: "maskable", 40 | }, 41 | ], 42 | screenshots: [ 43 | { 44 | src: "/screenshots/phone/academia.webp", 45 | type: "image/webp", 46 | sizes: "430x932", 47 | form_factor: "narrow", 48 | label: "Academia Page", 49 | platform: "ios", 50 | }, 51 | { 52 | src: "/screenshots/phone/calendar.webp", 53 | type: "image/webp", 54 | sizes: "430x932", 55 | form_factor: "narrow", 56 | label: "Calendar Planner", 57 | platform: "ios", 58 | }, 59 | { 60 | src: "/screenshots/phone/faculties.webp", 61 | type: "image/webp", 62 | sizes: "430x932", 63 | form_factor: "narrow", 64 | label: "Faculties Search", 65 | platform: "ios", 66 | }, 67 | { 68 | src: "/screenshots/phone/predict.webp", 69 | type: "image/webp", 70 | sizes: "430x932", 71 | form_factor: "narrow", 72 | label: "Attendance Prediction", 73 | platform: "ios", 74 | }, 75 | { 76 | src: "/screenshots/wide/academia.webp", 77 | type: "image/webp", 78 | sizes: "1600x940", 79 | label: "Academia Page", 80 | form_factor: "wide", 81 | }, 82 | { 83 | src: "/screenshots/wide/calendar.webp", 84 | type: "image/webp", 85 | sizes: "1600x940", 86 | label: "Calendar Planner", 87 | form_factor: "wide", 88 | }, 89 | { 90 | src: "/screenshots/wide/faculties.webp", 91 | type: "image/webp", 92 | sizes: "1600x940", 93 | form_factor: "wide", 94 | label: "Faculties Search", 95 | }, 96 | { 97 | src: "/screenshots/wide/links.webp", 98 | type: "image/webp", 99 | sizes: "1600x940", 100 | form_factor: "wide", 101 | label: "Links Search", 102 | }, 103 | { 104 | src: "/screenshots/wide/predict.webp", 105 | type: "image/webp", 106 | sizes: "1600x940", 107 | form_factor: "wide", 108 | label: "Attendance Prediction", 109 | }, 110 | ], 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTransitionRouter } from "next-view-transitions"; 3 | 4 | import React from "react"; 5 | import { FaQuestion } from "react-icons/fa6"; 6 | 7 | export default function NotFoundPage() { 8 | const router = useTransitionRouter(); 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 |

Why you here?

16 |

17 | We don't know what you were looking for, but it never existed. 18 | Don't worry, i gotchu. 19 |

20 | 21 | 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/offline/page.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "next-view-transitions"; 2 | import React from "react"; 3 | import { GrSatellite } from "react-icons/gr"; 4 | 5 | export default function offline() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 |

No Internet.

13 |

14 | Seems like you are offline, which sucks. Please check your internet 15 | connection and try again. 16 |

17 | 18 | 22 | Go back 23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Academia from "./home/page"; 2 | 3 | export default function Index() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/payment/page.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "next-view-transitions"; 2 | import React from "react"; 3 | import { FaCrown } from "react-icons/fa6"; 4 | 5 | export default function PayRequired() { 6 | return ( 7 |
10 |
11 | 12 |
13 |

Payment Required

14 |

15 | This feature is only available to supporters only. Please support me by subscribing. 16 |

17 |
18 |
19 |
20 | 21 | Support 22 | 23 | 24 | Back 25 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/ratelimit/page.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "next-view-transitions"; 2 | import React from "react"; 3 | import { HiOutlineHandRaised } from "react-icons/hi2"; 4 | 5 | export default function ratelimit() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 |

Chill mate.

13 |

14 | Stop spamming our free tier servers, We are working tirelessly to make 15 | this as a free service, So try again after some time 16 |

17 | 18 | 22 | Go back 23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/sleeping/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTransitionRouter } from "next-view-transitions"; 3 | import React from "react"; 4 | import { HiServerStack } from "react-icons/hi2"; 5 | import { RiZzzFill } from "react-icons/ri"; 6 | 7 | export default function Sleeping() { 8 | const router = useTransitionRouter(); 9 | return ( 10 |
11 |
12 | 13 | 14 |
15 |
16 |

Waking up..

17 |

18 | Our servers were busy sleeping.. We are waking them up now. Please 19 | wait for sometime and try again later. 20 |

21 | 22 | 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/suspended/page.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "next-view-transitions"; 2 | import React from "react"; 3 | import { BsLifePreserver } from "react-icons/bs"; 4 | 5 | export default function Suspended() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 |

Server Died.

13 |

14 | Our servers are suspended by our hosting platform as we are hosting it for free. Sorry for inconvenience. 15 |

16 | 17 | 21 | Retry 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/sw.ts: -------------------------------------------------------------------------------- 1 | import { defaultCache } from "@serwist/next/worker"; 2 | import type { PrecacheEntry, SerwistGlobalConfig } from "serwist"; 3 | import { Serwist } from "serwist"; 4 | 5 | // This declares the value of `injectionPoint` to TypeScript. 6 | // `injectionPoint` is the string that will be replaced by the 7 | // actual precache manifest. By default, this string is set to 8 | // `"self.__SW_MANIFEST"`. 9 | declare global { 10 | interface WorkerGlobalScope extends SerwistGlobalConfig { 11 | __SW_MANIFEST: (PrecacheEntry | string)[] | undefined; 12 | } 13 | } 14 | 15 | declare const self: ServiceWorkerGlobalScope; 16 | 17 | const serwist = new Serwist({ 18 | precacheEntries: self.__SW_MANIFEST, 19 | skipWaiting: true, 20 | clientsClaim: true, 21 | navigationPreload: true, 22 | runtimeCaching: defaultCache, 23 | disableDevLogs: true, 24 | offlineAnalyticsConfig: true, 25 | fallbacks: { 26 | entries: [ 27 | { 28 | url: "/offline", 29 | matcher({ request }) { 30 | return request.destination === "document"; 31 | }, 32 | }, 33 | ], 34 | }, 35 | precacheOptions: { 36 | cleanupOutdatedCaches: true, 37 | concurrency: 20, 38 | }, 39 | }); 40 | 41 | // Notification Handler 42 | self.addEventListener("push", (event) => { 43 | const options = { 44 | body: event.data?.text() ?? "No payload", 45 | icon: "/icon.png", 46 | vibrate: [100, 50, 100], 47 | }; 48 | 49 | event.waitUntil( 50 | self.registration.showNotification("Push Notification", options), 51 | ); 52 | }); 53 | 54 | // Cache 55 | self.addEventListener("fetch", (event) => { 56 | if (event.request.url === "/academia") { 57 | return false; 58 | } 59 | }); 60 | 61 | serwist.addEventListeners(); 62 | -------------------------------------------------------------------------------- /app/view/page.tsx: -------------------------------------------------------------------------------- 1 | import { fetchUserData } from '@/hooks/fetchUserData'; 2 | import { type Schedule, type ScheduleSlot } from '@/types/Timetable'; 3 | import { Time, timeConvert } from '@/utils/Times'; 4 | import React from 'react' 5 | 6 | export default async function ViewAll() { 7 | const {timetable, ophour} = await fetchUserData(); 8 | 9 | const ophours = ophour?.split(","); 10 | if (ophours?.[0]) { 11 | for (const ophour of ophours) { 12 | const [day, hour] = ophour.split("-"); 13 | const dayIndex = Number.parseInt(day.replace("D", "")) - 1; 14 | const hourIndex = Number.parseInt(hour.replace("H", "")) - 1; 15 | 16 | const slot = timetable.schedule[dayIndex]?.table[hourIndex]; 17 | if (slot) slot.isOptional = true; 18 | } 19 | } 20 | 21 | 22 | return ( 23 |
24 |
25 |
26 | 27 | 28 |
29 |
30 |
31 | ) 32 | } 33 | 34 | function TimeArr() { 35 | return ( 36 |
37 | {Time.start.map((start, index) => ( 38 |
42 |

43 | {timeConvert(start)} - {timeConvert(Time.end[index])} 44 |

45 |
46 | ))} 47 |
48 | ); 49 | } 50 | 51 | function TimetableImage({ timetable }: { timetable: Schedule[] }) { 52 | return ( 53 |
56 | {timetable.map((item, index) => ( 57 | 58 | ))} 59 |
60 | ); 61 | } 62 | 63 | function ImageGenerator({ timetable }: { timetable: Schedule }) { 64 | const theoryPosition = timetable?.table 65 | ?.slice(0, 5) 66 | .some((item) => item?.courseType === "Theory") 67 | ? 0 68 | : 1; 69 | 70 | return ( 71 |
72 |
75 | {timetable?.table?.slice(0, 5).map((item, index) => ( 76 | 77 | ))} 78 |
79 |
82 | {timetable?.table?.slice(5, 10).map((item, index) => ( 83 | 84 | ))} 85 |
86 |
87 | ); 88 | } 89 | 90 | function TableCell({ cell }: { cell: ScheduleSlot | null }) { 91 | return ( 92 |
95 |

96 | {cell?.name.split(":")[0]} 97 |

98 |
99 |

{cell?.roomNo}

100 | {cell?.isOptional &&

(Optional)

} 101 |
102 |
103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "rules": { 4 | "a11y": { 5 | "useSemanticElements": "off" 6 | }, 7 | "suspicious": { 8 | "noDebugger": "off", 9 | "noConsoleLog": "info", 10 | "noArrayIndexKey": "off", 11 | "noExplicitAny": "off" 12 | }, 13 | "correctness": { 14 | "useExhaustiveDependencies": "off" 15 | }, 16 | "style": { 17 | "noShoutyConstants": "warn", 18 | "useNamingConvention": "warn" 19 | } 20 | } 21 | }, 22 | "formatter": { 23 | "enabled": true, 24 | "formatWithErrors": false, 25 | "ignore": [], 26 | "attributePosition": "auto", 27 | "indentStyle": "tab", 28 | "indentWidth": 2, 29 | "lineWidth": 80, 30 | "lineEnding": "lf" 31 | }, 32 | "javascript": { 33 | "formatter": { 34 | "arrowParentheses": "always", 35 | "bracketSameLine": false, 36 | "bracketSpacing": true, 37 | "jsxQuoteStyle": "double", 38 | "quoteProperties": "asNeeded", 39 | "semicolons": "always", 40 | "trailingCommas": "all" 41 | } 42 | }, 43 | "json": { 44 | "formatter": { 45 | "trailingCommas": "none" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentProps } from "react"; 2 | 3 | export default function Button({ 4 | children, 5 | ...props 6 | }: ComponentProps<"button">) { 7 | return ( 8 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/Error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Error components must be Client Components 2 | 3 | import { Link } from "next-view-transitions"; 4 | import { useEffect } from "react"; 5 | import { BiError } from "react-icons/bi"; 6 | 7 | export default function ErrorComponent({ error }: { error: Error | string }) { 8 | useEffect(() => { 9 | // Enhanced error logging with more context 10 | console.group('Error Details'); 11 | console.error('Error object:', error); 12 | if (error instanceof Error) { 13 | console.error('Stack trace:', error.stack); 14 | console.error('Error name:', error.name); 15 | console.error('Component stack:', (error as any).componentStack); 16 | } 17 | console.groupEnd(); 18 | }, [error]); 19 | 20 | // Format error message for display 21 | const getErrorMessage = () => { 22 | if (typeof error === 'string') return error; 23 | if (error instanceof Error) { 24 | return `${error.name}: ${error.message}`; 25 | } 26 | return 'An unknown error occurred'; 27 | }; 28 | 29 | // Get additional debug info 30 | const getDebugInfo = () => { 31 | if (!(error instanceof Error)) return ''; 32 | const debugInfo = []; 33 | 34 | if (error.stack) { 35 | const stackLines = error.stack.split('\n'); 36 | // Extract the most relevant stack frames 37 | debugInfo.push('Stack Trace (most recent call first):'); 38 | debugInfo.push(...stackLines.slice(0, 5)); 39 | } 40 | 41 | if ((error as any).componentStack) { 42 | debugInfo.push('\nComponent Hierarchy:'); 43 | debugInfo.push((error as any).componentStack); 44 | } 45 | 46 | return debugInfo.join('\n'); 47 | }; 48 | 49 | return ( 50 |
51 |
52 |
53 | 54 |

55 | Error. 56 |

57 |

58 | *intense crash sound* 59 |

60 | 61 |
62 | 						
63 | 							
{getErrorMessage()}
64 |
{getDebugInfo()}
65 |
66 |
67 |
68 |
69 |
70 | 71 | Frequent errors? Try resetting (It logs you out tho) 72 | 73 | 77 | Clear cookies 78 | 79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /components/Indicator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Indicator({ 4 | type, 5 | extended, 6 | separator, 7 | }: { 8 | type: "Practical" | "Theory" | "Lab"; 9 | extended?: boolean; 10 | separator?: boolean; 11 | }) { 12 | return separator ? ( 13 | 31 | ) : ( 32 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /components/Sidebar/Badges/DayOrder.tsx: -------------------------------------------------------------------------------- 1 | import { fetchCalendar } from "@/hooks/fetchCalendar"; 2 | import React from "react"; 3 | 4 | export default async function DayOrder({ 5 | mini, 6 | ...props 7 | }: { 8 | mini?: boolean; 9 | className?: string; 10 | }) { 11 | const { today } = await fetchCalendar(); 12 | 13 | const day = today?.dayOrder; 14 | 15 | return ( 16 |
24 | {day?.includes("-") ? ( 25 | 34 | ) : ( 35 | 39 | {mini ? "" : "Day Order: "} 40 | {day} 41 | 42 | )} 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /components/Sidebar/Buttons/InstallButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | import { GrInstallOption } from "react-icons/gr"; 3 | import { usePwa } from "@dotmind/react-use-pwa"; 4 | import { useCallback } from "react"; 5 | 6 | export default forwardRef( 7 | function InstallButton({ anchor }, ref) { 8 | const { installPrompt, isInstalled, isStandalone, isOffline, canInstall } = 9 | usePwa(); 10 | 11 | const handleInstallPrompt = useCallback(() => { 12 | if (canInstall) { 13 | installPrompt(); 14 | } 15 | }, [canInstall, installPrompt]); 16 | 17 | if (isOffline) return; 18 | 19 | return ( 20 | (!isInstalled || !isStandalone) && 21 | canInstall && ( 22 | 38 | ) 39 | ); 40 | }, 41 | ); 42 | -------------------------------------------------------------------------------- /components/Sidebar/Buttons/MiniButtons.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ComponentProps, type ReactNode } from "react"; 2 | import type NextLink from "next/link"; 3 | import { Link } from "next-view-transitions"; 4 | 5 | export default function MiniButtons({ 6 | icon, 7 | ...props 8 | }: ComponentProps & { 9 | icon: ReactNode; 10 | }) { 11 | return ( 12 | 18 | {icon} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/Sidebar/Buttons/OpenButton.tsx: -------------------------------------------------------------------------------- 1 | import { FaAnglesLeft, FaAnglesRight } from "react-icons/fa6"; 2 | 3 | export default function OpenButton({ 4 | mobile, 5 | isOpen, 6 | onClick, 7 | anchor, 8 | }: { 9 | mobile?: boolean; 10 | isOpen: boolean; 11 | onClick: () => void; 12 | anchor?: boolean; 13 | }) { 14 | return mobile ? ( 15 | 27 | ) : ( 28 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/Sidebar/Popup.tsx: -------------------------------------------------------------------------------- 1 | // biome-ignore lint/a11y/useSemanticElements: 2 | "use client"; 3 | 4 | import Link from "next/link"; 5 | import { FaBookOpen } from "react-icons/fa"; 6 | import { useState, useEffect } from "react"; 7 | 8 | export default function Popup() { 9 | const [opened, setOpened] = useState(false); 10 | 11 | useEffect(() => { 12 | const isAcknowledged = localStorage.getItem("popup"); 13 | if (!isAcknowledged) setOpened(true); 14 | }, []); 15 | 16 | function clicked() { 17 | setOpened(false); 18 | localStorage.setItem("popup", "true"); 19 | } 20 | 21 | return ( 22 | opened && ( 23 |
29 |
{}} 33 | onClick={(e) => { 34 | e.stopPropagation(); 35 | }} 36 | className="relative md:w-[30%] w-[25%] min-w-[350px] max-w-[400px] text-light-color dark:text-dark-color cursor-default items-center justify-center flex flex-col rounded-[42px] bg-light-background-normal p-4 pb-20 shadow-lg dark:bg-dark-background-normal" 37 | > 38 |
39 | 43 |

44 | ClassPro 45 |

46 | {/*

47 | AcademiaPro is now ClassPro 48 |

*/} 49 |
50 |

51 | This free,{" "} 52 | 56 | open-source 57 | {" "} 58 | platform is entirely developed by students, operates independently, 59 | without any direct connection or endorsement from the university. 60 |

61 | 68 |
69 |
70 | ) 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /components/Sidebar/ProfileBadge.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { useState, useEffect } from "react"; 4 | import { FaUser } from "react-icons/fa"; 5 | import { createPortal } from "react-dom"; 6 | import { useRouter } from "next/navigation"; 7 | import { profileColor } from "@/utils/ProfileColor"; 8 | import Image from "next/image"; 9 | import dynamic from "next/dynamic"; 10 | import type { UserInfo } from "@/types/User"; 11 | import { useTheme } from "@/provider/ThemeProvider"; 12 | import { FaCrown } from "react-icons/fa6"; 13 | import { Link } from "next-view-transitions"; 14 | 15 | const UserDialog = dynamic( 16 | () => import("./UserDialog").then((a) => a.default), 17 | { ssr: false }, 18 | ); 19 | 20 | export default function ProfileBadge({ 21 | className, 22 | user, 23 | subscribed, 24 | }: { className?: string; user: UserInfo; subscribed: boolean }) { 25 | const router = useRouter(); 26 | const { theme } = useTheme(); 27 | 28 | const [isDialogOpen, setIsDialogOpen] = useState(false); 29 | const [dialogRoot, setDialogRoot] = useState(null); 30 | 31 | useEffect(() => { 32 | setDialogRoot(document.getElementById("dialog-root")); 33 | 34 | return () => { 35 | setDialogRoot(null); 36 | }; 37 | }, []); 38 | 39 | const openDialog = () => setIsDialogOpen(true); 40 | const closeDialog = () => setIsDialogOpen(false); 41 | const logout = () => { 42 | const log = confirm("Are you sure to logout?"); 43 | if (log) { 44 | router.push("/auth/logout"); 45 | } else return; 46 | }; 47 | 48 | return ( 49 |
50 | {!subscribed && 51 | Support us 52 | } 53 |
{ 59 | if (key.key === "p") openDialog(); 60 | }} 61 | onClick={openDialog} 62 | className={`${className} flex w-full items-center space-x-3 rounded-full bg-light-background-dark p-1 transition duration-150 lg:w-[82%] dark:bg-dark-background-darker`} 63 | > 64 |
70 | 71 | {subscribed ? ( 72 | 73 | ) : theme === "Batman" ? ( 74 | Batman 82 | ) : ( 83 | 84 | )} 85 | 86 |
87 | 88 | {user?.name?.toLowerCase()} 89 | 90 |
91 | {dialogRoot && 92 | createPortal( 93 | , 99 | dialogRoot, 100 | )} 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /components/Sidebar/SidebarLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ComponentProps } from "react"; 2 | import { Link as Alink } from "next-view-transitions"; 3 | import type NextLink from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | export default function SidebarLink({ 7 | children, 8 | ...props 9 | }: ComponentProps) { 10 | const pathname = usePathname(); 11 | return ( 12 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/Sidebar/UserDialog.tsx: -------------------------------------------------------------------------------- 1 | import type { UserInfo } from "@/types/User"; 2 | import { FaXmark } from "react-icons/fa6"; 3 | 4 | interface UserDialogProps { 5 | user: UserInfo; 6 | isOpen: boolean; 7 | onClose: () => void; 8 | logout: () => void; 9 | } 10 | 11 | export default function UserDialog({ 12 | user, 13 | isOpen, 14 | onClose, 15 | logout, 16 | }: UserDialogProps) { 17 | if (!isOpen) return null; 18 | 19 | return ( 20 |
{ 25 | if (e.key === "Escape") onClose(); 26 | }} 27 | onClick={onClose} 28 | > 29 |
{}} 34 | onClick={(e) => { 35 | e.stopPropagation(); 36 | }} 37 | className="w-96 cursor-default rounded-[36px] bg-light-background-normal p-4 text-white shadow-lg dark:bg-dark-background-normal" 38 | > 39 |
40 |
41 |

42 | {user?.name?.toLowerCase()} 43 |

44 |
45 | 54 |
55 |
56 |

57 | {user?.regNumber} 58 |

59 |
60 |
61 |

62 | Year: 63 |

64 |

{user?.year}

65 |
66 |
67 |

68 | Semester: 69 |

70 |

{user?.semester}

71 |
72 | {/*
73 |

74 | Classroom: 75 |

76 |

{user?.classRoom}

77 |
*/} 78 |
79 |

80 | Section: 81 |

82 |

{user?.section}

83 |
84 |
85 |

86 | Batch: 87 |

88 |

{user?.batch}

89 |
90 |
91 |

92 | Program: 93 |

94 |

{user?.program}

95 |
96 |
97 |
98 |

99 | Department: 100 |

101 |

{user?.department}

102 |
103 |
104 | 111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /components/States/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RiLoader3Fill } from "react-icons/ri"; 3 | 4 | export default function Loading({ size }: { size?: "xl" | "3xl" | "max" }) { 5 | return ( 6 |
7 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const buttonBaseClasses = 6 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:focus-visible:ring-neutral-300"; 7 | 8 | export const buttonVariantClasses = { 9 | default: 10 | "bg-neutral-900 text-neutral-50 shadow-sm hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90", 11 | destructive: 12 | "bg-red-500 text-neutral-50 shadow-xs hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90", 13 | outline: 14 | "border border-neutral-200 bg-white shadow-xs hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", 15 | secondary: 16 | "bg-neutral-100 text-neutral-900 shadow-xs hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80", 17 | ghost: "hover:bg-neutral-100 dark:hover:bg-neutral-800", 18 | link: "text-neutral-900 underline-offset-4 hover:underline dark:text-neutral-50", 19 | }; 20 | 21 | const buttonSizeClasses = { 22 | default: "h-9 px-4 py-2", 23 | sm: "h-8 rounded-md px-3 text-xs", 24 | lg: "h-10 rounded-md px-8", 25 | icon: "h-9 w-9", 26 | }; 27 | 28 | export interface ButtonProps 29 | extends React.ButtonHTMLAttributes { 30 | variant?: keyof typeof buttonVariantClasses; 31 | size?: keyof typeof buttonSizeClasses; 32 | asChild?: boolean; 33 | } 34 | 35 | const Button = React.forwardRef( 36 | ( 37 | { 38 | className, 39 | variant = "default", 40 | size = "default", 41 | asChild = false, 42 | ...props 43 | }, 44 | ref, 45 | ) => { 46 | const Comp = asChild ? Slot : "button"; 47 | return ( 48 | 58 | ); 59 | }, 60 | ); 61 | Button.displayName = "Button"; 62 | 63 | export { Button }; 64 | -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { LuChevronLeft, LuChevronRight } from "react-icons/lu"; 5 | import { DayPicker } from "react-day-picker"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | export type CalendarProps = React.ComponentProps; 10 | 11 | function Calendar({ 12 | className, 13 | classNames, 14 | showOutsideDays = true, 15 | ...props 16 | }: CalendarProps) { 17 | return ( 18 | .range_end)]:rounded-r-md [&:has(>.range_start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 44 | : "[&:has([aria-selected])]:rounded-md", 45 | ), 46 | day: cn( 47 | "h-8 w-8 p-0 flex items-center justify-center font-normal aria-selected:opacity-100", 48 | ), 49 | range_start: "range_start", 50 | range_end: "range_end", 51 | selected: 52 | "bg-light-accent scale-110 z-2 text-white rounded-lg focus:bg-light-accent focus:text-light-accent dark:bg-dark-accent dark:text-black dark:focus:bg-dark-accent dark:focus:text-black", 53 | today: 54 | "bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50", 55 | outside: 56 | "outside text-neutral-500 aria-selected:bg-neutral-100/50 aria-selected:text-neutral-500 dark:text-neutral-400 dark:aria-selected:bg-neutral-800/50 dark:aria-selected:text-neutral-400", 57 | disabled: "text-neutral-500 opacity-50 dark:text-neutral-400", 58 | range_middle: 59 | "aria-selected:bg-neutral-100 z-1! scale-100! rounded-none! aria-selected:text-neutral-900 dark:aria-selected:bg-neutral-800 dark:aria-selected:text-neutral-50", 60 | hidden: "invisible", 61 | ...classNames, 62 | }} 63 | components={{ 64 | Chevron: (props) => { 65 | if (props.orientation === "left") { 66 | return ( 67 | 70 | ); 71 | } 72 | return ( 73 | 76 | ); 77 | }, 78 | }} 79 | {...props} 80 | /> 81 | ); 82 | } 83 | Calendar.displayName = "Calendar"; 84 | 85 | export { Calendar }; 86 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = "Input"; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor; 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )); 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 34 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )); 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 47 | 48 | export { ScrollArea, ScrollBar }; 49 | -------------------------------------------------------------------------------- /components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SliderPrimitive from "@radix-ui/react-slider"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | const Slider = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 20 | 21 |
22 | {[...Array(6)].map((_, index) => 23 | index === 0 || index === 5 ? ( 24 |
25 | ) : ( 26 |
30 | ), 31 | )} 32 |
33 | 34 | 35 | 36 | )); 37 | Slider.displayName = SliderPrimitive.Root.displayName; 38 | 39 | export { Slider }; 40 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | # Comments are provided throughout this file to help you get started. 2 | # If you need more help, visit the Docker Compose reference guide at 3 | # https://docs.docker.com/go/compose-spec-reference/ 4 | 5 | # Here the instructions define your application as a service called "server". 6 | # This service is built from the Dockerfile in the current directory. 7 | # You can add other services your application may depend on here, such as a 8 | # database or a cache. For examples, see the Awesome Compose repository: 9 | # https://github.com/docker/awesome-compose 10 | services: 11 | server: 12 | build: 13 | context: . 14 | environment: 15 | NODE_ENV: production 16 | ports: 17 | - 243:243 18 | 19 | # The commented out section below is an example of how to define a PostgreSQL 20 | # database that your application can use. `depends_on` tells Docker Compose to 21 | # start the database before your application. The `db-data` volume persists the 22 | # database data between container restarts. The `db-password` secret is used 23 | # to set the database password. You must create `db/password.txt` and add 24 | # a password of your choosing to it before running `docker-compose up`. 25 | # depends_on: 26 | # db: 27 | # condition: service_healthy 28 | # db: 29 | # image: postgres 30 | # restart: always 31 | # user: postgres 32 | # secrets: 33 | # - db-password 34 | # volumes: 35 | # - db-data:/var/lib/postgresql/data 36 | # environment: 37 | # - POSTGRES_DB=example 38 | # - POSTGRES_PASSWORD_FILE=/run/secrets/db-password 39 | # expose: 40 | # - 5432 41 | # healthcheck: 42 | # test: [ "CMD", "pg_isready" ] 43 | # interval: 10s 44 | # timeout: 5s 45 | # retries: 5 46 | # volumes: 47 | # db-data: 48 | # secrets: 49 | # db-password: 50 | # file: db/password.txt 51 | 52 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc"; 2 | 3 | const compat = new FlatCompat({ 4 | // import.meta.dirname is available after Node.js v20.11.0 5 | baseDirectory: import.meta.dirname, 6 | }); 7 | 8 | const eslintConfig = [ 9 | ...compat.config({ 10 | extends: ["next"], 11 | rules: { 12 | "react/no-unescaped-entities": "off", 13 | "@next/next/no-page-custom-font": "off", 14 | "@typescript-eslint/no-explicit-any": "off", 15 | "no-console": "warn", 16 | }, 17 | }), 18 | ]; 19 | 20 | export default eslintConfig; 21 | -------------------------------------------------------------------------------- /hooks/fetchCalendar.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import type { CalendarResponse } from "@/types/Calendar"; 3 | import { token } from "@/utils/Tokenize"; 4 | import rotateUrl from "@/utils/URL"; 5 | import { cookies } from "next/headers"; 6 | import { redirect } from "next/navigation"; 7 | import { cache } from "react"; 8 | 9 | let cachedData: CalendarResponse | null = null; 10 | let lastFetchTime: number | null = null; 11 | const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes 12 | 13 | export default async function fetchCal() { 14 | const now = Date.now(); 15 | if (cachedData && lastFetchTime && now - lastFetchTime < CACHE_DURATION) { 16 | return cachedData; 17 | } 18 | 19 | const cookie = (await cookies()).get("key"); 20 | const a = await fetch(`${rotateUrl()}/calendar`, { 21 | method: "GET", 22 | headers: { 23 | "Content-Type": "application/json", 24 | "X-CSRF-Token": cookie?.value ?? "", 25 | // biome-ignore lint/style/useNamingConvention: 26 | Authorization: `Bearer ${token()}`, 27 | }, 28 | }); 29 | 30 | const json: CalendarResponse = await a.json(); 31 | 32 | if (json.ratelimit) redirect("/ratelimit"); 33 | 34 | 35 | cachedData = json; 36 | lastFetchTime = now; 37 | 38 | return json; 39 | } 40 | 41 | export const fetchCalendar = cache(fetchCal); 42 | -------------------------------------------------------------------------------- /hooks/fetchFiles.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { cache } from "react"; 3 | import { files } from "@/library/files"; 4 | 5 | 6 | async function fetchFiles() { 7 | return files; 8 | } 9 | 10 | export const fetchFileArray = cache(async () => { 11 | return await fetchFiles(); 12 | }); 13 | -------------------------------------------------------------------------------- /hooks/fetchResources.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { cache } from "react"; 3 | import { resourcesFiles } from "@/library/resources"; 4 | 5 | 6 | async function fetchResources() { 7 | return resourcesFiles; 8 | } 9 | 10 | export const fetchResourcesArray = cache(async () => { 11 | return await fetchResources(); 12 | }); 13 | -------------------------------------------------------------------------------- /hooks/fetchUserData.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { cache } from "react"; 3 | import type { AllResponse } from "@/types/Response"; 4 | import { token } from "@/utils/Tokenize"; 5 | import rotateUrl from "@/utils/URL"; 6 | import { cookies } from "next/headers"; 7 | import { redirect } from "next/navigation"; 8 | 9 | const dataCache: Map = 10 | new Map(); 11 | 12 | async function fetchData(): Promise { 13 | const cookie = (await cookies()).get("key"); 14 | const userKey = `key-${cookie?.value}`; 15 | 16 | const cachedData = dataCache.get(userKey); 17 | const now = Date.now(); 18 | if (cachedData && now - cachedData.timestamp < 2 * 60 * 1000) { 19 | return cachedData.data; 20 | } 21 | 22 | const controller = new AbortController(); 23 | const timeoutId = setTimeout(() => { 24 | controller.abort(); 25 | redirect("/sleeping"); 26 | }, 10000); 27 | 28 | try { 29 | const url = rotateUrl() 30 | const response = await fetch(`${url}/get`, { 31 | method: "GET", 32 | cache: "force-cache", 33 | next: { 34 | revalidate: 60, 35 | }, 36 | headers: { 37 | "Content-Type": "application/json", 38 | "X-CSRF-Token": cookie?.value ?? "", 39 | Authorization: `Bearer ${token()}`, 40 | }, 41 | signal: controller.signal, 42 | }); 43 | 44 | clearTimeout(timeoutId); 45 | 46 | let json: AllResponse; 47 | try { 48 | const text = await response.text(); 49 | json = JSON.parse(text); 50 | } catch (e) { 51 | throw e; 52 | } 53 | 54 | if (json.tokenInvalid) redirect("/invalid"); 55 | if (json.ratelimit) redirect("/ratelimit"); 56 | 57 | dataCache.set(userKey, { 58 | data: json, 59 | timestamp: now, 60 | }); 61 | 62 | if (dataCache.size > 100) { 63 | const oldEntries = Array.from(dataCache.entries()).filter( 64 | ([_, value]) => now - value.timestamp > 10 * 60 * 1000, 65 | ); 66 | for (const [key] of oldEntries) { 67 | dataCache.delete(key); 68 | } 69 | } 70 | 71 | return json; 72 | } catch (error) { 73 | if ((error as Error).name === "AbortError") { 74 | redirect("/sleeping"); 75 | } 76 | throw error; 77 | } 78 | } 79 | 80 | export const fetchUserData = cache(async () => { 81 | return await fetchData(); 82 | }); 83 | -------------------------------------------------------------------------------- /hooks/useGesture.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | interface UseGesturesProps { 4 | onSwipeLeft: () => void; 5 | onSwipeRight: () => void; 6 | } 7 | 8 | export function useGestures({ onSwipeLeft, onSwipeRight }: UseGesturesProps) { 9 | useEffect(() => { 10 | let touchstartX = 0; 11 | let touchendX = 0; 12 | 13 | function handleGesture() { 14 | const screenWidth = window.innerWidth; 15 | const swipeThreshold = screenWidth / 3; 16 | 17 | if (touchendX < touchstartX - swipeThreshold) { 18 | onSwipeLeft(); 19 | } 20 | 21 | if (touchendX > touchstartX + swipeThreshold) { 22 | onSwipeRight(); 23 | } 24 | } 25 | 26 | function startTouch(event: TouchEvent) { 27 | touchstartX = event.changedTouches[0].screenX; 28 | } 29 | 30 | function stopTouch(event: TouchEvent) { 31 | touchendX = event.changedTouches[0].screenX; 32 | handleGesture(); 33 | } 34 | 35 | window.addEventListener("touchstart", startTouch, false); 36 | window.addEventListener("touchend", stopTouch, false); 37 | 38 | return () => { 39 | window.removeEventListener("touchstart", startTouch); 40 | window.removeEventListener("touchend", stopTouch); 41 | }; 42 | }, [onSwipeLeft, onSwipeRight]); 43 | } 44 | -------------------------------------------------------------------------------- /hooks/useSearch.tsx: -------------------------------------------------------------------------------- 1 | import { priorityUrl, urls } from "@/misc/links"; 2 | 3 | export default function useSearch({ searchQuery }: { searchQuery: string }) { 4 | const priority = priorityUrl.filter((url) => { 5 | if (!searchQuery) return true; 6 | return ( 7 | includesSubstring(url.site, searchQuery) || 8 | includesSubstring(url.description, searchQuery) 9 | ); 10 | }); 11 | 12 | const officials = urls.filter((url) => { 13 | if (!searchQuery) return url.type === "official"; 14 | if (url.type !== "official") return false; 15 | return ( 16 | includesSubstring(url.site, searchQuery) || 17 | includesSubstring(url.description, searchQuery) 18 | ); 19 | }); 20 | 21 | const others = urls.filter((url) => { 22 | if (!searchQuery) return url.type === "unofficial"; 23 | if (url.type !== "unofficial") return false; 24 | return ( 25 | includesSubstring(url.site, searchQuery) || 26 | includesSubstring(url.description, searchQuery) 27 | ); 28 | }); 29 | 30 | return { priority, officials, others }; 31 | } 32 | function includesSubstring(str: string, query: string) { 33 | return str.toLowerCase()?.includes(query.toLowerCase()); 34 | } 35 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | 4 | 5 | const protectedRoutes = [ 6 | "/academia", 7 | "/academia/courses", 8 | "/academia/library", 9 | "/academia/faculties", 10 | ]; 11 | const home = ["/"]; 12 | const MAINTENANCE = false; 13 | 14 | const isAuthenticated = (request: NextRequest): boolean => { 15 | const token = request.cookies.get("key"); 16 | return !!token?.value; 17 | }; 18 | 19 | export function middleware(request: NextRequest) { 20 | const { pathname } = request.nextUrl; 21 | 22 | if (MAINTENANCE && pathname !== "/maintenance") { 23 | return NextResponse.redirect(new URL("/maintenance", request.url)); 24 | } 25 | if (!MAINTENANCE && pathname === "/maintenance") { 26 | return NextResponse.redirect(new URL("/", request.url)); 27 | } 28 | 29 | if (isAuthenticated(request) && home.includes(pathname)) { 30 | return NextResponse.redirect(new URL("/academia", request.url)); 31 | } 32 | if (!isAuthenticated(request) && home.includes(pathname)) { 33 | return NextResponse.redirect(new URL("/home", request.url)); 34 | } 35 | if (protectedRoutes.includes(pathname) && !isAuthenticated(request)) { 36 | return NextResponse.redirect(new URL("/auth/login", request.url)); 37 | } 38 | 39 | return NextResponse.next(); 40 | } 41 | 42 | export const config = { 43 | matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], 44 | }; 45 | -------------------------------------------------------------------------------- /misc/encode.ts: -------------------------------------------------------------------------------- 1 | const ALPHABET = 2 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 3 | 4 | export function encodeString(str: string): string { 5 | const buffer = Buffer.from(str, "utf-8"); 6 | let num = BigInt(`0x${buffer.toString("hex")}`); 7 | let encoded = ""; 8 | 9 | while (num > 0) { 10 | encoded = ALPHABET[Number(num % BigInt(62))] + encoded; 11 | num = num / BigInt(62); 12 | } 13 | 14 | return encoded.padStart(11, "0"); 15 | } 16 | 17 | export function decodeString(encoded: string): string { 18 | let num = BigInt(0); 19 | 20 | for (let i = 0; i < encoded.length; i++) { 21 | num = num * BigInt(62) + BigInt(ALPHABET.indexOf(encoded[i])); 22 | } 23 | 24 | const hex = num.toString(16); 25 | const buffer = Buffer.from( 26 | hex.padStart(2 * Math.ceil(hex.length / 2), "0"), 27 | "hex", 28 | ); 29 | 30 | return buffer.toString("utf-8"); 31 | } 32 | -------------------------------------------------------------------------------- /misc/users.ts: -------------------------------------------------------------------------------- 1 | export const elevatedUsers = [ 2 | "RA2311026010101", 3 | "RA2311026010126", 4 | "RA2311026010086", 5 | "RA2311026010227", 6 | "RA2311032010010", 7 | ]; 8 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "bun run build" 3 | publish = ".next" 4 | 5 | [build.environment] 6 | NODE_VERSION = "20" 7 | BUN_VERSION = "1.1.0" 8 | 9 | [dev] 10 | command = "bun run dev" 11 | port = 3000 12 | targetPort = 3000 13 | 14 | [[redirects]] 15 | from = "/elab" 16 | to = "https://better-lab.vercel.app" 17 | status = 301 18 | force = true 19 | 20 | [[redirects]] 21 | from = "/papers" 22 | to = "https://docu-pro.vercel.app" 23 | status = 301 24 | force = true 25 | 26 | [[redirects]] 27 | from = "/map" 28 | to = "https://d23qowwaqkh3fj.cloudfront.net/wp-content/uploads/2022/06/srmist-ktr-campus-layout.jpg" 29 | status = 301 30 | force = true 31 | 32 | [[redirects]] 33 | from = "/ssr" 34 | to = "https://academia.srmist.edu.in/#Form:Student_Service_Requests_SSR" 35 | status = 301 36 | force = true 37 | 38 | [[redirects]] 39 | from = "/leave" 40 | to = "http://10.1.105.62/srmleaveapp" 41 | status = 301 42 | force = true 43 | 44 | [[redirects]] 45 | from = "/github" 46 | to = "https://github.com/rahuletto/classpro" 47 | status = 301 48 | force = true 49 | 50 | [[redirects]] 51 | from = "/instagram" 52 | to = "https://www.instagram.com/srm_academiapro/" 53 | status = 301 54 | force = true 55 | 56 | [[redirects]] 57 | from = "/whatsapp" 58 | to = "https://chat.whatsapp.com/FLvHgGU87OLEOeQIqJEhto" 59 | status = 301 60 | force = true 61 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | import withSerwistInit from "@serwist/next"; 3 | import path from "path"; 4 | 5 | const withSerwist = withSerwistInit({ 6 | swSrc: "app/sw.ts", 7 | swDest: "public/sw.js", 8 | disable: process.env.NODE_ENV === "development", 9 | cacheOnNavigation: true, 10 | dontCacheBustURLsMatching: 11 | /^dist\/static\/([a-zA-Z0-9]+)\.([a-z0-9]+)\.(css|js)$/, 12 | exclude: [ 13 | /\.map$/, 14 | /asset-manifest\.json$/, 15 | /LICENSE/, 16 | /README/, 17 | /robots.txt/, 18 | ], 19 | reloadOnOnline: true, 20 | }); 21 | 22 | const nextConfig: NextConfig = { 23 | poweredByHeader: false, 24 | compress: true, 25 | eslint: { ignoreDuringBuilds: true }, 26 | webpack(config) { 27 | config.resolve.alias['@radix-ui/react-use-effect-event'] = 28 | path.resolve(__dirname, 'stubs/use-effect-event.js'); 29 | return config; 30 | }, 31 | experimental: { 32 | reactCompiler: true, 33 | nextScriptWorkers: true, 34 | viewTransition: true, 35 | }, 36 | }; 37 | export default withSerwist(nextConfig); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classpro", 3 | "version": "3.0.0", 4 | "private": true, 5 | "packageManager": "pnpm@9.15.4", 6 | "scripts": { 7 | "dev": "next dev --port 0243", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@dotmind/react-use-pwa": "^1.0.4", 14 | "@radix-ui/react-popover": "^1.1.5", 15 | "@radix-ui/react-scroll-area": "^1.2.2", 16 | "@radix-ui/react-slider": "^1.2.2", 17 | "@radix-ui/react-slot": "^1.1.1", 18 | "@serwist/next": "^9.0.11", 19 | "@supabase/supabase-js": "^2.48.1", 20 | "@vercel/analytics": "^1.4.1", 21 | "@vercel/speed-insights": "^1.1.0", 22 | "clsx": "^2.1.1", 23 | "crypto-js": "^4.2.0", 24 | "date-fns": "^4.1.0", 25 | "fuse.js": "^7.0.0", 26 | "geist": "^1.3.1", 27 | "next": "^15.3.1", 28 | "next-view-transitions": "^0.3.4", 29 | "react": "^19.0.0", 30 | "react-colorful": "^5.6.1", 31 | "react-day-picker": "^9.5.0", 32 | "react-dom": "^19.0.0", 33 | "react-icons": "^5.4.0", 34 | "tailwind-merge": "^2.6.0" 35 | }, 36 | "devDependencies": { 37 | "@eslint/eslintrc": "^3.2.0", 38 | "@tailwindcss/postcss": "^4.0.0", 39 | "@types/crypto-js": "^4.2.2", 40 | "@builder.io/partytown": "^0.10.3", 41 | "@types/node": "^22.10.10", 42 | "@types/react": "^19.0.8", 43 | "@types/react-dom": "^19.0.3", 44 | "all-contributors-cli": "^6.26.1", 45 | "babel-plugin-react-compiler": "^19.0.0-beta-27714ef-20250124", 46 | "eslint": "^9.19.0", 47 | "eslint-config-next": "15.1.6", 48 | "postcss": "^8.5.1", 49 | "serwist": "^9.0.11", 50 | "tailwindcss": "^4.0.0", 51 | "typescript": "^5.7.3" 52 | } 53 | } -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /provider/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Themes } from "@/misc/theme"; 3 | import React, { 4 | createContext, 5 | type ReactNode, 6 | useContext, 7 | useEffect, 8 | useState, 9 | } from "react"; 10 | 11 | const initialTheme = { 12 | theme: "dark", 13 | setTheme: (theme: string) => {}, 14 | }; 15 | 16 | const ThemeContext = createContext(initialTheme); 17 | export function useTheme() { 18 | const { theme, setTheme } = useContext(ThemeContext); 19 | return { theme, setTheme }; 20 | } 21 | 22 | export function ThemeProvider({ children }: { children: ReactNode }) { 23 | const [theme, setTheme] = useState(""); 24 | 25 | useEffect(() => { 26 | if (theme === "") return; 27 | const properties = Themes.find((t) => t.title === theme); 28 | 29 | if (properties?.properties.metacolor) { 30 | document 31 | .querySelector('meta[name="theme-color"]') 32 | ?.setAttribute("content", properties.properties.metacolor); 33 | } 34 | 35 | const root = window.document.documentElement; 36 | localStorage.setItem("theme", theme); 37 | if (theme === "Batman") { 38 | document.documentElement.style.filter = "grayscale(100%)"; 39 | } else document.documentElement.style.filter = "none"; 40 | 41 | if (properties) { 42 | for (const [key, value] of Object.entries(properties.properties)) { 43 | root.style.setProperty(`--${key}`, value.toString()); 44 | } 45 | } 46 | 47 | if (properties?.mode === "dark") { 48 | document.documentElement.classList.add("dark"); 49 | document.documentElement.classList.remove("light"); 50 | } else if (properties?.mode === "light") { 51 | document.documentElement.classList.add("light"); 52 | document.documentElement.classList.remove("dark"); 53 | } 54 | 55 | if (properties?.mono) { 56 | document.documentElement.classList.add("mono"); 57 | } else { 58 | document.documentElement.classList.remove("mono"); 59 | } 60 | 61 | }, [theme]); 62 | 63 | useEffect(() => { 64 | const localTheme = localStorage.getItem("theme"); 65 | if (localTheme) { 66 | setTheme(localTheme); 67 | } 68 | }, []); 69 | 70 | return ( 71 | 72 | {children} 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /public/fonts/Geist.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/fonts/Geist.ttf -------------------------------------------------------------------------------- /public/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/icons/apple-icon.png -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/icons/favicon.ico -------------------------------------------------------------------------------- /public/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/icons/icon.png -------------------------------------------------------------------------------- /public/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/icons/maskable_icon_x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/icons/maskable_icon_x192.png -------------------------------------------------------------------------------- /public/icons/maskable_icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/icons/maskable_icon_x512.png -------------------------------------------------------------------------------- /public/images/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/images/database.png -------------------------------------------------------------------------------- /public/images/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/images/og.png -------------------------------------------------------------------------------- /public/library/resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/library/resources.png -------------------------------------------------------------------------------- /public/library/sem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/library/sem.png -------------------------------------------------------------------------------- /public/screenshots/phone/academia.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/phone/academia.webp -------------------------------------------------------------------------------- /public/screenshots/phone/calendar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/phone/calendar.webp -------------------------------------------------------------------------------- /public/screenshots/phone/faculties.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/phone/faculties.webp -------------------------------------------------------------------------------- /public/screenshots/phone/predict.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/phone/predict.webp -------------------------------------------------------------------------------- /public/screenshots/wide/academia.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/wide/academia.webp -------------------------------------------------------------------------------- /public/screenshots/wide/calendar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/wide/calendar.jpeg -------------------------------------------------------------------------------- /public/screenshots/wide/calendar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/wide/calendar.webp -------------------------------------------------------------------------------- /public/screenshots/wide/faculties.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/wide/faculties.webp -------------------------------------------------------------------------------- /public/screenshots/wide/links.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/wide/links.webp -------------------------------------------------------------------------------- /public/screenshots/wide/predict.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rahuletto/ClassPro/083885848b55022828cccbf71028550eb0bcc14d/public/screenshots/wide/predict.webp -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /api/ 3 | Allow: / -------------------------------------------------------------------------------- /stubs/use-effect-event.js: -------------------------------------------------------------------------------- 1 | // Polyfill for useEffectEvent 2 | import * as React from 'react'; 3 | 4 | export function useEffectEvent(handler) { 5 | const handlerRef = React.useRef(handler); 6 | React.useEffect(() => { 7 | handlerRef.current = handler; 8 | }, [handler]); 9 | return React.useCallback((...args) => { 10 | return handlerRef.current(...args); 11 | }, []); 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext", "webworker"], 5 | "types": ["@serwist/next/typings"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts", 31 | "eslint.config.mjs" 32 | , "stubs/use-effect-event.js" ], 33 | "exclude": ["node_modules", "public/sw.js"] 34 | } 35 | -------------------------------------------------------------------------------- /types/Attendance.ts: -------------------------------------------------------------------------------- 1 | import type { ProscrapeError } from "./Error"; 2 | 3 | export type AttendanceResponse = Attendance & ProscrapeError; 4 | 5 | export interface Attendance { 6 | attendance: AttendanceCourse[]; 7 | regNumber: string; 8 | status: number; 9 | } 10 | 11 | export interface AttendanceCourse { 12 | attendancePercentage: string; 13 | category: "Practical" | "Theory"; 14 | courseCode: string; 15 | courseTitle: string; 16 | facultyName: string; 17 | hoursAbsent: string; 18 | hoursConducted: string; 19 | slot: string; 20 | } 21 | 22 | export interface DateRange { 23 | from: Date | null; 24 | to: Date | null; 25 | } 26 | 27 | export interface CategorizedDateRange { 28 | from: Date; 29 | to: Date; 30 | category: "Leave" | "OD"; 31 | } 32 | 33 | export interface CalendarMonth { 34 | month: string; 35 | days: { 36 | date: string; 37 | day: string; 38 | dayOrder: string; 39 | }[]; 40 | } 41 | 42 | export interface TimetableDay { 43 | day: number; 44 | table: (string | undefined)[]; 45 | } 46 | -------------------------------------------------------------------------------- /types/Calendar.ts: -------------------------------------------------------------------------------- 1 | import type { ProscrapeError } from "./Error"; 2 | 3 | export type CalendarResponse = SuccessCalendarResponse & ProscrapeError; 4 | export interface SuccessCalendarResponse { 5 | today: Day; 6 | tomorrow: Day; 7 | index: number; 8 | calendar: Calendar[]; 9 | requestedAt: number | null; 10 | } 11 | 12 | export interface Calendar { 13 | month: string; 14 | days: Day[]; 15 | } 16 | 17 | export interface Day { 18 | date: string; 19 | day: string; 20 | event?: string; 21 | dayOrder: string; 22 | } 23 | -------------------------------------------------------------------------------- /types/CalendarData.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface DayData { 3 | date: string; 4 | day: string; 5 | event: string | null; 6 | dayOrder: string; 7 | } 8 | 9 | export interface CalendarData { 10 | month: string; 11 | days: DayData[]; 12 | } 13 | -------------------------------------------------------------------------------- /types/Course.ts: -------------------------------------------------------------------------------- 1 | import type { ProscrapeError } from "./Error"; 2 | 3 | export type CourseResponse = SuccessCourseResponse & ProscrapeError; 4 | export interface SuccessCourseResponse { 5 | courses: Course[]; 6 | regNumber: string; 7 | } 8 | 9 | export interface Course { 10 | academicYear: string; 11 | category: string; 12 | code: string; 13 | courseCategory: string; 14 | credit: string; 15 | faculty: string; 16 | room: string; 17 | slot: string; 18 | slotType: string; 19 | title: string; 20 | type: string; 21 | } 22 | -------------------------------------------------------------------------------- /types/CoursePapers.ts: -------------------------------------------------------------------------------- 1 | export type CoursePapers = { 2 | name: string; 3 | code: string; 4 | urls: { 5 | semester: string; 6 | urls: { 7 | period: string; 8 | url: string; 9 | raw: string; 10 | }[]; 11 | }[]; 12 | }; -------------------------------------------------------------------------------- /types/DayOrder.ts: -------------------------------------------------------------------------------- 1 | import type { ProscrapeError } from "./Error"; 2 | 3 | export type DayOrderResponse = SuccessDayOrderResponse & ProscrapeError; 4 | export interface SuccessDayOrderResponse { 5 | date: string; 6 | dayOrder: string; 7 | requestedAt: number | null; 8 | } 9 | -------------------------------------------------------------------------------- /types/Error.ts: -------------------------------------------------------------------------------- 1 | export interface ProscrapeError { 2 | logout: boolean; 3 | error: boolean; 4 | ratelimit?: boolean; 5 | message: string; 6 | status: number; 7 | } 8 | -------------------------------------------------------------------------------- /types/Folders.ts: -------------------------------------------------------------------------------- 1 | export type Folders = Folder[] 2 | 3 | export interface Folder { 4 | name: string 5 | path: string 6 | encodedPath: string 7 | urls: Urls 8 | type: string 9 | children: Children[] 10 | } 11 | 12 | export interface Urls { 13 | rawUrl: string 14 | url: string 15 | } 16 | 17 | export interface Children { 18 | name: string 19 | path: string 20 | encodedPath: string 21 | urls: Urls2 22 | type: string 23 | children?: Children2[] 24 | } 25 | 26 | export interface Urls2 { 27 | rawUrl: string 28 | url: string 29 | } 30 | 31 | export interface Children2 { 32 | name: string 33 | path: string 34 | encodedPath: string 35 | urls: Urls3 36 | type: string 37 | children?: Children3[] 38 | } 39 | 40 | export interface Urls3 { 41 | rawUrl: string 42 | url: string 43 | } 44 | 45 | export interface Children3 { 46 | name: string 47 | path: string 48 | encodedPath: string 49 | urls: Urls4 50 | type: string 51 | } 52 | 53 | export interface Urls4 { 54 | rawUrl: string 55 | url: string 56 | } 57 | -------------------------------------------------------------------------------- /types/Grade.ts: -------------------------------------------------------------------------------- 1 | export const gradePoints: { [key: string]: number } = { 2 | // biome-ignore lint/style/useNamingConvention: 3 | O: 91, 4 | "A+": 81, 5 | // biome-ignore lint/style/useNamingConvention: 6 | A: 71, 7 | "B+": 61, 8 | // biome-ignore lint/style/useNamingConvention: 9 | B: 56, 10 | // biome-ignore lint/style/useNamingConvention: 11 | C: 50, 12 | }; 13 | 14 | export function getGrade(marks: number): string { 15 | if (marks >= 91) return "O"; 16 | if (marks >= 81) return "A+"; 17 | if (marks >= 71) return "A"; 18 | if (marks >= 61) return "B+"; 19 | if (marks >= 56) return "B"; 20 | if (marks >= 50) return "C"; 21 | return "F"; 22 | } 23 | -------------------------------------------------------------------------------- /types/Marks.ts: -------------------------------------------------------------------------------- 1 | import type { ProscrapeError } from "./Error"; 2 | 3 | export type MarksResponse = SuccessMarksResponse & ProscrapeError; 4 | export interface SuccessMarksResponse { 5 | marks: Mark[]; 6 | requestedAt: number | null; 7 | } 8 | 9 | export interface Mark { 10 | courseName: string; 11 | courseCode: string; 12 | courseType: string; 13 | overall: Marks; 14 | testPerformance: TestPerformance[]; 15 | } 16 | 17 | export interface TestPerformance { 18 | test: string; 19 | marks: Marks; 20 | } 21 | 22 | export interface Marks { 23 | scored?: string; 24 | total: string; 25 | } 26 | -------------------------------------------------------------------------------- /types/Response.ts: -------------------------------------------------------------------------------- 1 | import type { Attendance } from "./Attendance"; 2 | import type { SuccessCourseResponse } from "./Course"; 3 | import type { Mark } from "./Marks"; 4 | import type { Timetable } from "./Timetable"; 5 | import type { UserInfo } from "./User"; 6 | 7 | export interface AllResponse { 8 | attendance: Attendance; 9 | courses: SuccessCourseResponse; 10 | lastUpdated: number; 11 | marks: Marks; 12 | ophour?: string; 13 | subscribed?: boolean; 14 | subscribedSince?: number; 15 | regNumber: string; 16 | timetable: Timetable; 17 | token: string; 18 | user: UserInfo; 19 | tokenInvalid?: boolean; 20 | ratelimit?: boolean; 21 | error?: string; 22 | status: number; 23 | } 24 | 25 | export interface Marks { 26 | marks: Mark[]; 27 | regNumber: string; 28 | status: number; 29 | } 30 | -------------------------------------------------------------------------------- /types/Timetable.ts: -------------------------------------------------------------------------------- 1 | import type { ProscrapeError } from "./Error"; 2 | 3 | export type TimeTableResponse = Timetable & ProscrapeError; 4 | export interface Timetable { 5 | batch: string; 6 | regNumber: string; 7 | schedule: Schedule[]; 8 | } 9 | 10 | export interface ScheduleSlot { 11 | code: string; 12 | name: string; 13 | slot: string; 14 | roomNo: string; 15 | courseType: "Theory" | "Practical"; 16 | online: boolean; 17 | isOptional?: boolean; 18 | } 19 | 20 | export interface Schedule { 21 | day: number; 22 | table: (ScheduleSlot | null)[]; 23 | } 24 | -------------------------------------------------------------------------------- /types/User.ts: -------------------------------------------------------------------------------- 1 | import type { ProscrapeError } from "./Error"; 2 | 3 | export type UserData = SuccessUserData & ProscrapeError; 4 | export type User = UserInfo & ProscrapeError; 5 | 6 | export interface SuccessUserData { 7 | user: UserInfo; 8 | requestedAt: number | null; 9 | } 10 | 11 | export interface UserInfo { 12 | combo: string; 13 | batch: string; 14 | department: string; 15 | mobile: string; 16 | name: string; 17 | program: string; 18 | regNumber: string; 19 | section: string; 20 | semester: number; 21 | year: number; 22 | } 23 | -------------------------------------------------------------------------------- /types/UserData.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | regNumber: string; 3 | year: number; 4 | name: string; 5 | batch: string; 6 | mobile: string; 7 | program: string; 8 | department: string; 9 | section: string; 10 | semester: string; 11 | classRoom: string; 12 | } 13 | -------------------------------------------------------------------------------- /utils/Cookies.ts: -------------------------------------------------------------------------------- 1 | export function setCookie(name:string, value: string, expirationMonths = 3) { 2 | if (typeof document === "undefined") return null; 3 | 4 | const exdate = new Date(); 5 | exdate.setMonth(exdate.getMonth() + expirationMonths); 6 | 7 | const encodedValue = encodeURIComponent(value); 8 | 9 | const cookieString = `${name}=${encodedValue}; expires=${exdate.toUTCString()}; path=/; ${ 10 | window.location.hostname === "localhost" ? "" : "Secure; " 11 | } SameSite=Lax`; 12 | 13 | document.cookie = cookieString; 14 | 15 | return true; 16 | } 17 | 18 | 19 | export function encode(str: string): string { 20 | let hash = 2166136261; 21 | for (let i = 0; i < str?.length; i++) { 22 | hash ^= str.charCodeAt(i); 23 | hash += 24 | (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); 25 | } 26 | return ( 27 | (hash >>> 0).toString() + 28 | (hash >>> 1).toString(16) + 29 | (hash >>> 2).toString(32) 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /utils/Database/index.ts: -------------------------------------------------------------------------------- 1 | import type { CalendarData } from "@/types/CalendarData"; 2 | import { supabase } from "./supabase"; 3 | import { encode } from "../Cookies"; 4 | 5 | interface DatabaseRecord { 6 | regNumber: string; 7 | user?: string; 8 | timetable?: string; 9 | course?: string; 10 | attendance?: string; 11 | marks?: string; 12 | lastUpdated?: number; 13 | token?: string; 14 | } 15 | 16 | class Database { 17 | async getEvents(): Promise { 18 | const { data, error } = await supabase 19 | .from("gocal") 20 | .select("*"); 21 | 22 | if (error) { 23 | console.error("Error fetching events:", error.message); 24 | throw new Error("Failed to fetch calendar events"); 25 | } 26 | 27 | const events = data as { 28 | date: string; 29 | day: string; 30 | event: string; 31 | order: string; 32 | }[]; 33 | const calendar: CalendarData[] = []; 34 | 35 | for (const event of events) { 36 | const [month, date] = event.date.split(" - "); 37 | const monthData = calendar?.find((cal) => cal.month === month); 38 | 39 | if (monthData) { 40 | monthData.days.push({ 41 | date, 42 | day: event.day, 43 | event: event.event, 44 | dayOrder: event.order, 45 | }); 46 | } else { 47 | calendar.push({ 48 | month, 49 | days: [ 50 | { 51 | date, 52 | day: event.day, 53 | event: event.event, 54 | dayOrder: event.order, 55 | }, 56 | ], 57 | }); 58 | } 59 | } 60 | 61 | return calendar; 62 | } 63 | 64 | // Method to get data by regNumber 65 | async getData(regNumber: string): Promise { 66 | try { 67 | const { data, error } = await supabase 68 | .from("goscrape") // Replace with your actual table name 69 | .select("*") 70 | .eq("regNumber", regNumber) 71 | .single(); // Use single() to return a single record 72 | 73 | if (error) { 74 | console.error(error); 75 | console.error(`Error fetching data: ${error.message}`); 76 | return null; 77 | } 78 | 79 | return data as DatabaseRecord; 80 | } catch (error: any) { 81 | console.error(`Supabase getData error: ${error.message}`); 82 | return null; 83 | } 84 | } 85 | 86 | async getSpecific( 87 | cookie: string, 88 | selector = "*", 89 | ): Promise { 90 | try { 91 | const { data, error } = await supabase 92 | .from("goscrape") // Replace with your actual table name 93 | .select(selector) 94 | .eq("token", encode(cookie)) 95 | .single(); // Use single() to return a single record 96 | 97 | if (error) { 98 | if (error.code === "PGRST116") return null; 99 | 100 | console.error(`Error fetching data: ${error.message}`); 101 | return null; 102 | } 103 | 104 | return data as unknown as DatabaseRecord; 105 | } catch (error: any) { 106 | console.error(`Supabase getData error: ${error.message}`); 107 | return null; 108 | } 109 | } 110 | 111 | async checkCookie(cookie: string): Promise { 112 | try { 113 | const { data, error } = await supabase 114 | .from("goscrape") // Replace with your actual table name 115 | .select("*") 116 | .eq("token", encode(cookie)) 117 | .single(); // Use single() to return a single record 118 | 119 | if (error) { 120 | if (error.code === "PGRST116") return null; 121 | 122 | console.error(`Error fetching data: ${error.message}`); 123 | return null; 124 | } 125 | 126 | return data as DatabaseRecord; 127 | } catch (error: any) { 128 | console.error(`Supabase getData error: ${error.message}`); 129 | return null; 130 | } 131 | } 132 | 133 | // Method to delete data by regNumber 134 | async deleteData(regNumber: string): Promise { 135 | try { 136 | const { error } = await supabase 137 | .from("goscrape") // Replace with your actual table name 138 | .delete() 139 | .eq("regNumber", regNumber); 140 | 141 | if (error) throw new Error(`Error deleting data: ${error.message}`); 142 | console.info(`Data deleted for regNumber: ${regNumber}`); 143 | } catch (error: any) { 144 | console.error(`Supabase deleteData error: ${error.message}`); 145 | } 146 | } 147 | } 148 | 149 | export default Database; 150 | -------------------------------------------------------------------------------- /utils/Database/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | 3 | // Define your Supabase URL and Key 4 | const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL; 5 | const SUPABASE_KEY = process.env.NEXT_PUBLIC_SERVICE_KEY; 6 | 7 | // Create a Supabase client instance 8 | const supabase = createClient(SUPABASE_URL ?? "", SUPABASE_KEY ?? ""); 9 | 10 | export { supabase }; 11 | -------------------------------------------------------------------------------- /utils/Date.ts: -------------------------------------------------------------------------------- 1 | export function isDateInRange(date: Date, range: DateRange) { 2 | return date >= range.from && date <= range.to; 3 | } 4 | 5 | export function isDateInRanges(date: Date, ranges: DateRange[]) { 6 | return ranges.some((range) => isDateInRange(date, range)); 7 | } 8 | 9 | export type DateRange = { 10 | from: Date; 11 | to: Date; 12 | }; 13 | -------------------------------------------------------------------------------- /utils/Grade.ts: -------------------------------------------------------------------------------- 1 | // Moved outside the component to prevent recreation on each render 2 | export const gradePoints: { [key: string]: number } = { 3 | O: 10, 4 | "A+": 9, 5 | A: 8, 6 | "B+": 7, 7 | B: 6, 8 | C: 5, 9 | }; 10 | 11 | // Exported for use in GradeCard to avoid duplication 12 | export function determineGrade(scoredMarks: number, totalMarks: number): string { 13 | // Prevent NaN on initial render 14 | if (!totalMarks) return "O"; 15 | 16 | const percentage = (scoredMarks / totalMarks) * 100; 17 | 18 | if (percentage >= 91) return "O"; 19 | if (percentage >= 81) return "A+"; 20 | if (percentage >= 71) return "A"; 21 | if (percentage >= 61) return "B+"; 22 | if (percentage >= 56) return "B"; 23 | if (percentage >= 50) return "C"; 24 | return "F"; 25 | } 26 | -------------------------------------------------------------------------------- /utils/Interval.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | 3 | export function useInterval(callback: () => void, delay: number | null) { 4 | const intRef = useRef(null); 5 | const cb = useRef(callback); 6 | 7 | useEffect(() => { 8 | cb.current = callback; 9 | }, [callback]); 10 | 11 | useEffect(() => { 12 | if (typeof delay === "number") { 13 | intRef.current = window.setInterval(() => cb.current(), delay); 14 | 15 | return () => window.clearInterval(intRef.current as number); 16 | } 17 | }, [delay]); 18 | 19 | return intRef; 20 | } 21 | -------------------------------------------------------------------------------- /utils/Margin.ts: -------------------------------------------------------------------------------- 1 | export function calculateMargin(present: number, total: number) { 2 | const pMin = 75; 3 | if ((present / total) * 100 >= pMin) 4 | return Math.floor((present - 0.75 * total) / 0.75); 5 | 6 | let requiredClassesToAttend = 0; 7 | while ( 8 | ((present + requiredClassesToAttend) / (total + requiredClassesToAttend)) * 9 | 100 < 10 | pMin 11 | ) { 12 | requiredClassesToAttend++; 13 | } 14 | return -requiredClassesToAttend; 15 | } 16 | -------------------------------------------------------------------------------- /utils/ProfileColor.ts: -------------------------------------------------------------------------------- 1 | function hashString(str: string): number { 2 | let hash = 0; 3 | for (let i = 0; i < str.length; i++) { 4 | const char = str.charCodeAt(i); 5 | hash = (hash << 5) - hash + char; 6 | hash |= 0; 7 | } 8 | return Math.abs(hash); 9 | } 10 | 11 | export const colors: string[] = [ 12 | "#FFAC81", 13 | "#7EC8BD", 14 | "#9CA3DB", 15 | "#EFE9AE", 16 | "#64f58d", 17 | "#EA8592", 18 | "#E0FE9A", 19 | "#FF928B", 20 | "#8789AB", 21 | "#8BCAE5", 22 | "#A5D8FF", 23 | "#FFCCF9", 24 | "#F4C2C2", 25 | "#C1E1C1", 26 | "#FFD1DC", 27 | "#FFDFD3", 28 | "#B5EAD7", 29 | "#FFC3A0", 30 | "#D4A5A5", 31 | "#FFABAB", 32 | "#F5E1A4", 33 | "#AFCBFF", 34 | "#C7CEEA", 35 | "#FFB7C5", 36 | "#FFDAC1", 37 | "#E2F0CB", 38 | "#D0E6A5", 39 | "#B5EAD7", 40 | "#E3B5A4", 41 | "#FFC9A9", 42 | "#D3C2C9", 43 | "#FFD6E5", 44 | "#E0BBE4", 45 | "#C7D3F3", 46 | "#F3FFE3", 47 | "#FAD0C3", 48 | "#E6E6FA", 49 | "#FFB6B9", 50 | "#A1CAF1", 51 | "#D2E1FF", 52 | "#FFB3BA", 53 | "#FFDEB5", 54 | "#D1C2E5", 55 | "#FFEFBA", 56 | "#BDECB6", 57 | "#FFB4A2", 58 | "#C5B5E3", 59 | "#FDD7E4", 60 | "#E8D7FF", 61 | "#F9C8C9", 62 | "#F4B2A7", 63 | "#D4B5B0", 64 | "#E8C1A0", 65 | "#FFF2CC", 66 | "#D9F0FF", 67 | "#E8A5A5", 68 | "#FAE1DD", 69 | "#D7C1E0", 70 | "#FFCCBC", 71 | "#F4E1D2", 72 | "#FFAAA5", 73 | "#D4ECDD", 74 | "#E8B4BC", 75 | "#FDC5F5", 76 | "#FFDEE9", 77 | "#B5D8C7", 78 | "#C1E1E3", 79 | "#FAD3E7", 80 | "#FFE3E3", 81 | "#FFD6A5", 82 | "#FFF5E1", 83 | "#D6E4F0", 84 | "#C2F5E3", 85 | "#FFE1A8", 86 | "#FFD5C2", 87 | "#FFD8E0", 88 | "#DCE2F0", 89 | "#FFBED8", 90 | "#FFD9C0", 91 | "#E6CFCF", 92 | "#EFD5B3", 93 | "#FFF3CA", 94 | "#FAEBD7", 95 | "#D5F0E5", 96 | "#F5D3E0", 97 | "#E7F2E9", 98 | "#FFCECE", 99 | "#FFE4E1", 100 | "#F8E2E1", 101 | "#F2E0D0", 102 | "#FAD9D9", 103 | "#E5D8D6", 104 | "#F5E7E6", 105 | ]; 106 | 107 | export function profileColor(registrationNumber: string): string { 108 | if (registrationNumber === undefined) return colors[0]; 109 | const hash = hashString(registrationNumber); 110 | const colorIndex = hash % colors.length; 111 | return colors[colorIndex]; 112 | } 113 | -------------------------------------------------------------------------------- /utils/Range.ts: -------------------------------------------------------------------------------- 1 | export function timeRange(now: Date, timeRange: string) { 2 | const [startTime, endTime] = timeRange.split("-"); 3 | const [startHour, startMinute] = startTime.split(":").map(Number); 4 | const [endHour, endMinute] = endTime.split(":").map(Number); 5 | 6 | const currentHour = now.getHours(); 7 | const currentMinute = now.getMinutes(); 8 | 9 | if ( 10 | currentHour > startHour || 11 | (currentHour === startHour && currentMinute >= startMinute) 12 | ) { 13 | if ( 14 | currentHour < endHour || 15 | (currentHour === endHour && currentMinute < endMinute) 16 | ) { 17 | return true; 18 | } 19 | } 20 | return false; 21 | } 22 | -------------------------------------------------------------------------------- /utils/Times.ts: -------------------------------------------------------------------------------- 1 | export const Time = { 2 | start: [ 3 | "08:00", 4 | "08:50", 5 | "09:45", 6 | "10:40", 7 | "11:35", 8 | "12:30", 9 | "13:25", 10 | "14:20", 11 | "15:10", 12 | "16:00", 13 | ], 14 | end: [ 15 | "08:50", 16 | "09:40", 17 | "10:35", 18 | "11:30", 19 | "12:25", 20 | "13:20", 21 | "14:15", 22 | "15:10", 23 | "16:00", 24 | "16:50", 25 | ], 26 | }; 27 | 28 | export function timeConvert(time: string) { 29 | let convertedTime: (string | number)[] = time 30 | .toString() 31 | .match(/^([01]\d|2[0-3])(:)([0-5]\d)(:[0-5]\d)?$/) || [time]; 32 | 33 | if (convertedTime.length > 1) { 34 | convertedTime = convertedTime.slice(1); 35 | convertedTime[0] = +convertedTime[0] % 12 || 12; 36 | } 37 | return convertedTime.join(""); 38 | } 39 | 40 | export const getIstTime = (): Date => { 41 | const currentTime = new Date(); 42 | const currentOffset = currentTime.getTimezoneOffset(); 43 | const istOffset = 330; 44 | return new Date(currentTime.getTime() + (istOffset + currentOffset) * 60000); 45 | }; 46 | -------------------------------------------------------------------------------- /utils/Tokenize.ts: -------------------------------------------------------------------------------- 1 | function createToken(secretKey: string): string { 2 | const timestamp = Math.floor(Date.now() / 1000); 3 | const data = `${timestamp}.${secretKey}`; 4 | 5 | const encodedData = btoa(data); 6 | return encodedData; 7 | } 8 | 9 | export const token = () => 10 | createToken(process.env.NEXT_PUBLIC_VALIDATION_KEY || ""); 11 | -------------------------------------------------------------------------------- /utils/URL.ts: -------------------------------------------------------------------------------- 1 | const serverUrls = [process.env.NEXT_PUBLIC_URL]; 2 | 3 | export const revalUrl = serverUrls[0]; 4 | export const filesUrl = process.env.NEXT_PUBLIC_FILES_URL; 5 | 6 | export default function rotateUrl(): string { 7 | const timestamp = Date.now(); 8 | const index = timestamp % serverUrls.length; 9 | return serverUrls[index] ?? ""; 10 | } 11 | -------------------------------------------------------------------------------- /utils/color.ts: -------------------------------------------------------------------------------- 1 | export function hexToRgb(hex: string) { 2 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 3 | return result 4 | ? { 5 | r: Number.parseInt(result[1], 16), 6 | g: Number.parseInt(result[2], 16), 7 | b: Number.parseInt(result[3], 16), 8 | } 9 | : null; 10 | } 11 | 12 | export function rgbToHex(rgb: string) { 13 | const [r, g, b] = rgb.split(',').map(x => Number.parseInt(x.trim())); 14 | return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; 15 | } -------------------------------------------------------------------------------- /utils/encrypt.ts: -------------------------------------------------------------------------------- 1 | import cryptoJs from "crypto-js"; 2 | 3 | export function Encrypt(text: string, key: string): string { 4 | return cryptoJs.AES.encrypt(JSON.stringify(text), key).toString(); 5 | } 6 | 7 | export function Decrypt(text: string, key: string): string { 8 | const bytes = cryptoJs.AES.decrypt(text, key); 9 | return JSON.parse(bytes.toString(cryptoJs.enc.Utf8)); 10 | } 11 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "regions": ["bom1"], 3 | "cleanUrls": true, 4 | "framework": "nextjs", 5 | "redirects": [ 6 | { 7 | "source": "/courses", 8 | "destination": "https://class-pro.vercel.app/academia/courses", 9 | "permanent": true 10 | }, 11 | { 12 | "source": "/calendar", 13 | "destination": "https://class-pro.vercel.app/academia/calendar", 14 | "permanent": true 15 | }, 16 | { 17 | "source": "/elab", 18 | "destination": "https://better-lab.vercel.app", 19 | "permanent": true 20 | }, 21 | { 22 | "source": "/papers", 23 | "destination": "https://docu-pro.vercel.app", 24 | "permanent": true 25 | }, 26 | { 27 | "source": "/map", 28 | "destination": "https://d23qowwaqkh3fj.cloudfront.net/wp-content/uploads/2022/06/srmist-ktr-campus-layout.jpg", 29 | "permanent": true 30 | }, 31 | { 32 | "source": "/ssr", 33 | "destination": "https://academia.srmist.edu.in/#Form:Student_Service_Requests_SSR", 34 | "permanent": true 35 | }, 36 | { 37 | "source": "/leave", 38 | "destination": "http://10.1.105.62/srmleaveapp", 39 | "permanent": true 40 | }, 41 | { 42 | "source": "/github", 43 | "destination": "https://github.com/rahuletto/classpro", 44 | "permanent": true 45 | }, 46 | { 47 | "source": "/instagram", 48 | "destination": "https://www.instagram.com/srm_academiapro/", 49 | "permanent": true 50 | }, 51 | { 52 | "source": "/whatsapp", 53 | "destination": "https://chat.whatsapp.com/FLvHgGU87OLEOeQIqJEhto", 54 | "permanent": true 55 | } 56 | ] 57 | } 58 | --------------------------------------------------------------------------------