Add Dark Mode, Password Hashing for better security , Settings Page, Policy PDF in Policy section,UI Changes

This commit is contained in:
arav
2026-01-10 19:39:40 +05:30
parent 933c0741ab
commit 9b605279e6
35 changed files with 1344 additions and 659 deletions

View File

@@ -15,7 +15,9 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.3",
"jwa": "^2.0.1",
"jws": "^4.0.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"prisma": "^5.20.0" "prisma": "^5.20.0"
@@ -196,7 +198,8 @@
"node_modules/buffer-equal-constant-time": { "node_modules/buffer-equal-constant-time": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
}, },
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
@@ -414,6 +417,7 @@
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
@@ -789,12 +793,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/jsonwebtoken": { "node_modules/jsonwebtoken": {
"version": "9.0.2", "version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jws": "^3.2.2", "jws": "^4.0.1",
"lodash.includes": "^4.3.0", "lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3", "lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4", "lodash.isinteger": "^4.0.4",
@@ -816,21 +820,23 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}, },
"node_modules/jwa": { "node_modules/jwa": {
"version": "1.4.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": { "dependencies": {
"buffer-equal-constant-time": "1.0.1", "buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11", "ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"node_modules/jws": { "node_modules/jws": {
"version": "3.2.2", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": { "dependencies": {
"jwa": "^1.4.1", "jwa": "^2.0.1",
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
@@ -1124,6 +1130,7 @@
"integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==", "integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@prisma/engines": "5.20.0" "@prisma/engines": "5.20.0"
}, },

View File

@@ -6,7 +6,9 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.3",
"jwa": "^2.0.1",
"jws": "^4.0.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"prisma": "^5.20.0" "prisma": "^5.20.0"

View File

@@ -1,4 +1,3 @@
// Generator to create Prisma Client
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native", "darwin-arm64", "linux-musl-arm64-openssl-3.0.x"] binaryTargets = ["native", "darwin-arm64", "linux-musl-arm64-openssl-3.0.x"]
@@ -40,7 +39,7 @@ enum Designation {
model User { model User {
profileId String @id @default(uuid()) profileId String @id @default(uuid())
userName String userName String
email String @unique @db.Text email String @unique
password String password String
institute Institute? institute Institute?
@@ -54,9 +53,9 @@ model User {
} }
model Application { model Application {
applicationId String @id @default(uuid()) applicationId String @id @default(uuid())
applicantId String applicantId String
applicant User @relation("AppliedApplications", fields: [applicantId], references: [profileId]) applicant User @relation("AppliedApplications", fields: [applicantId], references: [profileId])
institute Institute institute Institute
department String department String

View File

@@ -1,10 +1,16 @@
import prisma from "../src/config/prismaConfig.js"; import { PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs";
const prisma = new PrismaClient();
async function main() { async function main() {
// Common password for all users // Use a common password for all development users to make testing easier
const commonPassword = "securePassword123"; const commonPassword = "securePassword123";
// Applicant and Validator data // Secure the password with hashing even for seed data!
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(commonPassword, salt);
const institutes = [ const institutes = [
"KJSIDS", "KJSIDS",
"SKSC", "SKSC",
@@ -28,96 +34,116 @@ async function main() {
"Library", "Library",
]; ];
// Create VC (single, no department or institute) console.log("Seeding started...");
console.log("Seeding VC...");
await prisma.user.create({ // VC is usually global
data: { // We use 'upsert' here. It means "Update if exists, Insert if not".
userName: "Validator_VC", // This prevents errors if we run the seed script multiple times.
email: "vc@example.com", await prisma.user.upsert({
password: commonPassword, where: { email: "vc@somaiya.edu" },
update: {},
create: {
userName: "Vice Chancellor",
email: "vc@somaiya.edu",
password: hashedPassword,
institute: "KJSCE",
department: "Management",
designation: "VC", designation: "VC",
}, },
}); });
for (const institute of institutes) { for (const institute of institutes) {
// Create HOI for each institute // 1. Create HOI for this institute
console.log(`Seeding HOI for ${institute}...`); console.log(`Seeding HOI for ${institute}...`);
const hoiEmail = `hoi.${institute.toLowerCase()}@example.com`; const hoiEmail = `hoi@${institute.toLowerCase()}.edu`;
await prisma.user.upsert({
await prisma.user.create({ where: { email: hoiEmail },
data: { update: {},
create: {
userName: `HOI_${institute}`, userName: `HOI_${institute}`,
email: hoiEmail, email: hoiEmail,
password: commonPassword, password: hashedPassword,
institute, institute,
department: "Administration",
designation: "HOI", designation: "HOI",
}, },
}); });
// Create Accounts for each institute // 2. Create Accounts for this institute
console.log(`Seeding Accounts for ${institute}...`); console.log(`Seeding Accounts for ${institute}...`);
await prisma.user.create({ const accountsEmail = `accounts.${institute.toLowerCase()}@example.com`;
data: { await prisma.user.upsert({
where: { email: accountsEmail },
update: {},
create: {
userName: `Validator_Accounts_${institute}`, userName: `Validator_Accounts_${institute}`,
email: `accounts.${institute.toLowerCase()}@example.com`, email: accountsEmail,
password: commonPassword, password: hashedPassword,
institute, institute,
designation: "ACCOUNTS", designation: "ACCOUNTS",
}, },
}); });
for (const department of departments) { for (const department of departments) {
// Create HOD for each department of each institute // 3. Create HOD for each department of each institute
console.log(`Seeding HOD for ${department} in ${institute}...`); console.log(`Seeding HOD for ${department} in ${institute}...`);
const hodEmail = `hod.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`; const hodEmail = `hod.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`;
await prisma.user.upsert({
await prisma.user.create({ where: { email: hodEmail },
data: { update: {},
create: {
userName: `HOD_${department}_${institute}`, userName: `HOD_${department}_${institute}`,
email: hodEmail, email: hodEmail,
password: commonPassword, password: hashedPassword,
institute, institute,
department, department,
designation: "HOD", designation: "HOD",
}, },
}); });
// Create Faculty for each department of each institute // 4. Create Faculty for each department of each institute
console.log(`Seeding Faculty for ${department} in ${institute}...`); console.log(`Seeding Faculty for ${department} in ${institute}...`);
const facultyEmail = `faculty.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`; const facultyEmail = `faculty.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`;
await prisma.user.upsert({
await prisma.user.create({ where: { email: facultyEmail },
data: { update: {},
create: {
userName: `Faculty_${department}_${institute}`, userName: `Faculty_${department}_${institute}`,
email: facultyEmail, email: facultyEmail,
password: commonPassword, password: hashedPassword,
institute, institute,
department, department,
designation: "FACULTY", designation: "FACULTY",
}, },
}); });
// Create Student for each department of each institute
// 5. Create Student for each department of each institute
console.log(`Seeding Student for ${department} in ${institute}...`); console.log(`Seeding Student for ${department} in ${institute}...`);
const studentEmail = `student.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`; const studentEmail = `student.${department.toLowerCase()}.${institute.toLowerCase()}@example.com`;
await prisma.user.upsert({
await prisma.user.create({ where: { email: studentEmail },
data: { update: {},
create: {
userName: `Student_${department}_${institute}`, userName: `Student_${department}_${institute}`,
email: studentEmail, email: studentEmail,
password: commonPassword, password: hashedPassword,
institute, institute,
department, department,
designation: "STUDENT", designation: "STUDENT",
}, },
}); });
} }
console.log("Seeding completed!");
} }
console.log("Seeding completed successfully.");
} }
main().catch((e) => { main()
console.error(e); .then(async () => {
process.exit(1); await prisma.$disconnect();
}).finally(async () => { })
await prisma.$disconnect(); .catch(async (e) => {
}); console.error(e);
await prisma.$disconnect();
process.exit(1);
});

View File

@@ -55,11 +55,16 @@ const createApplication = async (req, res) => {
}); });
if (!intimationApplication) { if (!intimationApplication) {
return res.status(404).send({ message: "Intimation Application not found" }); return res
.status(404)
.send({ message: "Intimation Application not found" });
} }
if ( intimationApplication["formName"] !== "Travel Intimation Form") { if (intimationApplication["formName"] !== "Travel Intimation Form") {
return res.status(400).send({ message: "Intimation Application ID is not of a Travel Intimation Form" }); return res.status(400).send({
message:
"Intimation Application ID is not of a Travel Intimation Form",
});
} }
const validationFields = [ const validationFields = [
@@ -111,7 +116,9 @@ const createApplication = async (req, res) => {
}, },
}); });
if (!primarySupervisor) { if (!primarySupervisor) {
return res.status(404).send({ message: "Faculty not found (Incorrect Primary Supervisor Email)" }); return res.status(404).send({
message: "Faculty not found (Incorrect Primary Supervisor Email)",
});
} }
anotherSupervisor = await prisma.user.findUnique({ anotherSupervisor = await prisma.user.findUnique({
where: { where: {
@@ -161,7 +168,10 @@ const createApplication = async (req, res) => {
if (applicant.designation === "STUDENT") { if (applicant.designation === "STUDENT") {
// Validate that primary supervisor email is provided for student applications // Validate that primary supervisor email is provided for student applications
if (!formData.primarySupervisorEmail) { if (!formData.primarySupervisorEmail) {
return res.status(400).send({ message: "Primary supervisor email is required for student applications" }); return res.status(400).send({
message:
"Primary supervisor email is required for student applications",
});
} }
} }
@@ -215,7 +225,7 @@ const createApplication = async (req, res) => {
department, department,
institute, institute,
totalExpense, totalExpense,
formData, formData: JSON.stringify(formData),
proofOfTravel: proofOfTravelBuffer, proofOfTravel: proofOfTravelBuffer,
proofOfAccommodation: proofOfAccommodationBuffer, proofOfAccommodation: proofOfAccommodationBuffer,
proofOfAttendance: proofOfAttendanceBuffer, proofOfAttendance: proofOfAttendanceBuffer,
@@ -312,7 +322,7 @@ const updateApplication = async (req, res) => {
return res.status(404).send({ message: "Application not found" }); return res.status(404).send({ message: "Application not found" });
} }
const originalFormData = originalApplication.formData; const originalFormData = JSON.parse(originalApplication.formData);
// Verify that disabled fields haven't been changed // Verify that disabled fields haven't been changed
// Only expenses can be edited, everything else should remain the same regardless of resubmission status // Only expenses can be edited, everything else should remain the same regardless of resubmission status
@@ -400,7 +410,7 @@ const updateApplication = async (req, res) => {
const updatedData = { const updatedData = {
totalExpense, totalExpense,
formData, formData: JSON.stringify(formData),
proofOfTravel: proofOfTravelBuffer, proofOfTravel: proofOfTravelBuffer,
proofOfAccommodation: proofOfAccommodationBuffer, proofOfAccommodation: proofOfAccommodationBuffer,
proofOfAttendance: proofOfAttendanceBuffer, proofOfAttendance: proofOfAttendanceBuffer,

View File

@@ -1,5 +1,6 @@
import prisma from "../config/prismaConfig.js"; import prisma from "../config/prismaConfig.js";
import generateToken from "../services/generateToken.js"; import generateToken from "../services/generateToken.js";
import bcrypt from "bcryptjs";
const applicantLogin = async (req, res) => { const applicantLogin = async (req, res) => {
try { try {
@@ -8,7 +9,7 @@ const applicantLogin = async (req, res) => {
// Check if the applicant profile exists // Check if the applicant profile exists
const validProfile = await prisma.user.findUnique({ const validProfile = await prisma.user.findUnique({
where: { where: {
email email,
}, },
}); });
@@ -19,14 +20,51 @@ const applicantLogin = async (req, res) => {
}); });
} }
// Check if the password is correct let isPasswordCorrect = false;
if (validProfile.password !== password) { let needsMigration = false;
// Step 1: Try checking if it's a hashed password
try {
isPasswordCorrect = await bcrypt.compare(password, validProfile.password);
} catch (err) {
isPasswordCorrect = false;
}
// Step 2: If the bcrypt check failed, maybe they have an old plain-text password?
// This is a "Fallback" mechanism to prevent old users from getting locked out after we added hashing.
if (!isPasswordCorrect) {
if (validProfile.password === password) {
isPasswordCorrect = true;
needsMigration = true; // Flag them for migration to secure hash
}
}
if (!isPasswordCorrect) {
return res.status(404).json({ return res.status(404).json({
message: "Wrong Password", message: "Wrong Password",
data: null, data: null,
}); });
} }
// Step 3: If they were using a plain-text password, let's secure it now!
// We automatically update their password to a hash so next time they log in safely.
if (needsMigration) {
try {
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
await prisma.user.update({
where: { profileId: validProfile.profileId },
data: { password: hashedPassword },
});
console.log(
`[Security] Automatically migrated password for user ${email}`
);
} catch (migrationError) {
console.log("Could not update password", migrationError);
// It's checked, so we just log the error and continue
}
}
// Create token object // Create token object
const tokenObject = { const tokenObject = {
id: validProfile.profileId, id: validProfile.profileId,
@@ -41,7 +79,11 @@ const applicantLogin = async (req, res) => {
// Set the token as a cookie // Set the token as a cookie
return res return res
.cookie("access_token", token, { sameSite: 'None', secure: true, httpOnly: true }) .cookie("access_token", token, {
sameSite: "Lax",
secure: false,
httpOnly: true,
})
.status(200) .status(200)
.json({ .json({
message: "Login Successful", message: "Login Successful",
@@ -62,7 +104,7 @@ const validatorLogin = async (req, res) => {
// Check if the validator profile exists // Check if the validator profile exists
let validProfile = await prisma.user.findUnique({ let validProfile = await prisma.user.findUnique({
where: { where: {
email email,
}, },
}); });
@@ -73,14 +115,48 @@ const validatorLogin = async (req, res) => {
}); });
} }
// Check if the password is correct let isPasswordCorrect = false;
if (validProfile.password !== password) { let needsMigration = false;
// Step 1: Try checking if it's a hashed password
try {
isPasswordCorrect = await bcrypt.compare(password, validProfile.password);
} catch (err) {
isPasswordCorrect = false;
}
// Step 2: If the bcrypt check failed, maybe they have an old plain-text password?
// We check this so old users don't get locked out.
if (!isPasswordCorrect) {
if (validProfile.password === password) {
isPasswordCorrect = true;
needsMigration = true;
}
}
if (!isPasswordCorrect) {
return res.status(404).json({ return res.status(404).json({
message: "Wrong Password", message: "Wrong Password",
data: null, data: null,
}); });
} }
// Step 3: If they had a plain-text password, let's secure it now!
// We update it to a hash so next time it is safe.
if (needsMigration) {
try {
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
await prisma.user.update({
where: { profileId: validProfile.profileId },
data: { password: hashedPassword },
});
console.log(`Updated password for validator ${email}`);
} catch (migrationError) {
console.log("Could not update password", migrationError);
}
}
// Create token object // Create token object
const tokenObject = { const tokenObject = {
id: validProfile.profileId, id: validProfile.profileId,
@@ -95,7 +171,11 @@ const validatorLogin = async (req, res) => {
// Set the token as a cookie // Set the token as a cookie
return res return res
.cookie("access_token", token, { sameSite: 'None', secure: true, httpOnly: true }) .cookie("access_token", token, {
sameSite: "Lax",
secure: false,
httpOnly: true,
})
.status(200) .status(200)
.json({ .json({
message: "Login Successful", message: "Login Successful",

View File

@@ -4,6 +4,7 @@ import {
applicantDesignations, applicantDesignations,
validatorDesignations, validatorDesignations,
} from "../config/designations.js"; } from "../config/designations.js";
import bcrypt from "bcryptjs";
const dataRoot = async (req, res) => { const dataRoot = async (req, res) => {
try { try {
@@ -98,7 +99,12 @@ const getApplicationsByStatus = async (req, res) => {
}), }),
...(status === "ACCEPTED" && { ...(status === "ACCEPTED" && {
AND: [ AND: [
{ OR: [{ facultyValidation: "ACCEPTED" }, { facultyValidation: null }] }, {
OR: [
{ facultyValidation: "ACCEPTED" },
{ facultyValidation: null },
],
},
{ OR: [{ hodValidation: "ACCEPTED" }, { hodValidation: null }] }, { OR: [{ hodValidation: "ACCEPTED" }, { hodValidation: null }] },
{ OR: [{ hoiValidation: "ACCEPTED" }, { hoiValidation: null }] }, { OR: [{ hoiValidation: "ACCEPTED" }, { hoiValidation: null }] },
{ OR: [{ vcValidation: "ACCEPTED" }, { vcValidation: null }] }, { OR: [{ vcValidation: "ACCEPTED" }, { vcValidation: null }] },
@@ -185,15 +191,18 @@ const getApplicationsByStatus = async (req, res) => {
} }
// Format response with selected fields // Format response with selected fields
const responseApplications = applications.map((application) => ({ const responseApplications = applications.map((application) => {
applicationId: application.applicationId, const parsedFormData = JSON.parse(application.formData);
applicantName: application.applicantName, return {
formData: { applicationId: application.applicationId,
eventName: application.formData.eventName, applicantName: application.applicantName,
applicantDepartment: application.formData.applicantDepartment, formData: {
}, eventName: parsedFormData.eventName,
createdAt: application.createdAt, applicantDepartment: parsedFormData.applicantDepartment,
})); },
createdAt: application.createdAt,
};
});
return res.status(200).json({ return res.status(200).json({
message: `${status} Applications Fetched Successfully`, message: `${status} Applications Fetched Successfully`,
@@ -308,9 +317,14 @@ const getApplicationData = async (req, res) => {
} }
// Respond with the full application data and current status // Respond with the full application data and current status
const parsedApplicationFull = {
...applicationFull,
formData: JSON.parse(applicationFull.formData),
};
return res.status(200).json({ return res.status(200).json({
message: "Application data retrieved successfully", message: "Application data retrieved successfully",
data: { ...applicationFull, currentStatus }, data: { ...parsedApplicationFull, currentStatus },
}); });
} catch (error) { } catch (error) {
console.error("Error retrieving application data:", error); console.error("Error retrieving application data:", error);
@@ -436,4 +450,62 @@ const getFile = async (req, res) => {
} }
}; };
export { getApplicationData, getFile, dataRoot, getApplicationsByStatus }; export {
getApplicationData,
getFile,
dataRoot,
getApplicationsByStatus,
changePassword,
};
const changePassword = async (req, res) => {
try {
const user = req.user;
const { oldPassword, newPassword } = req.body;
if (!user || !user.id) {
return res.status(401).json({ message: "Unauthorized" });
}
// Get the current user from DB to check password
const dbUser = await prisma.user.findUnique({
where: { profileId: user.id },
});
if (!dbUser) {
return res.status(404).json({ message: "User not found" });
}
let isPasswordCorrect = false;
// 1. Try bcrypt
try {
isPasswordCorrect = await bcrypt.compare(oldPassword, dbUser.password);
} catch (err) {
isPasswordCorrect = false;
}
// 2. Try plaintext (fallback)
if (!isPasswordCorrect && dbUser.password === oldPassword) {
isPasswordCorrect = true;
}
if (!isPasswordCorrect) {
return res.status(400).json({ message: "Incorrect old password" });
}
// Hash the new password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(newPassword, salt);
await prisma.user.update({
where: { profileId: user.id },
data: { password: hashedPassword },
});
return res.status(200).json({ message: "Password updated successfully" });
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Internal Server Error" });
}
};

View File

@@ -1,5 +1,9 @@
import express from 'express'; import express from "express";
import { applicantLogin, logout, validatorLogin } from '../controllers/authControllers.js'; import {
applicantLogin,
logout,
validatorLogin,
} from "../controllers/authControllers.js";
const router = express.Router(); const router = express.Router();
@@ -8,4 +12,8 @@ router.post('/validator-login', validatorLogin);
router.get('/logout', logout) router.get('/logout', logout)
export default router; router.get("/", (req, res) => {
res.send("Travel Policy Backend is Running!");
});
export default router;

View File

@@ -1,11 +1,19 @@
import express from 'express'; import express from "express";
import { dataRoot, getApplicationData, getApplicationsByStatus, getFile } from '../controllers/generalControllers.js'; import {
dataRoot,
getApplicationData,
getApplicationsByStatus,
getFile,
changePassword,
} from "../controllers/generalControllers.js";
const router = express.Router(); const router = express.Router();
router.get("/dataRoot", dataRoot ); router.get("/dataRoot", dataRoot );
router.get('/getApplications/:status', getApplicationsByStatus); router.post("/changePassword", changePassword);
router.get("/getApplications/:status", getApplicationsByStatus);
router.get("/getApplicationData/:applicationId", getApplicationData); router.get("/getApplicationData/:applicationId", getApplicationData);

View File

@@ -1,9 +1,17 @@
import nodeMailer from 'nodemailer'; import nodeMailer from 'nodemailer';
export default async function sendMail({ emailId, link, type, status, designation }) { export default async function sendMail({
if (!process.env.TravelPolicyEmail || !process.env.TravelPolicyEmailPass) { emailId,
return; link,
} type,
status,
designation,
}) {
// Check if we have email password in env
if (!process.env.TravelPolicyEmail || !process.env.TravelPolicyEmailPass) {
console.log("No email password found in .env, so I can't send emails.");
return;
}
console.log("parametrs", emailId, link, type, status, designation); console.log("parametrs", emailId, link, type, status, designation);
@@ -48,20 +56,20 @@ export default async function sendMail({ emailId, link, type, status, designatio
<a href=${link}>View Application</a> <a href=${link}>View Application</a>
<p>Thank you.</p> <p>Thank you.</p>
`, `,
}; };
break; break;
case 'accounts': case "accounts":
mailOptions = { mailOptions = {
from: process.env.TravelPolicyEmail, from: process.env.TravelPolicyEmail,
to: emailId, to: emailId,
subject: 'Transfer money to the applicant', subject: "Transfer money to the applicant",
html: ` html: `
<p>Please transfer the travel policy amount to the applicant's account. Click on the link below to view the application:</p>` <p>Please transfer the travel policy amount to the applicant's account. Click on the link below to view the application:</p>`,
} };
break; break;
default: default:
throw new Error('Invalid email type'); throw new Error("Invalid email type");
} }
try { try {
await transporter.sendMail(mailOptions); await transporter.sendMail(mailOptions);

View File

@@ -92,6 +92,7 @@
"version": "7.24.9", "version": "7.24.9",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7", "@babel/code-frame": "^7.24.7",
@@ -405,6 +406,7 @@
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@emotion/memoize": "^0.9.0" "@emotion/memoize": "^0.9.0"
} }
@@ -1716,6 +1718,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"dev": true, "dev": true,
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2153,6 +2156,7 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001646", "caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4", "electron-to-chromium": "^1.5.4",
@@ -2252,6 +2256,7 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
"integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@kurkle/color": "^0.3.0" "@kurkle/color": "^0.3.0"
}, },
@@ -2868,6 +2873,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
@@ -5138,6 +5144,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.8", "nanoid": "^3.3.8",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -5355,6 +5362,7 @@
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -5376,6 +5384,7 @@
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@@ -5502,6 +5511,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
@@ -6169,6 +6179,7 @@
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz",
"integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/helper-module-imports": "^7.0.0", "@babel/helper-module-imports": "^7.0.0",
"@babel/traverse": "^7.4.5", "@babel/traverse": "^7.4.5",
@@ -6458,6 +6469,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -6791,6 +6803,7 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },

View File

@@ -1,5 +1,6 @@
import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { createBrowserRouter, RouterProvider } from "react-router-dom";
import React, { Suspense } from "react"; import React, { Suspense } from "react";
import { ThemeProvider } from "./context/ThemeContext";
import "./App.css"; import "./App.css";
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
@@ -15,7 +16,10 @@ const Applications = React.lazy(() => import("./pages/Applications/Applications"
const Report = React.lazy(() => import("./pages/Report/Report")); const Report = React.lazy(() => import("./pages/Report/Report"));
const LoginRoot = React.lazy(() => import("./components/LoginRoot/LoginRoot")); const LoginRoot = React.lazy(() => import("./components/LoginRoot/LoginRoot"));
const ContactUs = React.lazy(() => import("./pages/ContactUs/ContactUs")); const ContactUs = React.lazy(() => import("./pages/ContactUs/ContactUs"));
const ApplicationView = React.lazy(() => import("./pages/ApplicationView/ApplicationView")); const ApplicationView = React.lazy(() =>
import("./pages/ApplicationView/ApplicationView")
);
const Settings = React.lazy(() => import("./pages/Settings/Settings"));
import userDataLoader from "./services/userDataLoader"; import userDataLoader from "./services/userDataLoader";
import { upsertApplicationAction } from "./services/upsertApplicationAction"; import { upsertApplicationAction } from "./services/upsertApplicationAction";
@@ -41,9 +45,14 @@ const router = createBrowserRouter([
children: [ children: [
{ path: "dashboard", element: <Dashboard /> }, { path: "dashboard", element: <Dashboard /> },
{ path: "dashboard/:status", element: <Applications /> }, { path: "dashboard/:status", element: <Applications /> },
{ path: "dashboard/:status/:applicationId", element: <ApplicationView />, action: upsertApplicationAction }, {
path: "dashboard/:status/:applicationId",
element: <ApplicationView />,
action: upsertApplicationAction,
},
{ path: "form", element: <Form />, action: upsertApplicationAction }, { path: "form", element: <Form />, action: upsertApplicationAction },
{ path: "contact-us", element: <ContactUs /> }, { path: "contact-us", element: <ContactUs /> },
{ path: "settings", element: <Settings /> },
{ path: "policy", element: <Policy /> }, { path: "policy", element: <Policy /> },
], ],
}, },
@@ -55,8 +64,13 @@ const router = createBrowserRouter([
children: [ children: [
{ path: "dashboard", element: <Dashboard /> }, { path: "dashboard", element: <Dashboard /> },
{ path: "dashboard/:status", element: <Applications /> }, { path: "dashboard/:status", element: <Applications /> },
{ path: "dashboard/:status/:applicationId", element: <ApplicationView />, action: applicationStatusAction }, {
path: "dashboard/:status/:applicationId",
element: <ApplicationView />,
action: applicationStatusAction,
},
{ path: "report", element: <Report /> }, { path: "report", element: <Report /> },
{ path: "settings", element: <Settings /> },
{ path: "policy", element: <Policy /> }, { path: "policy", element: <Policy /> },
], ],
}, },
@@ -64,12 +78,12 @@ const router = createBrowserRouter([
function App() { function App() {
return ( return (
<> <ThemeProvider>
<ToastContainer position="top-center" /> <ToastContainer position="top-center" />
<Suspense fallback={<Loading/>}> <Suspense fallback={<Loading/>}>
<RouterProvider router={router} /> <RouterProvider router={router} />
</Suspense> </Suspense>
</> </ThemeProvider>
); );
} }

View File

@@ -32,9 +32,17 @@ const Root = () => {
return ( return (
<div className="h-full"> <div className="h-full">
<Navbar userData={user} role={role} setSidebarIsVisible={setSidebarIsVisible} sidebarIsVisible={sidebarIsVisible} /> <Navbar
<div className= "flex h-full bg-gray-100 overflow-auto"> userData={user}
{sidebarIsVisible && !(urlPath.split("/").at(-1).includes("dashboard")) && <Sidebar role={role} />} role={role}
setSidebarIsVisible={setSidebarIsVisible}
sidebarIsVisible={sidebarIsVisible}
/>
<div className="flex h-full bg-gray-100 dark:bg-google-dark overflow-auto transition-colors duration-200">
{sidebarIsVisible &&
!urlPath.split("/").at(-1).includes("dashboard") && (
<Sidebar role={role} />
)}
<div className="w-full min-h-full h-screen overflow-y-scroll"> <div className="w-full min-h-full h-screen overflow-y-scroll">
<Outlet /> <Outlet />
</div> </div>

View File

@@ -1,258 +1,388 @@
import React from "react"; import { FaSignOutAlt, FaMoon, FaSun } from "react-icons/fa";
import { FaSignOutAlt } from "react-icons/fa"; import { useTheme } from "../../context/ThemeContext";
import { Link, NavLink } from "react-router-dom"; import { Link, NavLink } from "react-router-dom";
import { toast } from "react-toastify";
export const handleLogout = async () => { const handleLogout = async () => {
let res = await fetch(`${import.meta.env.VITE_APP_API_URL}/logout`, { // Step 1: Call the backend logout route
method: "GET", // We need to do this to clear the httpOnly cookie from the browser
credentials: "include", try {
}); let res = await fetch(`${import.meta.env.VITE_APP_API_URL}/logout`, {
method: "GET",
credentials: "include", // Important: This ensures the cookie is sent/cleared
});
return res; // Step 2: If successful, we can redirect or show a message
if (res.ok) {
toast.info("Logged out successfully");
}
return res;
} catch (error) {
// console.log("Logout failed", error);
toast.error("Logout failed, please check your connection.");
}
}; };
const Sidebar = ({ role }) => ( const Sidebar = ({ role }) => {
<div className="w-72 h-screen bg-white p-6 shadow-lg z-10 flex flex-col overflow-y-auto"> // Using our custom ThemeContext to switch between Light and Dark mode
<div className="mb-8 text-center border-b-2 border-gray-200 pb-6"> const { theme, toggleTheme } = useTheme();
<div className="bg-white shadow-lg rounded-lg px-4 py-4 border border-gray-300">
<h2 className="text-xl font-semibold text-red-700 tracking-tight"> return (
{`${role} Portal`} <div className="w-72 h-full md:h-screen fixed md:relative top-0 left-0 z-40 bg-white dark:bg-google-dark dark:text-google-text p-6 shadow-lg flex flex-col overflow-y-auto transition-colors duration-200">
</h2> <div className="mb-8 text-center border-b-2 border-gray-200 pb-6">
<p className="text-gray-700 text-sm font-medium py-1"> <div className="bg-white dark:bg-google-gray shadow-lg rounded-lg px-4 py-4 border border-gray-300 dark:border-gray-600 transition-colors duration-200">
Travel Policy SVU <h2 className="text-xl font-semibold text-red-700 dark:text-red-500 tracking-tight">
</p> {`${role} Portal`}
</h2>
<p className="text-gray-700 dark:text-google-text-secondary text-sm font-medium py-1">
Travel Policy SVU
</p>
</div>
</div>
<nav className="flex-grow">
<ul className="space-y-4 text-sm">
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard`}
end
className={({ isActive }) =>
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 13l4 4L10 13m5-5h6a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6m-4 6l4 4 4-4"
></path>
</svg>{" "}
Dashboard
</NavLink>
</li>
<li className="text-gray-700 dark:text-google-text border-b border-gray-200 dark:border-gray-600 pb-2">
<span className="flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded cursor-pointer">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 9l-7 7-7-7"
></path>
</svg>{" "}
Application Status
</span>
<ul className="pl-4 mt-2 border-l border-gray-200 ml-2">
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard/pending`}
className={({ isActive }) =>
`flex items-center text-gray-600 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-black" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 3v18m9-9H3"
></path>
</svg>{" "}
Pending
</NavLink>
</li>
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard/accepted`}
className={({ isActive }) =>
`flex items-center text-gray-600 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-black" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
></path>
</svg>{" "}
Accepted
</NavLink>
</li>
<li className="pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard/rejected`}
className={({ isActive }) =>
`flex items-center text-gray-600 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-black" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>{" "}
Rejected
</NavLink>
</li>
</ul>
</li>
{/*
Conditional Rendering based on User Role.
If role is "Applicant", show Applicant links.
Otherwise (Validator), show Validator links.
*/}
{role === "Applicant" ? (
<>
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
<NavLink
to="/applicant/policy"
className={({ isActive }) =>
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 10v6a2 2 0 01-2 2H7a2 2 0 01-2-2V10a2 2 0 012-2h10a2 2 0 012 2zM10 14h4m-2-2v4"
></path>
</svg>{" "}
Policy
</NavLink>
</li>
<li>
<NavLink
to="/applicant/contact-us"
className={({ isActive }) =>
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 10h5l2 6h6l2-6h5"
></path>
</svg>{" "}
Contact Us
</NavLink>
</li>
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
<NavLink
to="/applicant/settings"
className={({ isActive }) =>
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
></path>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
</svg>
Settings
</NavLink>
</li>
</>
) : (
//role = "Validator"
<>
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
<NavLink
to="/validator/policy"
className={({ isActive }) =>
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 10v6a2 2 0 01-2 2H7a2 2 0 01-2-2V10a2 2 0 012-2h10a2 2 0 012 2zM10 14h4m-2-2v4"
></path>
</svg>{" "}
Policy
</NavLink>
</li>
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
<NavLink
to="/validator/report"
className={({ isActive }) =>
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 10h5l2 6h6l2-6h5"
></path>
</svg>{" "}
Report
</NavLink>
</li>
<li className="border-b border-gray-200 dark:border-gray-600 pb-2">
<NavLink
to="/validator/settings"
className={({ isActive }) =>
`flex items-center text-gray-800 dark:text-google-text hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
></path>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
</svg>
Settings
</NavLink>
</li>
</>
)}
</ul>
</nav>
{/* Spacer to push logout to the bottom */}
<div className="mt-14 space-y-4">
{/* Theme Toggle */}
<button
onClick={toggleTheme}
className="flex items-center w-full text-gray-700 dark:text-google-text hover:bg-gray-100 dark:hover:bg-gray-700 px-4 py-2 rounded-md transition-all duration-200"
>
{theme === "light" ? (
<>
<FaMoon className="w-4 h-4 mr-2" />
<span>Dark Mode</span>
</>
) : (
<>
<FaSun className="w-4 h-4 mr-2 text-yellow-400" />
<span>Light Mode</span>
</>
)}
</button>
<Link
to="/"
onClick={handleLogout}
className="flex items-center text-gray-700 dark:text-google-text hover:bg-red-700 hover:text-white px-4 py-2 rounded-md transition-all duration-200"
>
<FaSignOutAlt className="w-4 h-4 mr-2" />
Logout
</Link>
</div> </div>
</div> </div>
);
<nav className="flex-grow"> };
<ul className="space-y-4 text-sm">
<li className="border-b border-gray-200 pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard`}
end
className={({ isActive }) =>
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 13l4 4L10 13m5-5h6a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6m-4 6l4 4 4-4"
></path>
</svg>{" "}
Dashboard
</NavLink>
</li>
<li className="text-gray-700 border-b border-gray-200 pb-2">
<span className="flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded cursor-pointer">
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 9l-7 7-7-7"
></path>
</svg>{" "}
Application Status
</span>
<ul className="pl-4 mt-2 border-l border-gray-200 ml-2">
<li className="border-b border-gray-200 pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard/pending`}
className={({ isActive }) =>
`flex items-center text-gray-600 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-black" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 3v18m9-9H3"
></path>
</svg>{" "}
Pending
</NavLink>
</li>
<li className="border-b border-gray-200 pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard/accepted`}
className={({ isActive }) =>
`flex items-center text-gray-600 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-black" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 13l4 4L19 7"
></path>
</svg>{" "}
Accepted
</NavLink>
</li>
<li className="pb-2">
<NavLink
to={`/${role.toLowerCase()}/dashboard/rejected`}
className={({ isActive }) =>
`flex items-center text-gray-600 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-black" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>{" "}
Rejected
</NavLink>
</li>
</ul>
</li>
{role === "Applicant" ? (
<>
<li className="border-b border-gray-200 pb-2">
<NavLink
to="/applicant/policy"
className={({ isActive }) =>
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 10v6a2 2 0 01-2 2H7a2 2 0 01-2-2V10a2 2 0 012-2h10a2 2 0 012 2zM10 14h4m-2-2v4"
></path>
</svg>{" "}
Policy
</NavLink>
</li>
<li>
<NavLink
to="/applicant/contact-us"
className={({ isActive }) =>
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 10h5l2 6h6l2-6h5"
></path>
</svg>{" "}
Contact Us
</NavLink>
</li>
</>
) : (
//role = "Validator"
<>
<li>
<NavLink
to="/validator/report"
className={({ isActive }) =>
`flex items-center text-gray-800 hover:text-white hover:bg-red-700 p-2 rounded ${
isActive ? "font-extrabold" : ""
}`
}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 10h5l2 6h6l2-6h5"
></path>
</svg>{" "}
Report
</NavLink>
</li>
</>
)}
</ul>
</nav>
{/* Spacer to push logout to the bottom */}
<div className="mt-14">
<Link
to="/"
onClick={handleLogout}
className="flex items-center text-gray-700 hover:bg-red-700 hover:text-white px-4 py-2 rounded-md transition-all duration-200"
>
<FaSignOutAlt className="w-4 h-4 mr-2" />
Logout
</Link>
</div>
</div>
);
export default Sidebar; export default Sidebar;

View File

@@ -40,7 +40,7 @@ const LoginRoot = () => {
<footer className="flex items-center justify-center h-6 w-full bg-gray-100 text-gray-800 fixed bottom-0 left-0 z-50"> <footer className="flex items-center justify-center h-6 w-full bg-gray-100 text-gray-800 fixed bottom-0 left-0 z-50">
<div className="text-center text-sm"> <div className="text-center text-sm">
©2024 KJSCE, All Rights Reserved. ©{new Date().getFullYear()} KJSCE, All Rights Reserved.
</div> </div>
</footer> </footer>

View File

@@ -7,13 +7,13 @@ const Modal = ({ onClose, children }) => {
onClick={onClose} onClick={onClose}
> >
<div <div
className="bg-white p-6 rounded-lg relative w-11/12 md:w-3/5 lg:w-2/5 max-h-[85%] h-min overflow-auto" className="bg-white dark:bg-[#303134] p-6 rounded-lg relative w-11/12 md:w-3/5 lg:w-2/5 max-h-[85%] h-min overflow-auto transition-colors duration-200"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="flex justify-start p-2"> <div className="flex justify-start p-2">
<button <button
type='button' type="button"
className="absolute top-3 right-3 text-xl font-bold text-gray-700 hover:text-gray-900" className="absolute top-3 right-3 text-xl font-bold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
onClick={onClose} onClick={onClose}
> >
X X

View File

@@ -0,0 +1,35 @@
import React, { createContext, useContext, useEffect, useState } from "react";
const ThemeContext = createContext({
theme: "light",
toggleTheme: () => {},
});
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(() => {
// Check local storage or default to light
return localStorage.getItem("theme") || "light";
});
useEffect(() => {
const root = window.document.documentElement;
if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
localStorage.setItem("theme", theme);
}, [theme]);
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);

View File

@@ -1,10 +1,11 @@
import React from 'react' console.log("Main.jsx is executing...");
import ReactDOM from 'react-dom/client' import React from "react";
import App from './App.jsx' import ReactDOM from "react-dom/client";
import './index.css' import App from "./App.jsx";
import "./index.css";
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>
) );

View File

@@ -2,99 +2,131 @@ import React from "react";
const About = () => { const About = () => {
return ( return (
<div className="bg-white text-gray-800"> <div className="bg-white text-gray-800 transition-colors duration-200 min-h-screen">
{/* Hero Section */} {/*
<div className="relative bg-red-600 text-white h-[60vh] flex items-center justify-center"> Hero Section:
<div className="absolute inset-0 bg-red-800 bg-opacity-50"></div> Displays a welcoming background image with a call-to-action button.
<div className="relative z-10 text-center px-6"> We use inline styles for the background image to easily swap it if needed.
<h1 className="text-4xl md:text-6xl font-bold mb-4"> */}
<div
className="relative h-[60vh] flex items-center justify-center bg-cover bg-center"
style={{
backgroundImage:
"url('https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?ixlib=rb-4.0.3&auto=format&fit=crop&w=1600&q=80')",
}}
>
<div className="absolute inset-0 bg-gradient-to-r from-red-900 via-red-800 to-red-900 opacity-80"></div>
<div className="relative z-10 text-center px-6 animate-fade-in">
<h1 className="text-4xl md:text-6xl font-extrabold mb-4 text-white drop-shadow-lg">
Welcome to Our Travel Policy Welcome to Our Travel Policy
</h1> </h1>
<p className="text-lg md:text-xl"> <p className="text-lg md:text-xl text-gray-100 max-w-2xl mx-auto font-medium">
Structured, efficient, and research-focused travel planning. Structured, efficient, and research-focused travel planning for the
modern academic world.
</p> </p>
<button type='button' className="mt-6 px-6 py-3 bg-white text-red-600 font-semibold rounded-lg shadow-md hover:bg-red-700 hover:text-white transition duration-300"> <a
href="#approach"
className="inline-block mt-8 px-8 py-3 bg-white text-red-700 font-bold rounded-full shadow-lg hover:bg-gray-100 hover:scale-105 transition-all duration-300"
>
Learn More Learn More
</button> </a>
</div> </div>
</div> </div>
{/* Mission & Vision */} {/*
<div className="flex flex-col md:flex-row items-center py-12 px-6 md:px-12 gap-12"> Our Approach Section:
Explains the philosophy behind the travel policy.
*/}
<div
id="approach"
className="flex flex-col md:flex-row items-center py-16 px-6 md:px-12 gap-12 max-w-7xl mx-auto"
>
<div className="md:w-1/2"> <div className="md:w-1/2">
<h2 className="text-3xl font-bold mb-4">Our Approach</h2> <h2 className="text-3xl font-bold mb-6 text-red-700">Our Approach</h2>
<p className="text-lg leading-relaxed"> <p className="text-lg leading-relaxed text-gray-700">
Our travel policy for research students and associates ensures a Our travel policy for research students and associates ensures a
structured process for approvals and financial support, fostering structured process for approvals and financial support, fostering
efficiency and alignment with academic and research objectives. efficiency and alignment with academic and research objectives. We
believe in transparency and speed, enabling you to focus on your
work.
</p> </p>
</div> </div>
<div className="md:w-1/2"> <div className="md:w-1/2">
<img <img
src="https://via.placeholder.com/600x400" src="https://images.unsplash.com/photo-1436491865332-7a61a109cc05?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80"
alt="Travel Policy" alt="Travel Policy"
className="rounded-lg shadow-lg transform transition duration-300 hover:scale-105" className="rounded-xl shadow-2xl transform transition duration-500 hover:scale-105 w-full object-cover h-64 md:h-80"
/> />
</div> </div>
</div> </div>
{/* Achievements & History */} {/* Achievements & History */}
<div className="bg-gray-100 py-12 px-6 md:px-12"> <div className="bg-gray-50 py-16 px-6 md:px-12 transition-colors duration-200">
<h2 className="text-3xl font-bold text-center mb-8">Policy Highlights</h2> <div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <h2 className="text-3xl font-bold text-center mb-12 text-gray-900">
{[ Policy Highlights
{ </h2>
title: "Travel Request Form", <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
description: {[
"Submit detailed forms including purpose, destination, budget, and supporting documentation.", {
icon: "📄", title: "Travel Request Form",
}, description:
{ "Submit detailed forms including purpose, destination, budget, and supporting documentation.",
title: "Approval Process", icon: "📄",
description: },
"Supervisor, department head, and Office of Research must approve for major travels.", {
icon: "✔️", title: "Approval Process",
}, description:
{ "Supervisor, department head, and Office of Research must approve for major travels.",
title: "Financial Support", icon: "✔️",
description: },
"Eligibility depends on travel relevance, available funds, and research alignment.", {
icon: "💰", title: "Financial Support",
}, description:
{ "Eligibility depends on travel relevance, available funds, and research alignment.",
title: "Documentation", icon: "💰",
description: },
"Attach conference invitations or research collaboration letters for approval.", {
icon: "📜", title: "Documentation",
}, description:
{ "Attach conference invitations or research collaboration letters for approval.",
title: "International Travel", icon: "📜",
description: },
"Managed through the Office of Research for additional oversight and funding.", {
icon: "✈️", title: "International Travel",
}, description:
{ "Managed through the Office of Research for additional oversight and funding.",
title: "Funding Sources", icon: "✈️",
description: },
"Includes department funds, institutional grants, or scholarships.", {
icon: "🏛️", title: "Funding Sources",
}, description:
].map((item, index) => ( "Includes department funds, institutional grants, or scholarships.",
<div icon: "🏛️",
key={index} },
className="flex flex-col items-center p-6 bg-white shadow-md rounded-lg transition transform hover:scale-105" ].map((item, index) => (
> <div
<div className="text-4xl mb-4">{item.icon}</div> key={index}
<h3 className="text-xl font-semibold mb-2">{item.title}</h3> className="flex flex-col items-center p-8 bg-white shadow-lg rounded-xl transition-all duration-300 hover:-translate-y-2 hover:shadow-xl border border-transparent"
<p className="text-gray-700 text-center">{item.description}</p> >
</div> <div className="text-5xl mb-6">{item.icon}</div>
))} <h3 className="text-xl font-semibold mb-3 text-gray-900">
{item.title}
</h3>
<p className="text-gray-600 text-center leading-relaxed">
{item.description}
</p>
</div>
))}
</div>
</div> </div>
</div> </div>
{/* Why Choose Us */} {/* Why Choose Us */}
<div className="py-12 px-6 md:px-12"> <div className="py-16 px-6 md:px-12 max-w-7xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-8">Why Our Policy Stands Out</h2> <h2 className="text-3xl font-bold text-center mb-12 text-gray-900">
Why Our Policy Stands Out
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[ {[
{ {
@@ -112,7 +144,8 @@ const About = () => {
}, },
{ {
title: "Global Outlook", title: "Global Outlook",
description: "Facilitates international collaborations and exchanges.", description:
"Facilitates international collaborations and exchanges.",
}, },
{ {
title: "Comprehensive", title: "Comprehensive",
@@ -120,24 +153,25 @@ const About = () => {
}, },
{ {
title: "Transparent", title: "Transparent",
description: "Approval criteria and funding sources clearly defined.", description:
"Approval criteria and funding sources clearly defined.",
}, },
].map((item, index) => ( ].map((item, index) => (
<div <div
key={index} key={index}
className="flex flex-col items-start p-6 bg-white shadow-md rounded-lg transition transform hover:scale-105" className="flex flex-col items-start p-6 bg-white shadow-md rounded-lg transition transform hover:scale-105 border-l-4 border-red-600"
> >
<h3 className="text-xl font-semibold mb-2">{item.title}</h3> <h3 className="text-xl font-bold mb-2 text-gray-800">
<p className="text-gray-700">{item.description}</p> {item.title}
</h3>
<p className="text-gray-600">{item.description}</p>
</div> </div>
))} ))}
</div> </div>
</div> </div>
{/* Back to Top */} {/* Footer / Back to Top */}
<div className="text-center py-6"> <div className="text-center py-8 border-t border-gray-200"></div>
</div>
</div> </div>
); );
}; };

View File

@@ -39,9 +39,11 @@ function Input({
return ( return (
<div <div
key={sectionIndex} key={sectionIndex}
className="space-y-4 bg-white p-6 rounded-lg shadow-md min-w-fit border-t-4 border-red-700 mb-4" className="space-y-4 bg-white dark:bg-google-gray p-6 rounded-lg shadow-md min-w-fit border-t-4 border-red-700 mb-4 transition-colors duration-200"
> >
<h3 className="text-xl font-semibold mt-2 mb-4">{section.label}</h3> <h3 className="text-xl font-semibold mt-2 mb-4 dark:text-google-text">
{section.label}
</h3>
<div <div
className={`${ className={`${
section.label === "Expense Details" section.label === "Expense Details"
@@ -76,11 +78,11 @@ function Input({
return ( return (
<div <div
key={formFeild.name} key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md" className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
> >
<label <label
htmlFor={formFeild.name} htmlFor={formFeild.name}
className="block font-medium" className="block font-medium dark:text-google-text"
> >
{formFeild.label} {formFeild.label}
</label> </label>
@@ -90,7 +92,7 @@ function Input({
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
value={values[formFeild.name] || ""} value={values[formFeild.name] || ""}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out dark:bg-[#303134] dark:text-white"
disabled={formFeild?.disabled} disabled={formFeild?.disabled}
> >
<option value="" label="Select option" /> <option value="" label="Select option" />
@@ -119,11 +121,11 @@ function Input({
return ( return (
<div <div
key={formFeild.name} key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md" className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
> >
<label <label
htmlFor={formFeild.name} htmlFor={formFeild.name}
className="inline-flex items-center space-x-2" className="inline-flex items-center space-x-2 dark:text-google-text"
> >
<input <input
type="checkbox" type="checkbox"
@@ -149,11 +151,11 @@ function Input({
return ( return (
<div <div
key={formFeild.name} key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md" className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
> >
<label <label
htmlFor={formFeild.name} htmlFor={formFeild.name}
className="block font-medium" className="block font-medium dark:text-google-text"
> >
{formFeild.label} {formFeild.label}
</label> </label>
@@ -163,7 +165,7 @@ function Input({
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
value={values[formFeild.name] || ""} value={values[formFeild.name] || ""}
className="w-full px-3 py-2 border border-gray-300 rounded-md max-h-32 min-h-20 focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md max-h-32 min-h-20 focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out dark:bg-[#303134] dark:text-white"
disabled={formFeild?.disabled} disabled={formFeild?.disabled}
/> />
<p className="text-red-500 text-sm"> <p className="text-red-500 text-sm">
@@ -178,11 +180,11 @@ function Input({
return ( return (
<div <div
key={formFeild.name} key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md" className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
> >
<label <label
htmlFor={formFeild.name} htmlFor={formFeild.name}
className="block font-medium" className="block font-medium dark:text-google-text"
> >
{formFeild.label} {formFeild.label}
</label> </label>
@@ -206,7 +208,7 @@ function Input({
setFieldValue(formFeild.name, e.target.files[0]); setFieldValue(formFeild.name, e.target.files[0]);
}} }}
onBlur={handleBlur} onBlur={handleBlur}
className="w-full bg-white px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out" className="w-full bg-white dark:bg-[#303134] dark:text-white px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out"
/> />
<p className="text-red-500 text-sm"> <p className="text-red-500 text-sm">
{errors[formFeild.name] && {errors[formFeild.name] &&
@@ -222,7 +224,7 @@ function Input({
return ( return (
<div <div
key={formFeild.name} key={formFeild.name}
className="space-y-4 bg-slate-50 p-6 rounded-md w-full" className="space-y-4 bg-slate-50 dark:bg-[#3c4043] p-6 rounded-md w-full transition-colors duration-200"
> >
{pdfIsVisible && ( {pdfIsVisible && (
<PdfViewer <PdfViewer
@@ -235,7 +237,7 @@ function Input({
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4"> <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4">
<label <label
htmlFor={formFeild.name} htmlFor={formFeild.name}
className="block text-lg font-medium text-gray-800 mb-3 sm:mb-0 sm:w-1/2" className="block text-lg font-medium text-gray-800 dark:text-google-text mb-3 sm:mb-0 sm:w-1/2"
> >
{`${formFeild.label}: ₹${values[formFeild.name] {`${formFeild.label}: ₹${values[formFeild.name]
?.reduce( ?.reduce(
@@ -341,11 +343,11 @@ function Input({
return ( return (
<div <div
key={formFeild.name} key={formFeild.name}
className="space-y-1 bg-slate-50 p-3 rounded-md" className="space-y-1 bg-slate-50 dark:bg-[#3c4043] p-3 rounded-md transition-colors duration-200"
> >
<label <label
htmlFor={formFeild.name} htmlFor={formFeild.name}
className="block font-medium" className="block font-medium dark:text-google-text"
> >
{formFeild.label} {formFeild.label}
</label> </label>
@@ -356,7 +358,7 @@ function Input({
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
value={values[formFeild.name] || ""} value={values[formFeild.name] || ""}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-red-700 transition duration-300 ease-in-out dark:bg-[#303134] dark:text-white"
disabled={formFeild?.disabled} disabled={formFeild?.disabled}
max={formFeild?.max} max={formFeild?.max}
min={formFeild?.min} min={formFeild?.min}

View File

@@ -14,11 +14,11 @@ function AcceptChoice({
return ( return (
<Modal onClose={onClose}> <Modal onClose={onClose}>
<div className="bg-white rounded-lg p-6 shadow-lg mx-auto"> <div className="bg-white dark:bg-google-gray rounded-lg p-6 shadow-lg mx-auto transition-colors duration-200">
<h2 className="text-2xl font-semibold text-red-700 mb-4"> <h2 className="text-2xl font-semibold text-red-700 dark:text-red-500 mb-4">
Confirm Application Approval Confirm Application Approval
</h2> </h2>
<p className="text-gray-600 mb-6"> <p className="text-gray-600 dark:text-gray-300 mb-6">
{(() => { {(() => {
switch (designation) { switch (designation) {
case "FACULTY": case "FACULTY":

View File

@@ -128,7 +128,7 @@ function ApplicationView() {
setCopySuccess(true); setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000); setTimeout(() => setCopySuccess(false), 2000);
}) })
.catch(err => { .catch((err) => {
console.error("Failed to copy application ID: ", err); console.error("Failed to copy application ID: ", err);
alert("Failed to copy application ID. Please try again."); alert("Failed to copy application ID. Please try again.");
}); });
@@ -138,10 +138,12 @@ function ApplicationView() {
const isTravelIntimationForm = applicationDisplay?.formData?.formName === "Travel Intimation Form"; const isTravelIntimationForm = applicationDisplay?.formData?.formName === "Travel Intimation Form";
return ( return (
<div className="min-w-min bg-white shadow rounded-lg p-2 sm:p-4 md:p-6 m-4"> <div className="min-w-min bg-white dark:bg-google-gray shadow rounded-lg p-2 sm:p-4 md:p-6 m-4 transition-colors duration-200">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start mb-6 gap-3"> <div className="flex flex-col sm:flex-row sm:justify-between sm:items-start mb-6 gap-3">
<div> <div>
<h1 className="text-3xl font-extrabold text-gray-800">{title}</h1> <h1 className="text-3xl font-extrabold text-gray-800 dark:text-google-text">
{title}
</h1>
</div> </div>
{isTravelIntimationForm && ( {isTravelIntimationForm && (
<button <button

View File

@@ -17,15 +17,18 @@ function RejectionFeedback({ onClose, onSubmit }) {
return ( return (
<Modal onClose={onClose}> <Modal onClose={onClose}>
<div className="bg-white rounded-lg p-1 shadow-lg"> <div className="bg-white dark:bg-google-gray rounded-lg p-1 shadow-lg transition-colors duration-200">
<h2 className="text-2xl font-semibold text-red-700 mb-4">Confirm Application Rejection</h2> <h2 className="text-2xl font-semibold text-red-700 dark:text-red-500 mb-4">
<p className="text-gray-600 mb-6"> Confirm Application Rejection
Please provide a reason for rejecting the application.<br/> This will help the applicant to improve future applications. </h2>
<p className="text-gray-600 dark:text-gray-300 mb-6">
Please provide a reason for rejecting the application.
<br /> This will help the applicant to improve future applications.
</p> </p>
<form className="space-y-4"> <form className="space-y-4">
<textarea <textarea
className="w-full p-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-700" className="w-full p-4 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-700 dark:bg-[#303134] dark:text-white transition-colors duration-200"
placeholder="Enter the reason for rejection" placeholder="Enter the reason for rejection"
rows="4" rows="4"
value={reason} value={reason}

View File

@@ -37,15 +37,13 @@ function ValidationStatus({ validations, rejectionFeedback }) {
role.status role.status
)}`} )}`}
></div> ></div>
<p>{role.name}</p> <p className="dark:text-google-text">{role.name}</p>
</div> </div>
))} ))}
</div> </div>
{rejectionFeedback && ( {rejectionFeedback && (
<div <div className="mt-4 p-4 bg-red-100 dark:bg-red-900/30 border-l-4 border-red-500 text-red-700 dark:text-red-400 rounded-lg shadow-md w-fit min-w-[30%]">
className="mt-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 rounded-lg shadow-md w-fit min-w-[30%]"
>
<div className="flex justify-start items-center gap-2"> <div className="flex justify-start items-center gap-2">
<MdWarning className="w-6 h-6 text-red-500" /> <MdWarning className="w-6 h-6 text-red-500" />
<p className="font-semibold">Rejection Reason:</p> <p className="font-semibold">Rejection Reason:</p>

View File

@@ -74,9 +74,25 @@ const Applications = () => {
applications={applications} applications={applications}
/> />
) : ( ) : (
<p className="text-gray-600"> <div className="flex flex-col items-center justify-center py-10 opacity-70">
No {status.toLowerCase()} applications found. <svg
</p> className="w-24 h-24 text-gray-400 dark:text-gray-500 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
></path>
</svg>
<p className="text-xl font-medium text-gray-600 dark:text-google-text-secondary">
No {status.toLowerCase()} applications found.
</p>
</div>
); );
if (loading) { if (loading) {
@@ -91,7 +107,7 @@ const Applications = () => {
return ( return (
<main className="flex flex-col p-6"> <main className="flex flex-col p-6">
<div className="min-w-min bg-white shadow rounded-lg p-6 mb-20"> <div className="min-w-min bg-white dark:bg-google-gray shadow rounded-lg p-6 mb-20 transition-colors duration-200">
<ApplicationsStatusDescription /> <ApplicationsStatusDescription />
{role === "Validator" && ( {role === "Validator" && (

View File

@@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
const ApplicationTable = ({ title, applications,}) => { const ApplicationTable = ({ title, applications }) => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
@@ -11,11 +10,21 @@ const ApplicationTable = ({ title, applications,}) => {
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead> <thead>
<tr> <tr>
<th className="border-b p-4 text-gray-700">Topic</th> <th className="border-b p-4 text-gray-700 dark:text-google-text">
<th className="border-b p-4 text-gray-700">Name</th> Topic
<th className="border-b p-4 text-gray-700">Submitted</th> </th>
<th className="border-b p-4 text-gray-700">Branch</th> <th className="border-b p-4 text-gray-700 dark:text-google-text">
<th className="border-b p-4 text-gray-700">Status</th> Name
</th>
<th className="border-b p-4 text-gray-700 dark:text-google-text">
Submitted
</th>
<th className="border-b p-4 text-gray-700 dark:text-google-text">
Branch
</th>
<th className="border-b p-4 text-gray-700 dark:text-google-text">
Status
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -28,14 +37,16 @@ const ApplicationTable = ({ title, applications,}) => {
const newPath = location.split('/').slice(0, -1).join('/'); const newPath = location.split('/').slice(0, -1).join('/');
navigate(`${newPath}/${title.split(" ")[0]?.toLowerCase()}/${app.applicationId}`); navigate(`${newPath}/${title.split(" ")[0]?.toLowerCase()}/${app.applicationId}`);
}} }}
className="odd:bg-gray-50 even:bg-white hover:bg-gray-200 cursor-pointer" className="odd:bg-gray-50 even:bg-white dark:odd:bg-[#303134] dark:even:bg-[#202124] hover:bg-gray-200 dark:hover:bg-[#3c4043] cursor-pointer text-gray-900 dark:text-google-text transition-colors duration-200"
style={{ height: '50px' }} style={{ height: "50px" }}
> >
<td className="p-4">{app.formData.eventName}</td> <td className="p-4">{app.formData.eventName}</td>
<td className="p-4">{app.applicantName}</td> <td className="p-4">{app.applicantName}</td>
<td className="p-4">{formatDateToDDMMYYYY(app.createdAt)}</td> <td className="p-4">{formatDateToDDMMYYYY(app.createdAt)}</td>
<td className="p-4">{app.formData.applicantDepartment}</td> <td className="p-4">{app.formData.applicantDepartment}</td>
<td className="p-4 text-green-500">{title.split(" ")[0]}</td> <td className="p-4 text-green-500 dark:text-green-400">
{title.split(" ")[0]}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -9,16 +9,16 @@ function ApplicationsStatusDescription() {
const { status } = useParams(); const { status } = useParams();
return ( return (
<div className="bg-slate-50 shadow-md rounded-lg p-6 mb-8 border border-slate-400"> <div className="bg-slate-50 dark:bg-[#3c4043] shadow-md rounded-lg p-6 mb-8 border border-slate-400 dark:border-gray-600 transition-colors duration-200">
<div className="flex justify-between items-center mb-6 gap-5"> <div className="relative flex flex-col md:flex-row items-center justify-center mb-6 gap-5">
<h1 className="text-3xl font-semibold text-gray-800"> <h1 className="text-3xl font-semibold text-gray-800 dark:text-google-text text-center">
{`${status.toUpperCase()} APPLICATIONS`} {`${status.toUpperCase()} APPLICATIONS`}
</h1> </h1>
{role === "Applicant" && ( {role === "Applicant" && (
<button <button
type='button' type='button'
onClick={() => navigate("../form")} onClick={() => navigate("../form")}
className="flex items-center bg-gradient-to-r from-red-600 to-red-800 hover:from-red-800 hover:to-red-600 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transform transition duration-300 ease-in-out hover:scale-105" className="md:absolute md:right-0 flex items-center bg-gradient-to-r from-red-600 to-red-800 hover:from-red-800 hover:to-red-600 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transform transition duration-300 ease-in-out hover:scale-105"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -38,7 +38,7 @@ function ApplicationsStatusDescription() {
</button> </button>
)} )}
</div> </div>
<p className="text-gray-600 text-lg leading-relaxed sm:block hidden"> <p className="text-gray-600 dark:text-google-text-secondary text-lg leading-relaxed sm:block hidden">
Easily track the details and statuses of all your submitted applications Easily track the details and statuses of all your submitted applications
in one place. in one place.
<br /> <br />

View File

@@ -13,7 +13,7 @@ function ContactUs() {
}; };
return ( return (
<div className="h-full bg-white text-gray-900"> <div className="h-full bg-white dark:bg-google-dark text-gray-900 dark:text-google-text transition-colors duration-200">
{/* Hero Section */} {/* Hero Section */}
<section className="relative bg-red-600 text-white h-[60vh] flex items-center justify-center"> <section className="relative bg-red-600 text-white h-[60vh] flex items-center justify-center">
<div className="absolute inset-0 bg-red-700 opacity-60"></div> <div className="absolute inset-0 bg-red-700 opacity-60"></div>
@@ -25,44 +25,67 @@ function ContactUs() {
</section> </section>
{/* Contact Form */} {/* Contact Form */}
<section id="contact-form" className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100"> <section
id="contact-form"
className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100 dark:bg-google-gray transition-colors duration-200"
>
<div className="max-w-lg mx-auto"> <div className="max-w-lg mx-auto">
<h2 className="text-2xl font-semibold text-center mb-6">Contact Form</h2> <h2 className="text-2xl font-semibold text-center mb-6">Contact Form</h2>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div> <div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Name</label> <label
htmlFor="name"
className="block text-sm font-medium text-gray-700 dark:text-google-text"
>
Name
</label>
<input <input
id="name" id="name"
type="text" type="text"
required required
className="w-full px-4 py-2 mt-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-red-700" className="w-full px-4 py-2 mt-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-red-700 dark:bg-[#3c4043] dark:text-white transition-colors duration-200"
/> />
</div> </div>
<div> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label> <label
htmlFor="email"
className="block text-sm font-medium text-gray-700 dark:text-google-text"
>
Email
</label>
<input <input
id="email" id="email"
type="email" type="email"
required required
className="w-full px-4 py-2 mt-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-red-700" className="w-full px-4 py-2 mt-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-red-700 dark:bg-[#3c4043] dark:text-white transition-colors duration-200"
/> />
</div> </div>
<div> <div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-700">Subject</label> <label
htmlFor="subject"
className="block text-sm font-medium text-gray-700 dark:text-google-text"
>
Subject
</label>
<input <input
id="subject" id="subject"
type="text" type="text"
required required
className="w-full px-4 py-2 mt-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-red-700" className="w-full px-4 py-2 mt-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-red-700 dark:bg-[#3c4043] dark:text-white transition-colors duration-200"
/> />
</div> </div>
<div> <div>
<label htmlFor="message" className="block text-sm font-medium text-gray-700">Message</label> <label
htmlFor="message"
className="block text-sm font-medium text-gray-700 dark:text-google-text"
>
Message
</label>
<textarea <textarea
id="message" id="message"
rows="4" rows="4"
required required
className="w-full px-4 py-2 mt-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-red-700" className="w-full px-4 py-2 mt-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-red-700 dark:bg-[#3c4043] dark:text-white transition-colors duration-200"
/> />
</div> </div>
<button <button
@@ -82,7 +105,7 @@ function ContactUs() {
</section> </section>
{/* Location Map */} {/* Location Map */}
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-white"> <section className="py-12 px-4 sm:px-6 lg:px-8 bg-white dark:bg-google-dark transition-colors duration-200">
<div className="max-w-screen-xl mx-auto"> <div className="max-w-screen-xl mx-auto">
<h2 className="text-2xl font-semibold text-center mb-6">Our Location</h2> <h2 className="text-2xl font-semibold text-center mb-6">Our Location</h2>
<div className="relative"> <div className="relative">
@@ -95,17 +118,20 @@ function ContactUs() {
</section> </section>
{/* Contact Details */} {/* Contact Details */}
<section className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100"> <section className="py-12 px-4 sm:px-6 lg:px-8 bg-gray-100 dark:bg-google-gray transition-colors duration-200">
<div className="max-w-screen-xl mx-auto flex flex-col sm:flex-row justify-between items-center"> <div className="max-w-screen-xl mx-auto flex flex-col sm:flex-row justify-between items-center">
<div className="flex flex-col items-center sm:items-start mb-6 sm:mb-0"> <div className="flex flex-col items-center sm:items-start mb-6 sm:mb-0">
<h3 className="text-xl font-semibold text-gray-800">Contact Details</h3> <h3 className="text-xl font-semibold text-gray-800 dark:text-google-text">
<p className="mt-2 text-gray-600"> Contact Details
<span className="font-medium">Address:</span> K. J. Somaiya College of Engineering, Vidya Nagar, Mumbai, India </h3>
<p className="mt-2 text-gray-600 dark:text-google-text-secondary">
<span className="font-medium">Address:</span> K. J. Somaiya
College of Engineering, Vidya Nagar, Mumbai, India
</p> </p>
<p className="mt-2 text-gray-600"> <p className="mt-2 text-gray-600 dark:text-google-text-secondary">
<span className="font-medium">Phone:</span> (022) 6728 8000 <span className="font-medium">Phone:</span> (022) 6728 8000
</p> </p>
<p className="mt-2 text-gray-600"> <p className="mt-2 text-gray-600 dark:text-google-text-secondary">
<span className="font-medium">Email:</span> info@somaiya.edu <span className="font-medium">Email:</span> info@somaiya.edu
</p> </p>
</div> </div>

View File

@@ -15,7 +15,7 @@ function Dashboard() {
const greetingLine2 = `${designation} in ${department} Department, ${institute}`; const greetingLine2 = `${designation} in ${department} Department, ${institute}`;
return ( return (
<div className="font-sans bg-white overflow-y-scroll scroll-smooth snap-y h-screen" > <div className="font-sans bg-white dark:bg-google-dark overflow-y-scroll scroll-smooth snap-y h-screen transition-colors duration-200">
{/* Hero Section */} {/* Hero Section */}
<section <section
className="relative w-full h-screen flex items-center justify-center text-white overflow-hidden bg-cover bg-center snap-start" className="relative w-full h-screen flex items-center justify-center text-white overflow-hidden bg-cover bg-center snap-start"
@@ -68,19 +68,21 @@ function Dashboard() {
{/* Features Section */} {/* Features Section */}
<section <section
id="features" id="features"
className="py-12 sm:py-16 px-6 sm:px-8 md:px-12 lg:px-16 bg-gradient-to-b from-white via-gray-100 to-red-50 min-h-screen snap-start" className="py-12 sm:py-16 px-6 sm:px-8 md:px-12 lg:px-16 bg-gradient-to-b from-white via-gray-100 to-red-50 dark:from-google-dark dark:via-[#25262a] dark:to-[#1e1f21] min-h-screen snap-start transition-colors duration-200"
> >
<h2 className="text-3xl sm:text-4xl md:text-5xl font-semibold text-center text-red-700 mb-10 sm:mb-12"> <h2 className="text-3xl sm:text-4xl md:text-5xl font-semibold text-center text-red-700 dark:text-red-500 mb-10 sm:mb-12">
Our Key Features Our Key Features
</h2> </h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-12"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-12">
{/* Feature 1 */} {/* Feature 1 */}
<div className="flex flex-col items-center bg-white p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105"> <div className="flex flex-col items-center bg-white dark:bg-google-gray p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105 duration-200">
<div className="text-5xl mb-4 text-red-700">🔍</div> <div className="text-5xl mb-4 text-red-700 dark:text-red-500">
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 text-center"> 🔍
</div>
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 dark:text-red-500 text-center">
View Your Applications View Your Applications
</h3> </h3>
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700"> <p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700 dark:text-gray-300">
{role === "Applicant" {role === "Applicant"
? "Manage and track the status of your funding applications. Review feedback and make necessary updates." ? "Manage and track the status of your funding applications. Review feedback and make necessary updates."
: "Review and validate applications submitted by applicants. Approve or reject based on eligibility and guidelines."} : "Review and validate applications submitted by applicants. Approve or reject based on eligibility and guidelines."}
@@ -97,12 +99,14 @@ function Dashboard() {
{/* Feature 2 */} {/* Feature 2 */}
{role === "Validator" && ( {role === "Validator" && (
<div className="flex flex-col items-center bg-white p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105"> <div className="flex flex-col items-center bg-white dark:bg-google-gray p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105 duration-200">
<div className="text-5xl mb-4 text-red-700">📊</div> <div className="text-5xl mb-4 text-red-700 dark:text-red-500">
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 text-center"> 📊
</div>
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 dark:text-red-500 text-center">
View Insights View Insights
</h3> </h3>
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700"> <p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700 dark:text-gray-300">
Analyze and gain insights about funding and related data. Analyze and gain insights about funding and related data.
</p> </p>
<button <button
@@ -116,12 +120,14 @@ function Dashboard() {
{/* Feature 3 */} {/* Feature 3 */}
{role === "Applicant" && ( {role === "Applicant" && (
<div className="flex flex-col items-center bg-white p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105"> <div className="flex flex-col items-center bg-white dark:bg-google-gray p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105 duration-200">
<div className="text-5xl mb-4 text-red-700">📝</div> <div className="text-5xl mb-4 text-red-700 dark:text-red-500">
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 text-center"> 📝
</div>
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 dark:text-red-500 text-center">
Create New Application Create New Application
</h3> </h3>
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700"> <p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700 dark:text-gray-300">
Start your application process to apply for funding and Start your application process to apply for funding and
financial assistance. financial assistance.
</p> </p>
@@ -135,12 +141,14 @@ function Dashboard() {
)} )}
{/* Feature 4 */} {/* Feature 4 */}
<div className="flex flex-col items-center bg-white p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105"> <div className="flex flex-col items-center bg-white dark:bg-google-gray p-6 sm:p-8 rounded-2xl shadow-lg hover:shadow-xl transition-transform transform hover:-translate-y-4 hover:scale-105 duration-200">
<div className="text-5xl mb-4 text-red-700">📚</div> <div className="text-5xl mb-4 text-red-700 dark:text-red-500">
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 text-center"> 📚
</div>
<h3 className="text-lg sm:text-xl md:text-2xl font-semibold mb-2 text-red-700 dark:text-red-500 text-center">
Understand the Policy Understand the Policy
</h3> </h3>
<p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700"> <p className="text-center text-sm sm:text-base md:text-lg mb-6 text-gray-700 dark:text-gray-300">
Learn about the eligibility, funding process, and guidelines for Learn about the eligibility, funding process, and guidelines for
financial assistance. financial assistance.
</p> </p>

View File

@@ -1,11 +1,15 @@
import React from 'react' const Policy = () => {
function Policy() {
return ( return (
<div> <div className="flex flex-col h-screen w-full bg-gray-100 dark:bg-google-dark">
Policy <div className="flex-grow w-full h-full">
<iframe
src="https://svu-iqac.somaiya.edu/University+Policies/12.Travel+Policy.pdf"
className="w-full h-full border-none"
title="Travel Policy"
/>
</div>
</div> </div>
) );
} };
export default Policy export default Policy;

View File

@@ -11,7 +11,7 @@ function Report() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
return ( return (
<main className="flex flex-col p-6"> <main className="flex flex-col p-6">
<div className="bg-white shadow rounded-lg p-6 w-full"> <div className="bg-white dark:bg-google-gray shadow rounded-lg p-6 w-full transition-colors duration-200">
<FilterDataForm setReportData={setReportData} setLoading={setLoading} /> <FilterDataForm setReportData={setReportData} setLoading={setLoading} />
{loading ? <Loading /> : <Charts reportData={reportData} />} {loading ? <Loading /> : <Charts reportData={reportData} />}
</div> </div>

View File

@@ -3,29 +3,42 @@ import React from "react";
const Table = ({ tableData }) => { const Table = ({ tableData }) => {
return ( return (
<div className="table-responsive"> <div className="overflow-x-auto">
<table <table className="w-full border-collapse shadow-sm rounded-lg overflow-hidden bg-white dark:bg-[#3c4043] transition-colors duration-200">
style={{ <thead className="bg-gray-100 dark:bg-[#303134]">
width: "100%", <tr className="text-left">
borderCollapse: "collapse", <th className="p-3 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-200 font-semibold">
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)", ID
}} </th>
> <th className="p-3 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-200 font-semibold">
<thead> Stream
<tr style={{ backgroundColor: "#f4f4f4" }}> </th>
<th style={{ padding: "10px", border: "1px solid #ddd" }}>ID</th> <th className="p-3 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-200 font-semibold">
<th style={{ padding: "10px", border: "1px solid #ddd" }}>Stream</th> Scholarship
<th style={{ padding: "10px", border: "1px solid #ddd" }}>Scholarship</th> </th>
<th style={{ padding: "10px", border: "1px solid #ddd" }}>Funds</th> <th className="p-3 border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-200 font-semibold">
Funds
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{tableData?.map((row) => ( {tableData?.map((row) => (
<tr key={row.id}> <tr
<td style={{ padding: "10px", border: "1px solid #ddd" }}>{row.id}</td> key={row.id}
<td style={{ padding: "10px", border: "1px solid #ddd" }}>{row.Stream}</td> className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
<td style={{ padding: "10px", border: "1px solid #ddd" }}>{row.Scholarship}</td> >
<td style={{ padding: "10px", border: "1px solid #ddd" }}>{row.Funds}</td> <td className="p-3 border border-gray-200 dark:border-gray-600 text-gray-800 dark:text-gray-300">
{row.id}
</td>
<td className="p-3 border border-gray-200 dark:border-gray-600 text-gray-800 dark:text-gray-300">
{row.Stream}
</td>
<td className="p-3 border border-gray-200 dark:border-gray-600 text-gray-800 dark:text-gray-300">
{row.Scholarship}
</td>
<td className="p-3 border border-gray-200 dark:border-gray-600 text-gray-800 dark:text-gray-300">
{row.Funds}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -15,7 +15,7 @@ import {
Tooltip, Tooltip,
Legend, Legend,
} from "chart.js"; } from "chart.js";
import ChartDataLabels from 'chartjs-plugin-datalabels'; import ChartDataLabels from "chartjs-plugin-datalabels";
import Table from "./Table"; import Table from "./Table";
import { PDFDownloadLink, PDFViewer } from "@react-pdf/renderer"; import { PDFDownloadLink, PDFViewer } from "@react-pdf/renderer";
import ApprovalVsRejectionTrends from "./map"; import ApprovalVsRejectionTrends from "./map";
@@ -46,19 +46,24 @@ function Charts({ reportData }) {
); );
} }
const { acceptedApplications, rejectedApplications, pendingApplications } = data; const { acceptedApplications, rejectedApplications, pendingApplications } =
data;
const tableData = []; const tableData = [];
const groupedData = {}; const groupedData = {};
// --- Data Processing for Table --- (Simple aggregation logic)
if (acceptedApplications) { if (acceptedApplications) {
for (const item of acceptedApplications) { for (const item of acceptedApplications) {
const { institute, department, formData } = item; const { institute, department, formData } = item;
const { totalExpense } = formData; const { totalExpense } = formData;
// Initialize institute object if not exists
if (!groupedData[institute]) { if (!groupedData[institute]) {
groupedData[institute] = {}; groupedData[institute] = {};
} }
// If filtering by specific institute, we group by Department (e.g. Computer, IT, Mech)
if (query.institute) { if (query.institute) {
if (!groupedData[institute][department]) { if (!groupedData[institute][department]) {
groupedData[institute][department] = { groupedData[institute][department] = {
@@ -67,11 +72,12 @@ function Charts({ reportData }) {
}; };
} }
// Aggregate the data // Add expense and increment count
groupedData[institute][department].totalExpense += groupedData[institute][department].totalExpense +=
parseFloat(totalExpense); // Summing the expenses parseFloat(totalExpense);
groupedData[institute][department].applications += 1; groupedData[institute][department].applications += 1;
} else { } else {
// If viewing all institutes, we group by Institute (e.g. KJSCE, KJSIM)
if (!groupedData[institute].applications) { if (!groupedData[institute].applications) {
groupedData[institute] = { groupedData[institute] = {
totalExpense: 0, totalExpense: 0,
@@ -79,38 +85,40 @@ function Charts({ reportData }) {
}; };
} }
// Aggregate the data // Add expense and increment count
groupedData[institute].totalExpense += parseFloat(totalExpense); // Summing the expenses groupedData[institute].totalExpense += parseFloat(totalExpense);
groupedData[institute].applications += 1; groupedData[institute].applications += 1;
} }
} }
} }
// Step 2: Transform grouped data into desired table format // --- Transform Groups to Array for Display ---
if (query.institute) { if (query.institute) {
// Loop through departments for the selected institute
for (const institute in groupedData) { for (const institute in groupedData) {
for (const department in groupedData[institute]) { for (const department in groupedData[institute]) {
const departmentData = groupedData[institute][department]; const departmentData = groupedData[institute][department];
tableData.push({ tableData.push({
id: tableData.length + 1, id: tableData.length + 1,
Stream: department, Stream: department, // 'Stream' here refers to the Department name
Scholarship: departmentData.applications, // Assuming each application is one scholarship Scholarship: departmentData.applications, // Number of applications
Purpose_of_Travel: departmentData.purposeOfTravel, Purpose_of_Travel: departmentData.purposeOfTravel, // (Placeholder)
Funds: departmentData.totalExpense.toFixed(2), // Formatting funds to 2 decimal places Funds: departmentData.totalExpense.toFixed(2), // Total money spent
}); });
} }
} }
} else { } else {
// Loop through all institutes
for (const institute in groupedData) { for (const institute in groupedData) {
const instituteData = groupedData[institute]; const instituteData = groupedData[institute];
tableData.push({ tableData.push({
id: tableData.length + 1, id: tableData.length + 1,
Stream: institute, Stream: institute, // 'Stream' here is the Institute name
Scholarship: instituteData.applications, // Assuming each application is one scholarship Scholarship: instituteData.applications, // Number of applications
Purpose_of_Travel: instituteData.purposeOfTravel, Purpose_of_Travel: instituteData.purposeOfTravel,
Funds: instituteData.totalExpense.toFixed(2), // Formatting funds to 2 decimal places Funds: instituteData.totalExpense.toFixed(2), // Total money spent
}); });
} }
} }
@@ -122,6 +130,8 @@ function Charts({ reportData }) {
isLoading: false, isLoading: false,
}); });
// --- Chart Configuration (Preserved for future use) ---
// Line Chart Data and Options // Line Chart Data and Options
const lineOptions = { const lineOptions = {
responsive: true, responsive: true,
@@ -281,80 +291,86 @@ function Charts({ reportData }) {
], ],
}; };
// const barChartRef = useRef(); const barChartRef = useRef();
// const pieChartRef1 = useRef(); const pieChartRef1 = useRef();
// const pieChartRef2 = useRef(); const pieChartRef2 = useRef();
// const loadChartsInPdf = () => { // Note: Chart generation logic for PDF is currently disabled as we focus on the Table view.
// const barChartInstance = barChartRef.current; // Uncomment this when we are ready to integrate charts into the PDF report.
// const pieChartInstance1 = pieChartRef1.current; /*
// const pieChartInstance2 = pieChartRef2.current; const loadChartsInPdf = () => {
const barChartInstance = barChartRef.current;
const pieChartInstance1 = pieChartRef1.current;
const pieChartInstance2 = pieChartRef2.current;
// if (barChartInstance) { if (barChartInstance) {
// const barBase64Image = barChartInstance.toBase64Image(); const barBase64Image = barChartInstance.toBase64Image();
// setChartImages((prevImages) => ({ setChartImages((prevImages) => ({
// ...prevImages, ...prevImages,
// barChart: barBase64Image, barChart: barBase64Image,
// })); }));
// } }
// if (pieChartInstance1) { if (pieChartInstance1) {
// const pieBase64Image = pieChartInstance1.toBase64Image(); const pieBase64Image = pieChartInstance1.toBase64Image();
// setChartImages((prevImages) => ({ setChartImages((prevImages) => ({
// ...prevImages, ...prevImages,
// pieChart1: pieBase64Image, pieChart1: pieBase64Image,
// })); }));
// } }
// if (pieChartInstance2) { if (pieChartInstance2) {
// const pieBase64Image = pieChartInstance2.toBase64Image(); const pieBase64Image = pieChartInstance2.toBase64Image();
// setChartImages((prevImages) => ({ setChartImages((prevImages) => ({
// ...prevImages, ...prevImages,
// pieChart2: pieBase64Image, pieChart2: pieBase64Image,
// })); }));
// } }
// }; };
// useEffect(() => { useEffect(() => {
// setChartImages((prevImages) => ({ ...prevImages, isLoading: true })); setChartImages((prevImages) => ({ ...prevImages, isLoading: true }));
// const handleRender = () => { const handleRender = () => {
// loadChartsInPdf(); loadChartsInPdf();
// setChartImages((prevImages) => ({ ...prevImages, isLoading: false })); setChartImages((prevImages) => ({ ...prevImages, isLoading: false }));
// }; };
// const barChartInstance = barChartRef.current; const barChartInstance = barChartRef.current;
// const pieChartInstance1 = pieChartRef1.current; const pieChartInstance1 = pieChartRef1.current;
// const pieChartInstance2 = pieChartRef2.current; const pieChartInstance2 = pieChartRef2.current;
// if (barChartInstance) { if (barChartInstance) {
// barChartInstance.options.animation.onComplete = handleRender; barChartInstance.options.animation.onComplete = handleRender;
// } }
// if (pieChartInstance1) { if (pieChartInstance1) {
// pieChartInstance1.options.animation.onComplete = handleRender; pieChartInstance1.options.animation.onComplete = handleRender;
// } }
// if (pieChartInstance2) { if (pieChartInstance2) {
// pieChartInstance2.options.animation.onComplete = handleRender; pieChartInstance2.options.animation.onComplete = handleRender;
// } }
// return () => { return () => {
// if (barChartInstance) { if (barChartInstance) {
// barChartInstance.options.animation.onComplete = null; barChartInstance.options.animation.onComplete = null;
// } }
// if (pieChartInstance1) { if (pieChartInstance1) {
// pieChartInstance1.options.animation.onComplete = null; pieChartInstance1.options.animation.onComplete = null;
// } }
// if (pieChartInstance2) { if (pieChartInstance2) {
// pieChartInstance2.options.animation.onComplete = null; pieChartInstance2.options.animation.onComplete = null;
// } }
// }; };
// }, []); }, []);
*/
return ( return (
<div className="p-10"> <div className="p-10">
<h1 className="text-3xl mb-6">Travel Policy Report</h1> <h1 className="text-3xl mb-6 text-gray-800 dark:text-google-text">
Travel Policy Report
</h1>
{/* Container for all three charts */} {/* Container for all three charts */}
{/* <div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-[2fr,1fr] gap-6"> {/* <div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-[2fr,1fr] gap-6">
@@ -387,9 +403,11 @@ function Charts({ reportData }) {
<div className="flex flex-col gap-10 items-center justify-center my-10"> <div className="flex flex-col gap-10 items-center justify-center my-10">
<div className="w-full"> <div className="w-full">
{/* Reuse the Table component to show aggregated data */}
<Table tableData={tableData} /> <Table tableData={tableData} />
</div> </div>
{/*
{/*
<div> <div>
<Pie options={pie_Options} data={pie_Data} ref={pieChartRef2} /> <Pie options={pie_Options} data={pie_Data} ref={pieChartRef2} />
</div> */} </div> */}
@@ -428,9 +446,11 @@ function Charts({ reportData }) {
} }
</PDFDownloadLink> </PDFDownloadLink>
<PDFViewer style={{ width: "70vw", height: "100vh" }}> <div className="mt-8 hidden md:block">
<ReportPDF tableData={tableData} chartImages={chartImages} /> <PDFViewer style={{ width: "70vw", height: "100vh" }}>
</PDFViewer> <ReportPDF tableData={tableData} chartImages={chartImages} />
</PDFViewer>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -0,0 +1,122 @@
import React, { useState } from "react";
import { toast } from "react-toastify";
import axios from "axios";
// Settings Change Password Component
const Settings = () => {
const [passwords, setPasswords] = useState({
oldPassword: "",
newPassword: "",
confirmPassword: "",
});
// Handle input changes
const handleChange = (e) => {
setPasswords({ ...passwords, [e.target.name]: e.target.value });
};
// Submit the form to the backend
const handleSubmit = async (e) => {
// Step 1: Prevent the page from reloading
e.preventDefault();
// Step 2: Check if the new password and confirm password are the same
if (passwords.newPassword !== passwords.confirmPassword) {
toast.error("New passwords do not match!");
return;
}
try {
// Step 3: Send the old and new password to the server
const res = await axios.post(
`${import.meta.env.VITE_APP_API_URL}/general/changePassword`,
{
oldPassword: passwords.oldPassword,
newPassword: passwords.newPassword,
},
{ withCredentials: true }
);
// Step 4: If everything is good, show success message
if (res.status === 200) {
toast.success("Password updated successfully!");
// Clear the form inputs
setPasswords({ oldPassword: "", newPassword: "", confirmPassword: "" });
}
} catch (error) {
// Step 5: If there is an error (like wrong old password), show it
console.log(error);
toast.error(error.response?.data?.message || "Failed to update password");
}
};
return (
<div className="p-8 bg-gray-50 dark:bg-google-dark min-h-screen transition-colors duration-200">
<h1 className="text-3xl font-bold text-gray-800 dark:text-google-text mb-6 border-b-2 border-red-700 pb-2 inline-block">
Account Settings
</h1>
<div className="bg-white dark:bg-google-gray p-8 rounded-xl shadow-lg border border-gray-100 dark:border-gray-700 max-w-lg mt-4 mx-auto transition-colors duration-200">
<h2 className="text-2xl font-semibold mb-6 text-red-700 dark:text-red-500 flex items-center">
Change Password
</h2>
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
<div>
<label className="block text-gray-700 dark:text-google-text mb-2 font-medium">
Current Password
</label>
<input
type="password"
name="oldPassword"
value={passwords.oldPassword}
onChange={handleChange}
placeholder="Enter your current password"
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all dark:bg-[#3c4043] dark:text-white"
required
/>
</div>
<div>
<label className="block text-gray-700 dark:text-google-text mb-2 font-medium">
New Password
</label>
<input
type="password"
name="newPassword"
value={passwords.newPassword}
onChange={handleChange}
placeholder="Enter new password"
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all dark:bg-[#3c4043] dark:text-white"
required
/>
</div>
<div>
<label className="block text-gray-700 dark:text-google-text mb-2 font-medium">
Confirm New Password
</label>
<input
type="password"
name="confirmPassword"
value={passwords.confirmPassword}
onChange={handleChange}
placeholder="Confirm new password"
className="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all dark:bg-[#3c4043] dark:text-white"
required
/>
</div>
<button
type="submit"
className="mt-2 bg-red-700 text-white font-bold py-3 px-6 rounded-lg hover:bg-red-800 transition-colors shadow-md hover:shadow-lg transform active:scale-95 duration-200"
>
Update Password
</button>
</form>
</div>
</div>
);
};
export default Settings;

View File

@@ -1,11 +1,16 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { export default {
content: [ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
"./index.html", darkMode: "class",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: { theme: {
extend: {}, extend: {
colors: {
"google-dark": "#202124",
"google-gray": "#303134",
"google-text": "#E8EAED",
"google-text-secondary": "#9AA0A6",
},
},
}, },
plugins: [], plugins: [],
} }