diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..3723623 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +yarn lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..98d8b57 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules +dist +build +.next +coverage +.env diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..addd9c8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 9993d51..f9d0932 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -120,4 +120,4 @@ version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. \ No newline at end of file +https://www.contributor-covenant.org/faq. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba69e3b..724bd1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,15 +20,18 @@ Please note that this project is released with a [Contributor Code of Conduct](C - Review code on other people’s submissions and help improving / finding vulnerabilities ## Making a PR + - Provide all the appropriate details asked in PR template - A pull request doesn’t have to represent finished work. It’s usually better to open a pull request early on, so others can watch or give feedback on your progress. Just mark it as a “WIP” (Work in Progress) in the subject line. You can always add more commits later. ## Opening an Issue + - Make use of an appropriate Issue Template - We welcome Feature request, Bug Report, Documentation fix and others - Do not open critical security issues here, report them directly at [our email](mailto:contact@codechefvit.com). ## Communicating effectively + **Give context.** Help others get quickly up to speed. If you’re running into an error, explain what you’re trying to do and how to reproduce it. If you’re suggesting a new idea, explain why you think it’d be useful to the project (not just to you!). ``` @@ -65,8 +68,10 @@ Please note that this project is released with a [Contributor Code of Conduct](C ``` ## Misc + - You are welcome to Propose a new feature by creating an **Issue**. - You may Discuss a high-level topic or idea (for example, community, vision or policies) by writing to us at our [Email](mailto:contact@codechefvit.com). ## Attribution -- [Open Source Guide](https://opensource.guide/how-to-contribute/) \ No newline at end of file + +- [Open Source Guide](https://opensource.guide/how-to-contribute/) diff --git a/README.md b/README.md index c25fe74..f47cc4e 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ FFCS-inator -## FFCS-inator - CodeChef VIT -Generate priority-based timetables in seconds with FFCS-inator. The smartest way to plan your VIT FFCS. Pick your favorite faculties, set your subject and faculty priorities, and let FFCS-inator create the perfect clash-free timetable for you. Save your timetables for later, and easily share them with friends. No hassle. No stress. Just your ideal schedule, tailored to you. +## FFCS-inator - CodeChef VIT +Generate priority-based timetables in seconds with FFCS-inator. The smartest way to plan your VIT FFCS. Pick your favorite faculties, set your subject and faculty priorities, and let FFCS-inator create the perfect clash-free timetable for you. Save your timetables for later, and easily share them with friends. No hassle. No stress. Just your ideal schedule, tailored to you. ## Tech Stack @@ -23,35 +23,39 @@ Generate priority-based timetables in seconds with FFCS-inator. The smartest way ## Features -**Faculty Listings by Domain, School, Subject, and Slot** +**Faculty Listings by Domain, School, Subject, and Slot** + - Search and explore a complete list of faculties, organized by school, domain, subject, and slot. -**Automatic Timetable Generation** +**Automatic Timetable Generation** + - Generate all possible clash-free timetables automatically using a smart 2D priority algorithm that considers your faculty and subject preferences. -**Download Detailed Reports** +**Download Detailed Reports** + - Export a comprehensive PDF report showing: - All faculties possible for each subject - Detailed clash information between selected slots and faculties -**Save Timetables** +**Save Timetables** + - Save your generated timetables for future access and comparison. -**Share Timetables Securely** +**Share Timetables Securely** + - Share your timetable with others via a unique shareable link. - Control privacy settings to make your timetable public or private as needed. **Interactive Slot View & Clash Detection** + - Quickly visualize all available slots on a timetable grid. - Once a slot is selected, conflicting slots are automatically blocked to prevent clashes and simplify your choices. - - ## Get Started The repository has two branches, 'prod' and 'staging'. -prod (Production Branch) represents the live, user-facing version of the project where bug-free, well-tested code resides. +prod (Production Branch) represents the live, user-facing version of the project where bug-free, well-tested code resides. staging (Staging Branch) serves as a pre-production environment, where features and fixes are tested before they go live. @@ -62,6 +66,7 @@ Clone the 'prod' branch (production) ```bash git clone -b prod https://github.com/CodeChefVIT/ffcs.git ``` + OR Clone the 'staging' branch (staging) @@ -91,6 +96,7 @@ Before getting started, please ensure that the .env file is properly configured. [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](http://badges.mit-license.org) ## Meet the Team + [![Meet the Team](https://img.shields.io/badge/Meet%20the%20Team-Click%20Here-ff69b4?style=for-the-badge)](https://www.codechefvit.com/ffcs-inator)

diff --git a/components.json b/components.json index ffe928f..3289f23 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 00151d0..16c821e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,6 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -10,7 +10,7 @@ const compat = new FlatCompat({ }); const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), + ...compat.extends('next/core-web-vitals', 'next/typescript'), { rules: { // "@next/next/no-img-element": "off", @@ -27,4 +27,4 @@ const eslintConfig = [ }, ]; -export default eslintConfig; \ No newline at end of file +export default eslintConfig; diff --git a/middleware.ts b/middleware.ts index bc178b7..cc741dd 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,16 +1,16 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest, NextResponse } from 'next/server'; export async function middleware(request: NextRequest) { const sessionToken = - request.cookies.get("next-auth.session-token")?.value || - request.cookies.get("__Secure-next-auth.session-token")?.value; + request.cookies.get('next-auth.session-token')?.value || + request.cookies.get('__Secure-next-auth.session-token')?.value; if (!sessionToken) { - return NextResponse.redirect(new URL("/", request.url)); + return NextResponse.redirect(new URL('/', request.url)); } return NextResponse.next(); } export const config = { - matcher: ["/user/favorites"], + matcher: ['/user/favorites'], }; diff --git a/next.config.ts b/next.config.ts index f2b6766..8c6918d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,13 +1,13 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { images: { remotePatterns: [ { - protocol: "https", - hostname: "lh3.googleusercontent.com", - port: "", - pathname: "/**", + protocol: 'https', + hostname: 'lh3.googleusercontent.com', + port: '', + pathname: '/**', }, ], }, diff --git a/package.json b/package.json index 09dc89f..00a7658 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,12 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "format": "prettier --write .", + "prepare": "husky" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx,css,md,json}": "prettier --write" }, "dependencies": { "@auth/mongodb-adapter": "^3.9.1", @@ -37,6 +42,9 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.2", + "husky": "^9.1.7", + "lint-staged": "^16.2.3", + "prettier": "^3.6.2", "tailwindcss": "^4", "tw-animate-css": "^1.2.9", "typescript": "^5" diff --git a/postcss.config.mjs b/postcss.config.mjs index c7bcb4b..ba720fe 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,5 +1,5 @@ const config = { - plugins: ["@tailwindcss/postcss"], + plugins: ['@tailwindcss/postcss'], }; export default config; diff --git a/public/service-worker.js b/public/service-worker.js index 4a84e62..705baab 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,37 +1,37 @@ /// -const CACHE_NAME = "FFCS-CodeChefVIT_v1.1.1"; // Increment this on each deploy +const CACHE_NAME = 'FFCS-CodeChefVIT_v1.1.1'; // Increment this on each deploy const CORE_ASSETS = [ - "/", - "/offline", - "/logo_ffcs/icon-16x16.webp", - "/logo_ffcs/icon-32x32.webp", - "/logo_ffcs/icon-48x48.webp", - "/logo_ffcs/icon-64x64.webp", - "/logo_ffcs/icon-72x72.webp", - "/logo_ffcs/icon-76x76.webp", - "/logo_ffcs/icon-96x96.webp", - "/logo_ffcs/icon-114x114.webp", - "/logo_ffcs/icon-120x120.webp", - "/logo_ffcs/icon-128x128.webp", - "/logo_ffcs/icon-144x144.webp", - "/logo_ffcs/icon-152x152.webp", - "/logo_ffcs/icon-180x180.webp", - "/logo_ffcs/icon-192x192.webp", - "/logo_ffcs/icon-196x196.webp", - "/logo_ffcs/icon-228x228.webp", - "/logo_ffcs/icon-256x256.webp", - "/logo_ffcs/icon-384x384.webp", - "/logo_ffcs/icon-512x512.webp", - "/logo_ffcs.png", - "/logo_ffcs.svg", - "/logo_ffcs.webp", + '/', + '/offline', + '/logo_ffcs/icon-16x16.webp', + '/logo_ffcs/icon-32x32.webp', + '/logo_ffcs/icon-48x48.webp', + '/logo_ffcs/icon-64x64.webp', + '/logo_ffcs/icon-72x72.webp', + '/logo_ffcs/icon-76x76.webp', + '/logo_ffcs/icon-96x96.webp', + '/logo_ffcs/icon-114x114.webp', + '/logo_ffcs/icon-120x120.webp', + '/logo_ffcs/icon-128x128.webp', + '/logo_ffcs/icon-144x144.webp', + '/logo_ffcs/icon-152x152.webp', + '/logo_ffcs/icon-180x180.webp', + '/logo_ffcs/icon-192x192.webp', + '/logo_ffcs/icon-196x196.webp', + '/logo_ffcs/icon-228x228.webp', + '/logo_ffcs/icon-256x256.webp', + '/logo_ffcs/icon-384x384.webp', + '/logo_ffcs/icon-512x512.webp', + '/logo_ffcs.png', + '/logo_ffcs.svg', + '/logo_ffcs.webp', ]; const limitCacheSize = (name, maxItems) => { - caches.open(name).then((cache) => - cache.keys().then((keys) => { + caches.open(name).then(cache => + cache.keys().then(keys => { if (keys.length > maxItems) { cache.delete(keys[0]).then(() => limitCacheSize(name, maxItems)); } @@ -39,22 +39,20 @@ const limitCacheSize = (name, maxItems) => { ); }; -self.addEventListener("install", (event) => { - console.log("Service Worker: Installing..."); - event.waitUntil( - caches.open(CACHE_NAME).then((cache) => cache.addAll(CORE_ASSETS)) - ); +self.addEventListener('install', event => { + console.log('Service Worker: Installing...'); + event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(CORE_ASSETS))); self.skipWaiting(); }); -self.addEventListener("activate", (event) => { - console.log("Service Worker: Activating..."); +self.addEventListener('activate', event => { + console.log('Service Worker: Activating...'); event.waitUntil( - caches.keys().then((keys) => + caches.keys().then(keys => Promise.all( - keys.map((key) => { + keys.map(key => { if (key !== CACHE_NAME) { - console.log("Service Worker: Removing old cache", key); + console.log('Service Worker: Removing old cache', key); return caches.delete(key); } }) @@ -64,47 +62,47 @@ self.addEventListener("activate", (event) => { self.clients.claim(); }); -self.addEventListener("message", (event) => { - if (event.data && event.data.type === "SKIP_WAITING") { - console.log("Service Worker: SKIP_WAITING message received"); +self.addEventListener('message', event => { + if (event.data && event.data.type === 'SKIP_WAITING') { + console.log('Service Worker: SKIP_WAITING message received'); self.skipWaiting(); } }); -self.addEventListener("fetch", (event) => { +self.addEventListener('fetch', event => { const { request } = event; if ( - request.method !== "GET" || - request.url.includes("/api") || - request.url.includes("chrome-extension") || - request.url.startsWith("chrome") + request.method !== 'GET' || + request.url.includes('/api') || + request.url.includes('chrome-extension') || + request.url.startsWith('chrome') ) { return; } - if (request.headers.get("accept")?.includes("text/html")) { + if (request.headers.get('accept')?.includes('text/html')) { event.respondWith( fetch(request) - .then((res) => { + .then(res => { const resClone = res.clone(); - caches.open(CACHE_NAME).then((cache) => { + caches.open(CACHE_NAME).then(cache => { cache.put(request, resClone); }); return res; }) - .catch(() => caches.match(request).then((res) => res || caches.match("/offline"))) + .catch(() => caches.match(request).then(res => res || caches.match('/offline'))) ); return; } event.respondWith( - caches.match(request).then((res) => { + caches.match(request).then(res => { return ( res || fetch(request) - .then((fetchRes) => { - return caches.open(CACHE_NAME).then((cache) => { + .then(fetchRes => { + return caches.open(CACHE_NAME).then(cache => { if (request.url.startsWith(self.location.origin)) { cache.put(request, fetchRes.clone()); limitCacheSize(CACHE_NAME, 100); diff --git a/src/app/SessionProvider.tsx b/src/app/SessionProvider.tsx index 738ebb2..a075cd0 100644 --- a/src/app/SessionProvider.tsx +++ b/src/app/SessionProvider.tsx @@ -1,11 +1,7 @@ -"use client"; -import { SessionProvider } from "next-auth/react"; -import React from "react"; +'use client'; +import { SessionProvider } from 'next-auth/react'; +import React from 'react'; -export default function SessionProviderWrapper({ - children, -}: { - children: React.ReactNode; -}) { +export default function SessionProviderWrapper({ children }: { children: React.ReactNode }) { return {children}; } diff --git a/src/app/api/auth/[...nextauth]/authOptions.ts b/src/app/api/auth/[...nextauth]/authOptions.ts index dda1923..37a3855 100644 --- a/src/app/api/auth/[...nextauth]/authOptions.ts +++ b/src/app/api/auth/[...nextauth]/authOptions.ts @@ -1,8 +1,8 @@ -import { AuthOptions, SessionStrategy } from "next-auth"; -import GoogleProvider from "next-auth/providers/google"; -import { MongoDBAdapter } from "@auth/mongodb-adapter"; -import type { MongoClient } from "mongodb"; -import clientPromise from "../../../../lib/mongodb-client"; +import { AuthOptions, SessionStrategy } from 'next-auth'; +import GoogleProvider from 'next-auth/providers/google'; +import { MongoDBAdapter } from '@auth/mongodb-adapter'; +import type { MongoClient } from 'mongodb'; +import clientPromise from '../../../../lib/mongodb-client'; export const authOptions: AuthOptions = { providers: [ @@ -11,28 +11,27 @@ export const authOptions: AuthOptions = { clientSecret: process.env.GOOGLE_CLIENT_SECRET!, authorization: { params: { - scope: "openid email profile", - hd: "vitstudent.ac.in", - prompt: "select_account", + scope: 'openid email profile', + hd: 'vitstudent.ac.in', + prompt: 'select_account', }, }, }), ], adapter: MongoDBAdapter(clientPromise as Promise), session: { - strategy: "database" as SessionStrategy, + strategy: 'database' as SessionStrategy, }, callbacks: { async signIn({ user }) { - if (user.email && user.email.endsWith("@vitstudent.ac.in")) { + if (user.email && user.email.endsWith('@vitstudent.ac.in')) { return true; } return false; }, async redirect({ url, baseUrl }) { - - if (url.startsWith("/")) return `${baseUrl}${url}`; - + if (url.startsWith('/')) return `${baseUrl}${url}`; + if (new URL(url).origin === baseUrl) return url; return baseUrl; }, diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index f1cc59e..1305089 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,5 @@ -import NextAuth from "next-auth"; -import { authOptions } from "./authOptions"; +import NextAuth from 'next-auth'; +import { authOptions } from './authOptions'; const handler = NextAuth(authOptions); diff --git a/src/app/api/collect-email/route.ts b/src/app/api/collect-email/route.ts index 31f030f..e418bfc 100644 --- a/src/app/api/collect-email/route.ts +++ b/src/app/api/collect-email/route.ts @@ -1,14 +1,14 @@ -import { NextResponse } from "next/server"; -import { google, sheets_v4 } from "googleapis"; -import { JWT } from "google-auth-library"; +import { NextResponse } from 'next/server'; +import { google, sheets_v4 } from 'googleapis'; +import { JWT } from 'google-auth-library'; -const SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]; +const SCOPES = ['https://www.googleapis.com/auth/spreadsheets']; const SHEET_ID = process.env.SHEET_ID; async function getAuth(): Promise { return new google.auth.JWT({ email: process.env.GOOGLE_CLIENT_EMAIL, - key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, "\n"), + key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'), scopes: SCOPES, }); } @@ -16,14 +16,14 @@ async function getAuth(): Promise { async function appendEmailToSheet(email: string) { const authClient = await getAuth(); const sheets: sheets_v4.Sheets = google.sheets({ - version: "v4", + version: 'v4', auth: authClient, }); await sheets.spreadsheets.values.append({ spreadsheetId: SHEET_ID, - range: "Sheet1!A:A", - valueInputOption: "RAW", + range: 'Sheet1!A:A', + valueInputOption: 'RAW', requestBody: { values: [[email]], }, @@ -35,24 +35,24 @@ export async function POST(req: Request) { const { email } = await req.json(); if (!email) { - return NextResponse.json({ error: "Email is required" }, { status: 400 }); + return NextResponse.json({ error: 'Email is required' }, { status: 400 }); } try { await appendEmailToSheet(email); - return NextResponse.json({ message: "Email added successfully" }); + return NextResponse.json({ message: 'Email added successfully' }); } catch (error: unknown) { - console.error("Error adding email to sheet:", error); + console.error('Error adding email to sheet:', error); if ( - typeof error === "object" && + typeof error === 'object' && error !== null && - "code" in error && + 'code' in error && (error as { code?: unknown }).code === 403 ) { return NextResponse.json( { error: - "Permission denied. Make sure the service account has editor access to the spreadsheet.", + 'Permission denied. Make sure the service account has editor access to the spreadsheet.', details: (error as { message?: string }).message, }, { status: 403 } @@ -61,7 +61,7 @@ export async function POST(req: Request) { throw error; } } catch (error) { - console.error("Error processing request:", error); - return NextResponse.json({ error: "Failed to add email" }, { status: 500 }); + console.error('Error processing request:', error); + return NextResponse.json({ error: 'Failed to add email' }, { status: 500 }); } } diff --git a/src/app/api/save-timetable/route.ts b/src/app/api/save-timetable/route.ts index 07e4bf6..88da932 100644 --- a/src/app/api/save-timetable/route.ts +++ b/src/app/api/save-timetable/route.ts @@ -1,7 +1,7 @@ -import { NextRequest, NextResponse } from "next/server"; -import dbConnect from "@/lib/db"; -import Timetable from "@/models/timetable"; -import { generateShareId } from "@/lib/shareIDgenerate"; +import { NextRequest, NextResponse } from 'next/server'; +import dbConnect from '@/lib/db'; +import Timetable from '@/models/timetable'; +import { generateShareId } from '@/lib/shareIDgenerate'; export async function POST(req: NextRequest) { await dbConnect(); @@ -10,7 +10,7 @@ export async function POST(req: NextRequest) { const { title, slots, owner, isPublic } = body; if (!title || !slots || !owner) { - return NextResponse.json({ error: "Missing fields" }, { status: 400 }); + return NextResponse.json({ error: 'Missing fields' }, { status: 400 }); } try { @@ -30,9 +30,6 @@ export async function POST(req: NextRequest) { }); return NextResponse.json({ success: true, timetable }); } catch { - return NextResponse.json( - { error: "Failed to save timetable" }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to save timetable' }, { status: 500 }); } } diff --git a/src/app/api/shared-timetable/[shareId]/route.ts b/src/app/api/shared-timetable/[shareId]/route.ts index 5148da1..81702b5 100644 --- a/src/app/api/shared-timetable/[shareId]/route.ts +++ b/src/app/api/shared-timetable/[shareId]/route.ts @@ -1,27 +1,27 @@ -import { NextRequest, NextResponse } from "next/server"; -import dbConnect from "@/lib/db"; -import Timetable from "@/models/timetable"; +import { NextRequest, NextResponse } from 'next/server'; +import dbConnect from '@/lib/db'; +import Timetable from '@/models/timetable'; export async function GET(req: NextRequest) { await dbConnect(); - const shareId = req.nextUrl.pathname.split("/").pop(); + const shareId = req.nextUrl.pathname.split('/').pop(); if (!shareId) { - return NextResponse.json({ error: "Missing shareId" }, { status: 400 }); + return NextResponse.json({ error: 'Missing shareId' }, { status: 400 }); } try { const timetable = await Timetable.findOne({ shareId }); if (!timetable) { - return NextResponse.json({ error: "Not found" }, { status: 404 }); + return NextResponse.json({ error: 'Not found' }, { status: 404 }); } if (!timetable.isPublic) { return NextResponse.json({ success: false, - message: "Timetable is private", + message: 'Timetable is private', timetable: { title: timetable.title, }, @@ -38,6 +38,6 @@ export async function GET(req: NextRequest) { }, }); } catch { - return NextResponse.json({ error: "Server error" }, { status: 500 }); + return NextResponse.json({ error: 'Server error' }, { status: 500 }); } } diff --git a/src/app/api/timetables/[id]/route.ts b/src/app/api/timetables/[id]/route.ts index ceee6b2..b969f5e 100644 --- a/src/app/api/timetables/[id]/route.ts +++ b/src/app/api/timetables/[id]/route.ts @@ -1,11 +1,8 @@ -import { NextRequest, NextResponse } from "next/server"; -import dbConnect from "@/lib/db"; -import Timetable from "@/models/timetable"; +import { NextRequest, NextResponse } from 'next/server'; +import dbConnect from '@/lib/db'; +import Timetable from '@/models/timetable'; -export async function DELETE( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { await dbConnect(); const { id } = await params; @@ -13,14 +10,11 @@ export async function DELETE( await Timetable.findByIdAndDelete(id); return NextResponse.json({ success: true }); } catch { - return NextResponse.json({ error: "Failed to delete" }, { status: 500 }); + return NextResponse.json({ error: 'Failed to delete' }, { status: 500 }); } } -export async function PATCH( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { +export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { await dbConnect(); const { id } = await params; const body = await req.json(); @@ -33,30 +27,21 @@ export async function PATCH( await Timetable.findByIdAndUpdate(id, update); return NextResponse.json({ success: true }); } catch { - return NextResponse.json({ error: "Failed to update" }, { status: 500 }); + return NextResponse.json({ error: 'Failed to update' }, { status: 500 }); } } -export async function GET( - req: NextRequest, - { params }: { params: Promise<{ id: string }> } -) { +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { await dbConnect(); const { id } = await params; try { const timetable = await Timetable.findById(id).lean(); if (!timetable) { - return NextResponse.json( - { error: "Timetable not found" }, - { status: 404 } - ); + return NextResponse.json({ error: 'Timetable not found' }, { status: 404 }); } return NextResponse.json(timetable, { status: 200 }); } catch { - return NextResponse.json( - { error: "Failed to fetch timetable" }, - { status: 500 } - ); + return NextResponse.json({ error: 'Failed to fetch timetable' }, { status: 500 }); } } diff --git a/src/app/api/timetables/route.ts b/src/app/api/timetables/route.ts index 2d8069c..97bf1c5 100644 --- a/src/app/api/timetables/route.ts +++ b/src/app/api/timetables/route.ts @@ -1,18 +1,18 @@ -import { NextRequest, NextResponse } from "next/server"; -import dbConnect from "@/lib/db"; -import Timetable from "@/models/timetable"; +import { NextRequest, NextResponse } from 'next/server'; +import dbConnect from '@/lib/db'; +import Timetable from '@/models/timetable'; export async function GET(req: NextRequest) { await dbConnect(); const { searchParams } = new URL(req.url); - const owner = searchParams.get("owner"); + const owner = searchParams.get('owner'); if (!owner) { - return NextResponse.json({ error: "Missing owner" }, { status: 400 }); + return NextResponse.json({ error: 'Missing owner' }, { status: 400 }); } try { const timetables = await Timetable.find({ owner }).lean(); return NextResponse.json(timetables); } catch { - return NextResponse.json({ error: "Failed to fetch" }, { status: 500 }); + return NextResponse.json({ error: 'Failed to fetch' }, { status: 500 }); } } diff --git a/src/app/four04/four04-mobile.tsx b/src/app/four04/four04-mobile.tsx index 220a81f..2a84e35 100644 --- a/src/app/four04/four04-mobile.tsx +++ b/src/app/four04/four04-mobile.tsx @@ -1,9 +1,9 @@ -"use client"; +'use client'; -import { ZButton } from "@/components/ui/Buttons"; -import Footer from "@/components/ui/Footer"; -import Image from "next/image"; -import { useRouter } from "next/navigation"; +import { ZButton } from '@/components/ui/Buttons'; +import Footer from '@/components/ui/Footer'; +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; export default function NotFound() { const router = useRouter(); @@ -24,20 +24,16 @@ export default function NotFound() {

-
- FFCS-inator -
+
FFCS-inator
-
- By CodeChef-VIT -
+
By CodeChef-VIT
{/*Shadow*/} 404 @@ -46,7 +42,7 @@ export default function NotFound() { 404 @@ -68,7 +64,7 @@ export default function NotFound() { text="Home" color="purple" image="/icons/home.svg" - onClick={() => router.push("/")} + onClick={() => router.push('/')} />
diff --git a/src/app/four04/four04.tsx b/src/app/four04/four04.tsx index fdf307e..a340b61 100644 --- a/src/app/four04/four04.tsx +++ b/src/app/four04/four04.tsx @@ -1,11 +1,11 @@ -"use client"; +'use client'; -import Image from "next/image"; +import Image from 'next/image'; -import Navbar from "@/components/ui/Navbar"; -import { ZButton } from "@/components/ui/Buttons"; -import Footer from "@/components/ui/Footer"; -import { useRouter } from "next/navigation"; +import Navbar from '@/components/ui/Navbar'; +import { ZButton } from '@/components/ui/Buttons'; +import Footer from '@/components/ui/Footer'; +import { useRouter } from 'next/navigation'; export default function NotFound() { const router = useRouter(); @@ -32,7 +32,7 @@ export default function NotFound() { 404 @@ -41,7 +41,7 @@ export default function NotFound() { 404 @@ -63,7 +63,7 @@ export default function NotFound() { text="Home" color="purple" image="/icons/home.svg" - onClick={() => router.push("/")} + onClick={() => router.push('/')} />
diff --git a/src/app/globals.css b/src/app/globals.css index ed7b350..3dd01f0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,8 +1,8 @@ -@import url("https://fonts.googleapis.com/css2?family=Poppins&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Pangolin&display=swap"); -@import url("https://fonts.googleapis.com/css2?family=Inter&display=swap"); -@import "tailwindcss"; -@import "tw-animate-css"; +@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Pangolin&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Inter&display=swap'); +@import 'tailwindcss'; +@import 'tw-animate-css'; html { font-size: 16px; diff --git a/src/app/landing/landing-mobile.tsx b/src/app/landing/landing-mobile.tsx index b768e4d..d150cda 100644 --- a/src/app/landing/landing-mobile.tsx +++ b/src/app/landing/landing-mobile.tsx @@ -1,14 +1,14 @@ -"use client"; +'use client'; -import React, { useState } from "react"; -import Image from "next/image"; -import Footer from "@/components/ui/Footer"; -import Accordion from "@/components/ui/Accordion"; +import React, { useState } from 'react'; +import Image from 'next/image'; +import Footer from '@/components/ui/Footer'; +import Accordion from '@/components/ui/Accordion'; -import { ZButton } from "@/components/ui/Buttons"; -import { useRouter } from "next/navigation"; -import { signIn, useSession } from "next-auth/react"; -import { PopupLogin } from "@/components/ui/PopupMobile"; +import { ZButton } from '@/components/ui/Buttons'; +import { useRouter } from 'next/navigation'; +import { signIn, useSession } from 'next-auth/react'; +import { PopupLogin } from '@/components/ui/PopupMobile'; export default function View() { const router = useRouter(); @@ -21,9 +21,7 @@ export default function View() { {showLoginPopupSaved && ( setShowLoginPopupSaved(false)} - onLoginClick={() => - signIn("google", { callbackUrl: "/saved", redirect: true }) - } + onLoginClick={() => signIn('google', { callbackUrl: '/saved', redirect: true })} /> )} @@ -42,12 +40,8 @@ export default function View() {
-
- FFCS-inator -
-
- By CodeChef-VIT -
+
FFCS-inator
+
By CodeChef-VIT
router.push("/saved") - : () => setShowLoginPopupSaved(true) - } + onClick={loggedin ? () => router.push('/saved') : () => setShowLoginPopupSaved(true)} />
diff --git a/src/app/landing/landing.tsx b/src/app/landing/landing.tsx index ec1e97c..cc48748 100644 --- a/src/app/landing/landing.tsx +++ b/src/app/landing/landing.tsx @@ -1,45 +1,41 @@ -"use client"; +'use client'; -import React, { useEffect, useState } from "react"; -import Image from "next/image"; +import React, { useEffect, useState } from 'react'; +import Image from 'next/image'; -import Hero from "@/components/ui/Hero"; -import Navbar from "@/components/ui/Navbar"; -import Footer from "@/components/ui/Footer"; -import Accordion from "@/components/ui/Accordion"; +import Hero from '@/components/ui/Hero'; +import Navbar from '@/components/ui/Navbar'; +import Footer from '@/components/ui/Footer'; +import Accordion from '@/components/ui/Accordion'; -import FacultySelector from "@/components/cards/FacultySelector"; -import CourseCard from "@/components/cards/CourseCard"; -import ViewTimeTable from "@/components/cards/ViewTimeTable"; -import { TimetableProvider } from "@/lib/TimeTableContext"; -import { fullCourseData } from "@/lib/type"; -const LOCAL_STORAGE_KEY = "selectedCourses"; +import FacultySelector from '@/components/cards/FacultySelector'; +import CourseCard from '@/components/cards/CourseCard'; +import ViewTimeTable from '@/components/cards/ViewTimeTable'; +import { TimetableProvider } from '@/lib/TimeTableContext'; +import { fullCourseData } from '@/lib/type'; +const LOCAL_STORAGE_KEY = 'selectedCourses'; export default function View() { - const [selectedCourses, setSelectedCourses] = useState( - () => { - if (typeof window === "undefined") return []; - try { - const saved = localStorage.getItem("selectedCourses"); - return saved ? JSON.parse(saved) : []; - } catch { - return []; - } + const [selectedCourses, setSelectedCourses] = useState(() => { + if (typeof window === 'undefined') return []; + try { + const saved = localStorage.getItem('selectedCourses'); + return saved ? JSON.parse(saved) : []; + } catch { + return []; } - ); + }); const facultySelectorOnConfirm = (newCourse: fullCourseData) => { - setSelectedCourses((prevCourses) => { - const existingIndex = prevCourses.findIndex( - (course) => course.id === newCourse.id - ); + setSelectedCourses(prevCourses => { + const existingIndex = prevCourses.findIndex(course => course.id === newCourse.id); if (existingIndex !== -1) { const existingCourse = prevCourses[existingIndex]; - const updatedSlots = newCourse.courseSlots.map((newSlot) => { + const updatedSlots = newCourse.courseSlots.map(newSlot => { const existingSlot = existingCourse.courseSlots.find( - (slot) => slot.slotName === newSlot.slotName + slot => slot.slotName === newSlot.slotName ); if (existingSlot) { @@ -53,10 +49,7 @@ export default function View() { }); const preservedOldSlots = existingCourse.courseSlots.filter( - (oldSlot) => - !newCourse.courseSlots.some( - (newSlot) => newSlot.slotName === oldSlot.slotName - ) + oldSlot => !newCourse.courseSlots.some(newSlot => newSlot.slotName === oldSlot.slotName) ); const mergedSlots = [...preservedOldSlots, ...updatedSlots]; @@ -77,12 +70,12 @@ export default function View() { }; useEffect(() => { - if (typeof window === "undefined") return; + if (typeof window === 'undefined') return; try { localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(selectedCourses)); } catch (e) { - console.error("Failed to save courses to localStorage", e); + console.error('Failed to save courses to localStorage', e); } }, [selectedCourses]); @@ -112,10 +105,8 @@ export default function View() {
- setSelectedCourses((prev) => prev.filter((c) => c.id !== id)) - } - onUpdate={(updatedCourses) => setSelectedCourses(updatedCourses)} + onDelete={id => setSelectedCourses(prev => prev.filter(c => c.id !== id))} + onUpdate={updatedCourses => setSelectedCourses(updatedCourses)} />
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 52a8bfe..06533b7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,75 +1,69 @@ -import { Pangolin, Poppins, Inter } from "next/font/google"; -import "./globals.css"; -import SessionProviderWrapper from "./SessionProvider"; -import ServiceWorkerRegister from "../components/ServiceWorkerRegister/ServiceWorkerRegister"; -import Script from "next/script"; +import { Pangolin, Poppins, Inter } from 'next/font/google'; +import './globals.css'; +import SessionProviderWrapper from './SessionProvider'; +import ServiceWorkerRegister from '../components/ServiceWorkerRegister/ServiceWorkerRegister'; +import Script from 'next/script'; const pangolin = Pangolin({ - weight: "400", - subsets: ["latin"], - variable: "--font-pangolin", - display: "swap", + weight: '400', + subsets: ['latin'], + variable: '--font-pangolin', + display: 'swap', }); const poppins = Poppins({ - weight: ["400", "500", "600", "700"], - subsets: ["latin"], - variable: "--font-poppins", - display: "swap", + weight: ['400', '500', '600', '700'], + subsets: ['latin'], + variable: '--font-poppins', + display: 'swap', }); const inter = Inter({ - weight: ["400", "500", "600", "700"], - subsets: ["latin"], - variable: "--font-inter", - display: "swap", + weight: ['400', '500', '600', '700'], + subsets: ['latin'], + variable: '--font-inter', + display: 'swap', }); export const metadata = { - title: "FFCS-inator", + title: 'FFCS-inator', description: - "Generate priority-based timetables in seconds with FFCS-inator. No hassle. No stress. No more clashes. The smartest way to plan your VIT FFCS.", + 'Generate priority-based timetables in seconds with FFCS-inator. No hassle. No stress. No more clashes. The smartest way to plan your VIT FFCS.', icons: { - icon: "/logo_ffcs.svg", + icon: '/logo_ffcs.svg', }, - manifest: "/manifest.webmanifest", + manifest: '/manifest.webmanifest', openGraph: { - title: "FFCS-inator", + title: 'FFCS-inator', description: - "Generate priority-based timetables in seconds with FFCS-inator. No hassle. No stress. No more clashes. The smartest way to plan your VIT FFCS.", - siteName: "FFCS-inator", - type: "website", + 'Generate priority-based timetables in seconds with FFCS-inator. No hassle. No stress. No more clashes. The smartest way to plan your VIT FFCS.', + siteName: 'FFCS-inator', + type: 'website', images: [ { - url: "/og-image.png", - alt: "FFCS-inator - Timetable Generator for VIT", + url: '/og-image.png', + alt: 'FFCS-inator - Timetable Generator for VIT', width: 1200, height: 630, }, ], }, twitter: { - card: "summary_large_image", - title: "FFCS-inator", - description: - "Generate priority-based timetables in seconds with FFCS-inator.", - images: ["/og-image.png"], + card: 'summary_large_image', + title: 'FFCS-inator', + description: 'Generate priority-based timetables in seconds with FFCS-inator.', + images: ['/og-image.png'], }, - robots: "index, follow", + robots: 'index, follow', }; -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( - - +