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.
[](http://badges.mit-license.org)
## Meet the Team
+
[](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 (
-
-
+