diff --git a/backend/.gitignore b/backend/.gitignore
new file mode 100644
index 0000000..9f62ec0
--- /dev/null
+++ b/backend/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+# Keep environment variables out of version control
+.env
+
+/generated/prisma
diff --git a/backend/config/db.js b/backend/config/db.js
index dfd50b9..811bb3d 100644
--- a/backend/config/db.js
+++ b/backend/config/db.js
@@ -1,69 +1,10 @@
-// const { Pool } = require('pg');
-import { Pool } from 'pg';
-import dotenv from 'dotenv';
-dotenv.config();
+import "dotenv/config";
+import { PrismaPg } from '@prisma/adapter-pg'
+import { PrismaClient } from '../generated/prisma/client.js';
-const pool = new Pool({
- user: process.env.DB_USER,
- host: process.env.DB_HOST,
- database: process.env.DB_NAME,
- password: process.env.DB_PASSWORD,
- port: process.env.DB_PORT,
-});
+const connectionString = `${process.env.DATABASE_URL}`
-const initDB = async () => {
- const client = await pool.connect();
-
- try {
- await client.query(`
- CREATE TABLE IF NOT EXISTS users (
- id SERIAL PRIMARY KEY,
- email VARCHAR(255) UNIQUE NOT NULL,
- password VARCHAR(255) NOT NULL,
- role VARCHAR(50) DEFAULT 'user',
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- )
- `);
- await client.query(`
- CREATE TABLE IF NOT EXISTS events (
- id SERIAL PRIMARY KEY,
- title VARCHAR(255) NOT NULL,
- description TEXT,
- date DATE,
- max_team_size INTEGER DEFAULT 1,
- registration_fee DECIMAL(10, 2),
- qr_code_image VARCHAR(255),
- status VARCHAR(50) DEFAULT 'active',
- created_by INTEGER REFERENCES users(id),
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- )
- `);
+const adapter = new PrismaPg({ connectionString })
+const prisma = new PrismaClient({ adapter })
- await client.query(`
- CREATE TABLE IF NOT EXISTS registrations (
- id SERIAL PRIMARY KEY,
- event_id INTEGER REFERENCES events(id) ON DELETE CASCADE,
- user_id INTEGER REFERENCES users(id),
- team_size INTEGER NOT NULL,
- team_leader_name VARCHAR(255) NOT NULL,
- team_leader_email VARCHAR(255) NOT NULL,
- team_leader_contact VARCHAR(20) NOT NULL,
- team_members JSONB,
- payment_screenshot VARCHAR(255),
- transaction_id VARCHAR(255),
- status VARCHAR(50) DEFAULT 'pending',
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- )
- `);
-
- console.log('Database tables created successfully');
- } catch (error) {
- console.error('Database initialization error:', error);
- throw error;
- } finally {
- client.release();
- }
-};
-
-// module.exports = { pool, initDB };
-export { pool, initDB };
\ No newline at end of file
+export { prisma }
\ No newline at end of file
diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js
index dd98ad9..11a9a36 100644
--- a/backend/controllers/adminController.js
+++ b/backend/controllers/adminController.js
@@ -1,24 +1,27 @@
-import { pool } from '../config/db.js';
+import { prisma } from '../config/db.js';
import bcrypt from 'bcryptjs';
const getDashboardStats = async (req, res) => {
try {
- const usersCount = await pool.query('SELECT COUNT(*) FROM users WHERE role = $1', ['user']);
- const eventsCount = await pool.query('SELECT COUNT(*) FROM events');
- const registrationsCount = await pool.query('SELECT COUNT(*) FROM registrations');
- const pendingCount = await pool.query(
- 'SELECT COUNT(*) FROM registrations WHERE status = $1',
- ['pending']
- );
-
- res.json({
- stats: {
- totalUsers: parseInt(usersCount.rows[0].count),
- totalEvents: parseInt(eventsCount.rows[0].count),
- totalRegistrations: parseInt(registrationsCount.rows[0].count),
- pendingRegistrations: parseInt(pendingCount.rows[0].count)
- }
- });
+ const [usersCount, eventsCount, registrationsCount, pendingCount] = await Promise.all([
+ prisma.user.count({
+ where: { role: 'user' }
+ }),
+ prisma.event.count(),
+ prisma.registration.count(),
+ prisma.registration.count({
+ where: { status: 'pending' }
+ })
+]);
+
+res.json({
+ stats: {
+ totalUsers: usersCount,
+ totalEvents: eventsCount,
+ totalRegistrations: registrationsCount,
+ pendingRegistrations: pendingCount
+ }
+});
} catch (error) {
console.error('Get dashboard stats error:', error);
res.status(500).json({ message: 'Server error' });
@@ -26,11 +29,19 @@ const getDashboardStats = async (req, res) => {
};
const getAllUsers = async (req, res) => {
try {
- const result = await pool.query(
- 'SELECT id, email, role, created_at FROM users ORDER BY created_at DESC'
- );
+ const result = await prisma.user.findMany({
+ select: {
+ id: true,
+ email: true,
+ role: true,
+ createdAt: true
+ },
+ orderBy: {
+ createdAt: 'desc'
+ }
+ });
- res.json({ users: result.rows });
+ res.json({ users: result });
} catch (error) {
console.error('Get all users error:', error);
res.status(500).json({ message: 'Server error' });
@@ -42,27 +53,26 @@ const updateUser = async (req, res) => {
const { id } = req.params;
const { email, role, password } = req.body;
- let query = 'UPDATE users SET email = $1, role = $2';
- let params = [email, role];
-
+ const updateData = {
+ email,
+ role,
+};
if (password) {
- const hashedPassword = await bcrypt.hash(password, 10);
- query += ', password = $3';
- params.push(hashedPassword);
- }
-
- query += ` WHERE id = $${params.length + 1} RETURNING id, email, role, created_at`;
- params.push(id);
-
- const result = await pool.query(query, params);
-
- if (result.rows.length === 0) {
- return res.status(404).json({ message: 'User not found' });
- }
-
+ updateData.password = await bcrypt.hash(password, 10);
+}
+const updatedUser = await prisma.user.update({
+ where: { id: parseInt(id) },
+ data: updateData,
+ select: {
+ id: true,
+ email: true,
+ role: true,
+ createdAt: true,
+ },
+ });
res.json({
message: 'User updated successfully',
- user: result.rows[0]
+ user: updatedUser
});
} catch (error) {
console.error('Update user error:', error);
@@ -74,14 +84,18 @@ const deleteUser = async (req, res) => {
try {
const { id } = req.params;
- const user = await pool.query('SELECT role FROM users WHERE id = $1', [id]);
- if (user.rows.length > 0 && user.rows[0].role === 'admin') {
+ const user = await prisma.user.findUnique({
+ where: { id: parseInt(id) },
+ select: { role: true }
+ });
+ if (user && user.role === 'admin') {
return res.status(403).json({ message: 'Cannot delete admin account' });
}
- const result = await pool.query('DELETE FROM users WHERE id = $1 RETURNING *', [id]);
-
- if (result.rows.length === 0) {
+ const result = await prisma.user.delete({
+ where: { id: parseInt(id) }
+ });
+ if (!result) {
return res.status(404).json({ message: 'User not found' });
}
@@ -96,25 +110,20 @@ const getAllRegistrations = async (req, res) => {
try {
const { eventId } = req.query;
- let query = `
- SELECT r.*, e.title as event_title, e.date as event_date,
- u.email as user_email
- FROM registrations r
- JOIN events e ON r.event_id = e.id
- JOIN users u ON r.user_id = u.id
- `;
-
- const params = [];
- if (eventId) {
- query += ' WHERE r.event_id = $1';
- params.push(eventId);
- }
-
- query += ' ORDER BY r.created_at DESC';
-
- const result = await pool.query(query, params);
+ const registrations = await prisma.registration.findMany({
+ where: eventId ? { eventId: Number(eventId) } : {},
+ orderBy: { createdAt: 'desc' },
+ include: {
+ event: {
+ select: { title: true, date: true }
+ },
+ user: {
+ select: { email: true }
+ }
+ }
+ });
- res.json({ registrations: result.rows });
+ res.json({ registrations: registrations });
} catch (error) {
console.error('Get all registrations error:', error);
res.status(500).json({ message: 'Server error' });
@@ -130,18 +139,18 @@ const updateRegistrationStatus = async (req, res) => {
return res.status(400).json({ message: 'Invalid status' });
}
- const result = await pool.query(
- 'UPDATE registrations SET status = $1 WHERE id = $2 RETURNING *',
- [status, id]
- );
+ const updatedRegistration = await prisma.registration.update({
+ where: { id: parseInt(id) },
+ data: { status: status }
+ });
- if (result.rows.length === 0) {
+ if (!updatedRegistration) {
return res.status(404).json({ message: 'Registration not found' });
}
res.json({
message: 'Registration status updated successfully',
- registration: result.rows[0]
+ registration: updatedRegistration
});
} catch (error) {
console.error('Update registration status error:', error);
@@ -162,22 +171,26 @@ const updateRegistration = async (req, res) => {
status
} = req.body;
- const result = await pool.query(
- `UPDATE registrations
- SET team_size = $1, team_leader_name = $2, team_leader_email = $3,
- team_leader_contact = $4, team_members = $5, transaction_id = $6, status = $7
- WHERE id = $8 RETURNING *`,
- [teamSize, teamLeaderName, teamLeaderEmail, teamLeaderContact,
- teamMembers, transactionId, status, id]
- );
+ const updatedRegistration = await prisma.registration.update({
+ where: { id: parseInt(id) },
+ data: {
+ teamSize,
+ teamLeaderName,
+ teamLeaderEmail,
+ teamLeaderContact,
+ teamMembers,
+ transactionId,
+ status
+ }
+ });
- if (result.rows.length === 0) {
+ if (!updatedRegistration) {
return res.status(404).json({ message: 'Registration not found' });
}
res.json({
message: 'Registration updated successfully',
- registration: result.rows[0]
+ registration: updatedRegistration
});
} catch (error) {
console.error('Update registration error:', error);
@@ -189,9 +202,10 @@ const deleteRegistration = async (req, res) => {
try {
const { id } = req.params;
- const result = await pool.query('DELETE FROM registrations WHERE id = $1 RETURNING *', [id]);
-
- if (result.rows.length === 0) {
+ const deletedRegistration = await prisma.registration.delete({
+ where: { id: parseInt(id) }
+ });
+ if (!deletedRegistration) {
return res.status(404).json({ message: 'Registration not found' });
}
diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js
index e208e3e..4a62d6c 100644
--- a/backend/controllers/authController.js
+++ b/backend/controllers/authController.js
@@ -1,6 +1,6 @@
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
-import { pool } from '../config/db.js';
+import { prisma } from '../config/db.js';
const register = async (req, res) => {
try {
@@ -18,23 +18,23 @@ const register = async (req, res) => {
return res.status(400).json({ message: 'Password must be at least 6 characters long' });
}
- const userExists = await pool.query(
- 'SELECT * FROM users WHERE email = $1',
- [email]
- );
+ const UserExist=await prisma.user.findUnique({
+ where:{email:email}
+ });
- if (userExists.rows.length > 0) {
+ if (UserExist) {
return res.status(400).json({ message: 'User already exists with this email' });
}
const hashedPassword = await bcrypt.hash(password, 10);
- const result = await pool.query(
- 'INSERT INTO users (email, password, role) VALUES ($1, $2, $3) RETURNING id, email, role, created_at',
- [email, hashedPassword, 'user']
- );
-
- const user = result.rows[0];
+ const user = await prisma.user.create({
+ data: {
+ email,
+ password: hashedPassword,
+ role: 'user'
+ }
+ });
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
@@ -66,15 +66,13 @@ const login = async (req, res) => {
return res.status(400).json({ message: 'Email and password are required' });
}
- const result = await pool.query(
- 'SELECT * FROM users WHERE email = $1',
- [email]
- );
+ const user = await prisma.user.findUnique({
+ where: { email: email }
+ });
- if (result.rows.length === 0) {
+ if (!user) {
return res.status(401).json({ message: 'Invalid email or password' });
}
- const user = result.rows[0];
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ message: 'Invalid email or password' });
@@ -102,16 +100,15 @@ const login = async (req, res) => {
const getCurrentUser = async (req, res) => {
try {
- const result = await pool.query(
- 'SELECT id, email, role, created_at FROM users WHERE id = $1',
- [req.user.id]
- );
+ const user = await prisma.user.findUnique({
+ where: { id: req.user.id }
+ });
- if (result.rows.length === 0) {
+ if (!user) {
return res.status(404).json({ message: 'User not found' });
}
- res.json({ user: result.rows[0] });
+ res.json({ user: user });
} catch (error) {
console.error('Get current user error:', error);
res.status(500).json({ message: 'Server error' });
diff --git a/backend/controllers/eventController.js b/backend/controllers/eventController.js
index fe297a0..2d02641 100644
--- a/backend/controllers/eventController.js
+++ b/backend/controllers/eventController.js
@@ -1,15 +1,17 @@
-import { pool } from '../config/db.js';
+import { prisma } from "../config/db.js";
const getAllEvents = async (req, res) => {
try {
- const result = await pool.query(`
- SELECT e.*, u.email as created_by_email
- FROM events e
- LEFT JOIN users u ON e.created_by = u.id
- WHERE e.status = 'active'
- ORDER BY e.created_at DESC
- `);
-
- res.json({ events: result.rows });
+ const events = await prisma.event.findMany({
+ where: { status: 'active' },
+ orderBy: { createdAt: 'desc' },
+ include: {
+ createdBy: {
+ select: { email: true }
+ }
+ }
+ });
+
+ res.json({ events: events });
} catch (error) {
console.error('Get events error:', error);
res.status(500).json({ message: 'Failed to fetch events' });
@@ -19,18 +21,20 @@ const getEventById = async (req, res) => {
try {
const { id } = req.params;
- const result = await pool.query(`
- SELECT e.*, u.email as created_by_email
- FROM events e
- LEFT JOIN users u ON e.created_by = u.id
- WHERE e.id = $1
- `, [id]);
+ const event = await prisma.event.findUnique({
+ where: { id: Number(id) },
+ include: {
+ createdBy: {
+ select: { email: true }
+ }
+ }
+ });
- if (result.rows.length === 0) {
+ if (!event) {
return res.status(404).json({ message: 'Event not found' });
}
- res.json({ event: result.rows[0] });
+ res.json({ event: event });
} catch (error) {
console.error('Get event error:', error);
res.status(500).json({ message: 'Failed to fetch event' });
@@ -47,15 +51,22 @@ const createEvent = async (req, res) => {
return res.status(400).json({ message: 'Title, description, and date are required' });
}
- const result = await pool.query(
- `INSERT INTO events (title, description, date, max_team_size, registration_fee, qr_code_image, created_by)
- VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`,
- [title, description, date, maxTeamSize || 1, registrationFee || 0, qrCodeImage, req.user.id]
+ const event = await prisma.event.create({
+ data: {
+ title,
+ description,
+ date:date ? new Date(date) : null,
+ max_team_size : maxTeamSize || 1,
+ registration_fee: registrationFee || 0,
+ qr_code_image: qrCodeImage,
+ createdBy: { connect: { id: req.user.id } }
+ }
+ }
);
res.status(201).json({
message: 'Event created successfully',
- event: result.rows[0]
+ event: event
});
} catch (error) {
console.error('Create event error:', error);
@@ -68,21 +79,26 @@ const updateEvent = async (req, res) => {
const { title, description, date, maxTeamSize, registrationFee, status } = req.body;
const qrCodeImage = req.file ? req.file.path : req.body.existingQrCode;
- const result = await pool.query(
- `UPDATE events
- SET title = $1, description = $2, date = $3, max_team_size = $4,
- registration_fee = $5, qr_code_image = $6, status = $7
- WHERE id = $8 RETURNING *`,
- [title, description, date, maxTeamSize, registrationFee, qrCodeImage, status || 'active', id]
- );
+ const result = await prisma.event.update({
+ where: { id: Number(id) },
+ data: {
+ title,
+ description,
+ date,
+ maxTeamSize,
+ registrationFee,
+ qrCodeImage,
+ status: status || 'active'
+ }
+ })
- if (result.rows.length === 0) {
+ if (!result) {
return res.status(404).json({ message: 'Event not found' });
}
res.json({
message: 'Event updated successfully',
- event: result.rows[0]
+ event: result
});
} catch (error) {
console.error('Update event error:', error);
@@ -93,9 +109,11 @@ const deleteEvent = async (req, res) => {
try {
const { id } = req.params;
- const result = await pool.query('DELETE FROM events WHERE id = $1 RETURNING *', [id]);
+ const result = await prisma.event.delete({
+ where: { id: Number(id) }
+ });
- if (result.rows.length === 0) {
+ if (!result) {
return res.status(404).json({ message: 'Event not found' });
}
diff --git a/backend/controllers/registrationController.js b/backend/controllers/registrationController.js
index 778dd8f..c196517 100644
--- a/backend/controllers/registrationController.js
+++ b/backend/controllers/registrationController.js
@@ -1,4 +1,4 @@
-import { pool } from '../config/db.js';
+import { prisma } from "../config/db.js";
const createRegistration = async (req, res) => {
try {
const {
@@ -34,13 +34,15 @@ const createRegistration = async (req, res) => {
}
}
}
- const eventCheck = await pool.query('SELECT * FROM events WHERE id = $1', [eventId]);
- if (eventCheck.rows.length === 0) {
+ const eventCheck = await prisma.event.findUnique({
+ where: { id: Number(eventId) }
+ });
+ if (!eventCheck) {
return res.status(404).json({ message: 'Event not found' });
}
- const event = eventCheck.rows[0];
- if (event.registration_fee > 0) {
+ const event = eventCheck;
+ if (event.registrationFee > 0) {
if (!paymentScreenshot) {
return res.status(400).json({ message: 'Payment screenshot is required for this event' });
}
@@ -48,36 +50,35 @@ const createRegistration = async (req, res) => {
return res.status(400).json({ message: 'Transaction ID is required' });
}
}
- const existingReg = await pool.query(
- 'SELECT * FROM registrations WHERE event_id = $1 AND user_id = $2',
- [eventId, req.user.id]
- );
+ const existingReg = await prisma.registration.findFirst({
+ where: {
+ event_id: Number(eventId),
+ user_id: req.user.id
+ }
+ });
- if (existingReg.rows.length > 0) {
+ if (existingReg) {
return res.status(400).json({ message: 'You have already registered for this event' });
}
- const result = await pool.query(
- `INSERT INTO registrations
- (event_id, user_id, team_size, team_leader_name, team_leader_email,
- team_leader_contact, team_members, payment_screenshot, transaction_id, status)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`,
- [
- eventId,
- req.user.id,
- teamSize,
- teamLeaderName,
- teamLeaderEmail,
- teamLeaderContact,
- teamMembers || null,
- paymentScreenshot,
- transactionId || null,
- 'pending'
- ]
- );
+ const result = await prisma.registration.create({
+ data: {
+ event_id: Number(eventId),
+ user_id: req.user.id,
+ team_size: teamSize,
+ team_leader_name: teamLeaderName,
+ team_leader_email: teamLeaderEmail,
+ team_leader_contact: teamLeaderContact,
+ team_members: teamMembers || null,
+ payment_screenshot: paymentScreenshot,
+ transaction_id: transactionId || null,
+ status: 'pending'
+ }
+ });
+
res.status(201).json({
message: 'Registration submitted successfully! Await admin approval.',
- registration: result.rows[0]
+ registration: result
});
} catch (error) {
console.error('Create registration error:', error);
@@ -86,16 +87,17 @@ const createRegistration = async (req, res) => {
};
const getUserRegistrations = async (req, res) => {
try {
- const result = await pool.query(
- `SELECT r.*, e.title as event_title, e.date as event_date
- FROM registrations r
- JOIN events e ON r.event_id = e.id
- WHERE r.user_id = $1
- ORDER BY r.created_at DESC`,
- [req.user.id]
- );
+ const result = await prisma.registration.findMany({
+ where: { user_id: req.user.id },
+ orderBy: { createdAt: 'desc' },
+ include: {
+ event: {
+ select: { title: true, date: true }
+ }
+ }
+ });
- res.json({ registrations: result.rows });
+ res.json({ registrations: result });
} catch (error) {
console.error('Get user registrations error:', error);
res.status(500).json({ message: 'Failed to fetch registrations' });
@@ -104,24 +106,26 @@ const getUserRegistrations = async (req, res) => {
const getRegistrationById = async (req, res) => {
try {
const { id } = req.params;
-
- const result = await pool.query(
- `SELECT r.*, e.title as event_title, e.date as event_date, u.email as user_email
- FROM registrations r
- JOIN events e ON r.event_id = e.id
- JOIN users u ON r.user_id = u.id
- WHERE r.id = $1`,
- [id]
- );
+ const result = await prisma.registration.findUnique({
+ where: { id: Number(id) },
+ include: {
+ event: {
+ select: { title: true, date: true }
+ },
+ user: {
+ select: { email: true }
+ }
+ }
+ });
- if (result.rows.length === 0) {
+ if (!result) {
return res.status(404).json({ message: 'Registration not found' });
}
- if (result.rows[0].user_id !== req.user.id && req.user.role !== 'admin') {
+ if (result.user_id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ message: 'Access denied' });
}
- res.json({ registration: result.rows[0] });
+ res.json({ registration: result });
} catch (error) {
console.error('Get registration error:', error);
res.status(500).json({ message: 'Failed to fetch registration' });
diff --git a/backend/middleware/rateLimiter.js b/backend/middleware/rateLimiter.js
new file mode 100644
index 0000000..e69de29
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 932e1cf..76a11ab 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -18,10 +18,10 @@
"jsonwebtoken": "^9.0.3",
"multer": "^1.4.5-lts.1",
"multer-storage-cloudinary": "^4.0.0",
- "pg": "^8.17.2"
+ "pg": "^8.18.0"
},
"devDependencies": {
- "@types/node": "^25.0.10",
+ "@types/node": "^25.1.0",
"@types/pg": "^8.16.0",
"nodemon": "^3.0.1",
"prisma": "^7.3.0"
@@ -336,9 +336,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.0.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz",
- "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
+ "version": "25.1.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz",
+ "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1969,12 +1969,12 @@
"license": "MIT"
},
"node_modules/pg": {
- "version": "8.17.2",
- "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz",
- "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
+ "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
"license": "MIT",
"dependencies": {
- "pg-connection-string": "^2.10.1",
+ "pg-connection-string": "^2.11.0",
"pg-pool": "^3.11.0",
"pg-protocol": "^1.11.0",
"pg-types": "2.2.0",
@@ -2003,9 +2003,9 @@
"optional": true
},
"node_modules/pg-connection-string": {
- "version": "2.10.1",
- "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz",
- "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==",
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz",
+ "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==",
"license": "MIT"
},
"node_modules/pg-int8": {
diff --git a/backend/package.json b/backend/package.json
index fe876e4..c38d3fa 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -19,10 +19,10 @@
"jsonwebtoken": "^9.0.3",
"multer": "^1.4.5-lts.1",
"multer-storage-cloudinary": "^4.0.0",
- "pg": "^8.17.2"
+ "pg": "^8.18.0"
},
"devDependencies": {
- "@types/node": "^25.0.10",
+ "@types/node": "^25.1.0",
"@types/pg": "^8.16.0",
"nodemon": "^3.0.1",
"prisma": "^7.3.0"
diff --git a/backend/prisma.config.ts b/backend/prisma.config.ts
new file mode 100644
index 0000000..831a20f
--- /dev/null
+++ b/backend/prisma.config.ts
@@ -0,0 +1,14 @@
+// This file was generated by Prisma, and assumes you have installed the following:
+// npm install --save-dev prisma dotenv
+import "dotenv/config";
+import { defineConfig } from "prisma/config";
+
+export default defineConfig({
+ schema: "prisma/schema.prisma",
+ migrations: {
+ path: "prisma/migrations",
+ },
+ datasource: {
+ url: process.env["DATABASE_URL"],
+ },
+});
diff --git a/backend/prisma/migrations/20260131175617_init/migration.sql b/backend/prisma/migrations/20260131175617_init/migration.sql
new file mode 100644
index 0000000..40257ff
--- /dev/null
+++ b/backend/prisma/migrations/20260131175617_init/migration.sql
@@ -0,0 +1,61 @@
+-- CreateEnum
+CREATE TYPE "Role" AS ENUM ('user', 'admin');
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" SERIAL NOT NULL,
+ "email" TEXT NOT NULL,
+ "password" TEXT NOT NULL,
+ "role" "Role" NOT NULL DEFAULT 'user',
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Event" (
+ "id" SERIAL NOT NULL,
+ "title" TEXT NOT NULL,
+ "description" TEXT,
+ "date" DATE,
+ "max_team_size" INTEGER NOT NULL DEFAULT 1,
+ "registration_fee" DECIMAL(65,30),
+ "qr_code_image" TEXT,
+ "status" TEXT NOT NULL DEFAULT 'active',
+ "createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(6) NOT NULL,
+ "createdById" INTEGER,
+
+ CONSTRAINT "Event_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Registration" (
+ "id" SERIAL NOT NULL,
+ "event_id" INTEGER NOT NULL,
+ "user_id" INTEGER,
+ "team_size" INTEGER NOT NULL,
+ "team_leader_name" VARCHAR(255) NOT NULL,
+ "team_leader_email" VARCHAR(255) NOT NULL,
+ "team_leader_contact" VARCHAR(20) NOT NULL,
+ "team_members" JSONB,
+ "payment_screenshot" VARCHAR(255),
+ "transaction_id" VARCHAR(255),
+ "status" VARCHAR(50) NOT NULL DEFAULT 'pending',
+ "createdAt" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "Registration_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- AddForeignKey
+ALTER TABLE "Event" ADD CONSTRAINT "Event_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Registration" ADD CONSTRAINT "Registration_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Registration" ADD CONSTRAINT "Registration_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/backend/prisma/migrations/20260131183656_init/migration.sql b/backend/prisma/migrations/20260131183656_init/migration.sql
new file mode 100644
index 0000000..5d44858
--- /dev/null
+++ b/backend/prisma/migrations/20260131183656_init/migration.sql
@@ -0,0 +1,4 @@
+-- AlterTable
+ALTER TABLE "Event" ALTER COLUMN "max_team_size" SET DEFAULT '1',
+ALTER COLUMN "max_team_size" SET DATA TYPE TEXT,
+ALTER COLUMN "registration_fee" SET DATA TYPE TEXT;
diff --git a/backend/prisma/migrations/20260131184207_init/migration.sql b/backend/prisma/migrations/20260131184207_init/migration.sql
new file mode 100644
index 0000000..ae31b9c
--- /dev/null
+++ b/backend/prisma/migrations/20260131184207_init/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Registration" ALTER COLUMN "team_size" SET DATA TYPE TEXT;
diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..044d57c
--- /dev/null
+++ b/backend/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (e.g., Git)
+provider = "postgresql"
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
new file mode 100644
index 0000000..b0c0e98
--- /dev/null
+++ b/backend/prisma/schema.prisma
@@ -0,0 +1,69 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
+// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
+
+generator client {
+ provider = "prisma-client-js"
+ output = "../generated/prisma"
+}
+
+datasource db {
+ provider = "postgresql"
+}
+
+
+
+
+model User{
+ id Int @id @default(autoincrement())
+ email String @unique
+ password String
+ role Role @default(user)
+ events Event[]
+ registrations Registration[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+
+
+enum Role {
+ user
+ admin
+}
+
+model Event{
+ id Int @id @default(autoincrement())
+ title String
+ description String?
+ date DateTime? @db.Date
+ max_team_size String @default("1")
+ registration_fee String?
+ qr_code_image String?
+ status String @default("active")
+ registrations Registration[]
+ createdAt DateTime @default(now()) @db.Timestamp(6)
+ updatedAt DateTime @updatedAt @db.Timestamp(6)
+ createdById Int?
+ createdBy User? @relation(fields: [createdById], references: [id])
+}
+
+
+model Registration {
+ id Int @id @default(autoincrement())
+ event_id Int
+ user_id Int?
+ team_size String
+ team_leader_name String @db.VarChar(255)
+ team_leader_email String @db.VarChar(255)
+ team_leader_contact String @db.VarChar(20)
+ team_members Json? @db.JsonB
+ payment_screenshot String? @db.VarChar(255)
+ transaction_id String? @db.VarChar(255)
+ status String @default("pending") @db.VarChar(50)
+ createdAt DateTime @default(now()) @db.Timestamp(6)
+ event Event @relation(fields: [event_id], references: [id], onDelete: Cascade)
+ user User? @relation(fields: [user_id], references: [id])
+
+}
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
index 92356f5..bfc083b 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -7,11 +7,12 @@ import dotenv from 'dotenv';
dotenv.config();
-import { pool, initDB } from './config/db.js';
+
import authRoutes from './routes/authRoutes.js';
import eventRoutes from './routes/eventRoutes.js';
import registrationRoutes from './routes/registrationRoutes.js';
import adminRoutes from './routes/adminRoutes.js';
+import { prisma } from './config/db.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -44,21 +45,22 @@ app.use((err, req, res, next) => {
const initializeApp = async () => {
try {
- await initDB();
- const adminExists = await pool.query(
- 'SELECT * FROM users WHERE email = $1',
- [process.env.ADMIN_EMAIL || 'abesec.codechef@gmail.com']
- );
+ const adminExists = await prisma.user.findUnique({
+ where: { email: process.env.ADMIN_EMAIL || 'abesec.codechef@gmail.com' }
+ });
- if (adminExists.rows.length === 0) {
+ if (!adminExists) {
const hashedPassword = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10);
- await pool.query(
- 'INSERT INTO users (email, password, role) VALUES ($1, $2, $3)',
- [process.env.ADMIN_EMAIL || 'abesec.codechef@gmail.com', hashedPassword, 'admin']
- );
+ await prisma.user.create({
+ data: {
+ email: process.env.ADMIN_EMAIL || 'abesec.codechef@gmail.com',
+ password: hashedPassword,
+ role: 'admin'
+ }
+ });
console.log(' Default admin user created');
- console.log(' Email:', process.env.ADMIN_EMAIL);
+ console.log(' Email:', process.env.ADMIN_EMAIL || 'abesec.codechef@gmail.com');
console.log(' Password:', process.env.ADMIN_PASSWORD);
}
app.listen(PORT, () => {
diff --git a/package-lock.json b/package-lock.json
index 78455e5..69e6e94 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,10 +14,10 @@
"@studio-freight/lenis": "^1.0.42",
"@tabler/icons-react": "^3.35.0",
"@tailwindcss/vite": "^4.1.17",
- "axios": "^1.13.3",
+ "axios": "^1.13.4",
"clsx": "^2.1.1",
"face-api.js": "^0.22.2",
- "framer-motion": "^12.23.24",
+ "framer-motion": "^12.31.0",
"gsap": "^3.13.0",
"lenis": "^1.3.14",
"lucide-react": "^0.553.0",
@@ -26,6 +26,7 @@
"postprocessing": "^6.38.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
+ "react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.9.5",
"react-scroll": "^1.9.3",
@@ -40,6 +41,7 @@
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
+ "baseline-browser-mapping": "^2.9.19",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
@@ -2030,9 +2032,9 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.13.3",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.3.tgz",
- "integrity": "sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==",
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
+ "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -2068,9 +2070,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.23",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz",
- "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==",
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -2863,13 +2865,13 @@
}
},
"node_modules/framer-motion": {
- "version": "12.23.24",
- "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
- "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
+ "version": "12.31.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.31.0.tgz",
+ "integrity": "sha512-Tnd0FU05zGRFI3JJmBegXonF1rfuzYeuXd1QSdQ99Ysnppk0yWBWSW2wUsqzRpS5nv0zPNx+y0wtDj4kf0q5RQ==",
"license": "MIT",
"dependencies": {
- "motion-dom": "^12.23.23",
- "motion-utils": "^12.23.6",
+ "motion-dom": "^12.30.1",
+ "motion-utils": "^12.29.2",
"tslib": "^2.4.0"
},
"peerDependencies": {
@@ -2991,6 +2993,15 @@
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
"license": "MIT"
},
+ "node_modules/goober": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
+ "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -3742,18 +3753,18 @@
}
},
"node_modules/motion-dom": {
- "version": "12.23.23",
- "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
- "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
+ "version": "12.30.1",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.30.1.tgz",
+ "integrity": "sha512-QXB+iFJRzZTqL+Am4a1CRoHdH+0Nq12wLdqQQZZsfHlp9AMt6PA098L/61oVZsDA+Ep3QSGudzpViyRrhYhGcQ==",
"license": "MIT",
"dependencies": {
- "motion-utils": "^12.23.6"
+ "motion-utils": "^12.29.2"
}
},
"node_modules/motion-utils": {
- "version": "12.23.6",
- "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
- "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
+ "version": "12.29.2",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
+ "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
"license": "MIT"
},
"node_modules/ms": {
@@ -4043,6 +4054,23 @@
"react": "^19.2.0"
}
},
+ "node_modules/react-hot-toast": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
+ "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.3",
+ "goober": "^2.1.16"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
diff --git a/package.json b/package.json
index 58ff3c6..17865f6 100644
--- a/package.json
+++ b/package.json
@@ -16,10 +16,10 @@
"@studio-freight/lenis": "^1.0.42",
"@tabler/icons-react": "^3.35.0",
"@tailwindcss/vite": "^4.1.17",
- "axios": "^1.13.3",
+ "axios": "^1.13.4",
"clsx": "^2.1.1",
"face-api.js": "^0.22.2",
- "framer-motion": "^12.23.24",
+ "framer-motion": "^12.31.0",
"gsap": "^3.13.0",
"lenis": "^1.3.14",
"lucide-react": "^0.553.0",
@@ -28,6 +28,7 @@
"postprocessing": "^6.38.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
+ "react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.9.5",
"react-scroll": "^1.9.3",
@@ -42,6 +43,7 @@
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
+ "baseline-browser-mapping": "^2.9.19",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.22",
diff --git a/public/wheel/DialWheel.svg b/public/wheel/DialWheel.svg
new file mode 100644
index 0000000..2b78927
--- /dev/null
+++ b/public/wheel/DialWheel.svg
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/wheel/hand.svg b/public/wheel/hand.svg
new file mode 100644
index 0000000..d5cb125
--- /dev/null
+++ b/public/wheel/hand.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/wheel/hand_left.svg b/public/wheel/hand_left.svg
new file mode 100644
index 0000000..418c6ed
--- /dev/null
+++ b/public/wheel/hand_left.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/wheel/hands_line.svg b/public/wheel/hands_line.svg
new file mode 100644
index 0000000..a83c03a
--- /dev/null
+++ b/public/wheel/hands_line.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/wheel/rays.svg b/public/wheel/rays.svg
new file mode 100644
index 0000000..09e91fe
--- /dev/null
+++ b/public/wheel/rays.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/App.css b/src/App.css
index 032f33d..509afa5 100644
--- a/src/App.css
+++ b/src/App.css
@@ -4,76 +4,24 @@
font-weight: 100 900;
font-display: swap;
}
+
@font-face {
font-family: "Technor";
src: url("./assets/fonts/Technor-Variable.ttf") format("truetype");
font-weight: 100 900;
font-display: swap;
}
-.offering-container{
-
- background: #000000;
- overflow-x: hidden;
-}
-.offering-main{
- padding-top: 1rem;
-}
-.offering-main h2{
- font-size: 4rem;
- text-align: center;
-font-family: 'Panchang', sans-serif;
-
-}
-.offering-main p{
- font-size: 1.2rem;
- text-align: center;
- margin-top: 1rem;
-}
-
-.offering-main span{
-text-transform: uppercase;
- font-weight: 800;
+@keyframes spin-slow {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
}
-.cards-container{
- width: 20rem;
- padding-bottom: 2rem;
- padding: 1rem;
-}
-.cards{
- background: transparent !important;
- padding: 3rem ;
-border: 1px solid rgba(255, 255, 255, 0.2);
- border-radius: 1rem;
- backdrop-filter: blur(10px);
-}
-.cards h3{
- font-family: 'Technor',sans-serif;
- font-size: 1.2rem;
-}
-.cards span{
- font-size: 0.8rem;
-}
-@media (max-width:480px) {
- .cards-container{
- padding: 5rem;
- gap: 3rem;
- }
- .cards{
- padding: 2rem;
- gap: 1rem;
- }
- .offering-main{
- padding-top: 5rem;
- padding: 1.8rem;
- }
- .offering-main h2{
- font-size: 2.3rem;
- }
- .offering-main p{
- font-size: 1rem;
- margin-top: 1rem;
- }
+.animate-spin-slow {
+ animation: spin-slow 20s linear infinite;
}
\ No newline at end of file
diff --git a/src/components/Achievements.jsx b/src/components/Achievements.jsx
index 7005e0c..6cb5907 100644
--- a/src/components/Achievements.jsx
+++ b/src/components/Achievements.jsx
@@ -153,26 +153,92 @@ const AchievementsSection = () => {
];
return (
-
+
- {/* Responsive Hero Section */}
-
-
-
-
- Celebrating {' '}
- Our
-
-
- Remarkable {' '}
- Achievements
-
-
+ {/* Hero Section */}
+
+ {/* Subtle background pattern */}
+
+
+
+ {/* Heading */}
+
+
+
+ Remarkable {' '}
+ Achievements
+
+
+ Celebrating {' '}
+ Excellence
+
+
+
+
+ {/* Achievement Cards */}
+
+ {/* Card 1 - Trophy */}
+
+
+
+
+
+
+
+ Excellence in Innovation
+
+
+ Recognizing outstanding contributions to technology and creative problem-solving
+
+
+
+
+
+ {/* Card 2 - Medal (Hidden on mobile) */}
+
+
+
+
+
+ Team Excellence
+
+
+ Celebrating collaborative success and collective achievements
+
+
+
+
+
-
+
{achievements.map((achievement, index) => (
{
};
return (
-
-
-
-
+
+
+
+
-
-
-
{achievement.badge}
+
+ {achievement.badge}
-
+
{achievement.name}
-
+
{achievement.description}
@@ -263,7 +329,7 @@ const AchievementCard = ({ achievement }) => {
href={achievement.github}
target="_blank"
rel="noopener noreferrer"
- className="group flex items-center justify-center w-10 h-10 sm:w-12 sm:h-12 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition-all duration-300 hover:scale-110"
+ className="group flex items-center justify-center w-10 h-10 sm:w-12 sm:h-12 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-all duration-300 hover:scale-110"
title="GitHub"
>
diff --git a/src/components/BackgroundOverlayCard.jsx b/src/components/BackgroundOverlayCard.jsx
index a7f7106..9dfa8bc 100644
--- a/src/components/BackgroundOverlayCard.jsx
+++ b/src/components/BackgroundOverlayCard.jsx
@@ -12,7 +12,7 @@ export const BackgroundOverlayCard = ({
diff --git a/src/components/CircularGallery.jsx b/src/components/CircularGallery.jsx
new file mode 100644
index 0000000..6a4b849
--- /dev/null
+++ b/src/components/CircularGallery.jsx
@@ -0,0 +1,475 @@
+import { Camera, Mesh, Plane, Program, Renderer, Texture, Transform } from 'ogl';
+import { useEffect, useRef } from 'react';
+
+function debounce(func, wait) {
+ let timeout;
+ return function (...args) {
+ clearTimeout(timeout);
+ timeout = setTimeout(() => func.apply(this, args), wait);
+ };
+}
+
+function lerp(p1, p2, t) {
+ return p1 + (p2 - p1) * t;
+}
+
+function autoBind(instance) {
+ const proto = Object.getPrototypeOf(instance);
+ Object.getOwnPropertyNames(proto).forEach(key => {
+ if (key !== 'constructor' && typeof instance[key] === 'function') {
+ instance[key] = instance[key].bind(instance);
+ }
+ });
+}
+
+function createTextTexture(gl, text, font = 'bold 30px monospace', color = 'black') {
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ context.font = font;
+ const metrics = context.measureText(text);
+ const textWidth = Math.ceil(metrics.width);
+ const textHeight = Math.ceil(parseInt(font, 10) * 1.2);
+ canvas.width = textWidth + 20;
+ canvas.height = textHeight + 20;
+ context.font = font;
+ context.fillStyle = color;
+ context.textBaseline = 'middle';
+ context.textAlign = 'center';
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ context.fillText(text, canvas.width / 2, canvas.height / 2);
+ const texture = new Texture(gl, { generateMipmaps: false });
+ texture.image = canvas;
+ return { texture, width: canvas.width, height: canvas.height };
+}
+
+class Title {
+ constructor({ gl, plane, renderer, text, textColor = '#545050', font = '30px sans-serif' }) {
+ autoBind(this);
+ this.gl = gl;
+ this.plane = plane;
+ this.renderer = renderer;
+ this.text = text;
+ this.textColor = textColor;
+ this.font = font;
+ this.createMesh();
+ }
+ createMesh() {
+ const { texture, width, height } = createTextTexture(this.gl, this.text, this.font, this.textColor);
+ const geometry = new Plane(this.gl);
+ const program = new Program(this.gl, {
+ vertex: `
+ attribute vec3 position;
+ attribute vec2 uv;
+ uniform mat4 modelViewMatrix;
+ uniform mat4 projectionMatrix;
+ varying vec2 vUv;
+ void main() {
+ vUv = uv;
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+ }
+ `,
+ fragment: `
+ precision highp float;
+ uniform sampler2D tMap;
+ varying vec2 vUv;
+ void main() {
+ vec4 color = texture2D(tMap, vUv);
+ if (color.a < 0.1) discard;
+ gl_FragColor = color;
+ }
+ `,
+ uniforms: { tMap: { value: texture } },
+ transparent: true
+ });
+ this.mesh = new Mesh(this.gl, { geometry, program });
+ const aspect = width / height;
+ const textHeight = this.plane.scale.y * 0.15;
+ const textWidth = textHeight * aspect;
+ this.mesh.scale.set(textWidth, textHeight, 1);
+ this.mesh.position.y = -this.plane.scale.y * 0.5 - textHeight * 0.5 - 0.05;
+ this.mesh.setParent(this.plane);
+ }
+}
+
+class Media {
+ constructor({
+ geometry,
+ gl,
+ image,
+ index,
+ length,
+ renderer,
+ scene,
+ screen,
+ text,
+ viewport,
+ bend,
+ textColor,
+ borderRadius = 0,
+ font
+ }) {
+ this.extra = 0;
+ this.geometry = geometry;
+ this.gl = gl;
+ this.image = image;
+ this.index = index;
+ this.length = length;
+ this.renderer = renderer;
+ this.scene = scene;
+ this.screen = screen;
+ this.text = text;
+ this.viewport = viewport;
+ this.bend = bend;
+ this.textColor = textColor;
+ this.borderRadius = borderRadius;
+ this.font = font;
+ this.createShader();
+ this.createMesh();
+ this.createTitle();
+ this.onResize();
+ }
+ createShader() {
+ const texture = new Texture(this.gl, {
+ generateMipmaps: true
+ });
+ this.program = new Program(this.gl, {
+ depthTest: false,
+ depthWrite: false,
+ vertex: `
+ precision highp float;
+ attribute vec3 position;
+ attribute vec2 uv;
+ uniform mat4 modelViewMatrix;
+ uniform mat4 projectionMatrix;
+ uniform float uTime;
+ uniform float uSpeed;
+ varying vec2 vUv;
+ void main() {
+ vUv = uv;
+ vec3 p = position;
+ p.z = (sin(p.x * 4.0 + uTime) * 1.5 + cos(p.y * 2.0 + uTime) * 1.5) * (0.1 + uSpeed * 0.5);
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
+ }
+ `,
+ fragment: `
+ precision highp float;
+ uniform vec2 uImageSizes;
+ uniform vec2 uPlaneSizes;
+ uniform sampler2D tMap;
+ uniform float uBorderRadius;
+ varying vec2 vUv;
+
+ float roundedBoxSDF(vec2 p, vec2 b, float r) {
+ vec2 d = abs(p) - b;
+ return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - r;
+ }
+
+ void main() {
+ vec2 ratio = vec2(
+ min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),
+ min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)
+ );
+ vec2 uv = vec2(
+ vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
+ vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
+ );
+ vec4 color = texture2D(tMap, uv);
+
+ float d = roundedBoxSDF(vUv - 0.5, vec2(0.5 - uBorderRadius), uBorderRadius);
+
+ // Smooth antialiasing for edges
+ float edgeSmooth = 0.002;
+ float alpha = 1.0 - smoothstep(-edgeSmooth, edgeSmooth, d);
+
+ gl_FragColor = vec4(color.rgb, alpha);
+ }
+ `,
+ uniforms: {
+ tMap: { value: texture },
+ uPlaneSizes: { value: [0, 0] },
+ uImageSizes: { value: [0, 0] },
+ uSpeed: { value: 0 },
+ uTime: { value: 100 * Math.random() },
+ uBorderRadius: { value: this.borderRadius }
+ },
+ transparent: true
+ });
+ const img = new Image();
+ img.crossOrigin = 'anonymous';
+ img.src = this.image;
+ img.onload = () => {
+ texture.image = img;
+ this.program.uniforms.uImageSizes.value = [img.naturalWidth, img.naturalHeight];
+ };
+ }
+ createMesh() {
+ this.plane = new Mesh(this.gl, {
+ geometry: this.geometry,
+ program: this.program
+ });
+ this.plane.setParent(this.scene);
+ }
+ createTitle() {
+ this.title = new Title({
+ gl: this.gl,
+ plane: this.plane,
+ renderer: this.renderer,
+ text: this.text,
+ textColor: this.textColor,
+ fontFamily: this.font
+ });
+ }
+ update(scroll, direction) {
+ this.plane.position.x = this.x - scroll.current - this.extra;
+
+ const x = this.plane.position.x;
+ const H = this.viewport.width / 2;
+
+ if (this.bend === 0) {
+ this.plane.position.y = 0;
+ this.plane.rotation.z = 0;
+ } else {
+ const B_abs = Math.abs(this.bend);
+ const R = (H * H + B_abs * B_abs) / (2 * B_abs);
+ const effectiveX = Math.min(Math.abs(x), H);
+
+ const arc = R - Math.sqrt(R * R - effectiveX * effectiveX);
+ if (this.bend > 0) {
+ this.plane.position.y = -arc;
+ this.plane.rotation.z = -Math.sign(x) * Math.asin(effectiveX / R);
+ } else {
+ this.plane.position.y = arc;
+ this.plane.rotation.z = Math.sign(x) * Math.asin(effectiveX / R);
+ }
+ }
+
+ this.speed = scroll.current - scroll.last;
+ this.program.uniforms.uTime.value += 0.04;
+ this.program.uniforms.uSpeed.value = this.speed;
+
+ const planeOffset = this.plane.scale.x / 2;
+ const viewportOffset = this.viewport.width / 2;
+ this.isBefore = this.plane.position.x + planeOffset < -viewportOffset;
+ this.isAfter = this.plane.position.x - planeOffset > viewportOffset;
+ if (direction === 'right' && this.isBefore) {
+ this.extra -= this.widthTotal;
+ this.isBefore = this.isAfter = false;
+ }
+ if (direction === 'left' && this.isAfter) {
+ this.extra += this.widthTotal;
+ this.isBefore = this.isAfter = false;
+ }
+ }
+ onResize({ screen, viewport } = {}) {
+ if (screen) this.screen = screen;
+ if (viewport) {
+ this.viewport = viewport;
+ if (this.plane.program.uniforms.uViewportSizes) {
+ this.plane.program.uniforms.uViewportSizes.value = [this.viewport.width, this.viewport.height];
+ }
+ }
+ this.scale = this.screen.height / 1500;
+ this.plane.scale.y = (this.viewport.height * (900 * this.scale)) / this.screen.height;
+ this.plane.scale.x = (this.viewport.width * (700 * this.scale)) / this.screen.width;
+ this.plane.program.uniforms.uPlaneSizes.value = [this.plane.scale.x, this.plane.scale.y];
+ this.padding = 2;
+ this.width = this.plane.scale.x + this.padding;
+ this.widthTotal = this.width * this.length;
+ this.x = this.width * this.index;
+ }
+}
+
+class App {
+ constructor(
+ container,
+ {
+ items,
+ bend,
+ textColor = '#ffffff',
+ borderRadius = 0,
+ font = 'bold 30px Figtree',
+ scrollSpeed = 2,
+ scrollEase = 0.05
+ } = {}
+ ) {
+ document.documentElement.classList.remove('no-js');
+ this.container = container;
+ this.scrollSpeed = scrollSpeed;
+ this.scroll = { ease: scrollEase, current: 0, target: 0, last: 0 };
+ this.onCheckDebounce = debounce(this.onCheck, 200);
+ this.createRenderer();
+ this.createCamera();
+ this.createScene();
+ this.onResize();
+ this.createGeometry();
+ this.createMedias(items, bend, textColor, borderRadius, font);
+ this.update();
+ this.addEventListeners();
+ }
+ createRenderer() {
+ this.renderer = new Renderer({
+ alpha: true,
+ antialias: true,
+ dpr: Math.min(window.devicePixelRatio || 1, 2)
+ });
+ this.gl = this.renderer.gl;
+ this.gl.clearColor(0, 0, 0, 0);
+ this.container.appendChild(this.gl.canvas);
+ }
+ createCamera() {
+ this.camera = new Camera(this.gl);
+ this.camera.fov = 45;
+ this.camera.position.z = 20;
+ }
+ createScene() {
+ this.scene = new Transform();
+ }
+ createGeometry() {
+ this.planeGeometry = new Plane(this.gl, {
+ heightSegments: 50,
+ widthSegments: 100
+ });
+ }
+ createMedias(items, bend = 1, textColor, borderRadius, font) {
+ const defaultItems = [
+ { image: `/bc2.webp?grayscale`, text: '' },
+ { image: `/rust4.webp?grayscale`, text: '' },
+ { image: `/rust6.webp?grayscale`, text: '' },
+ { image: `/hn2.webp?grayscale`, text: '' },
+ { image: `/COC1.JPG?grayscale`, text: '' },
+ { image: `/TERROR1.webp?grayscale`, text: '' },
+ { image: `/OUAC1.webp?grayscale`, text: '' },
+ { image: `/OUAC2.webp?grayscale`, text: '' },
+ { image: `/COC4.jpeg`, text: '' },
+ { image: `/rust3.webp`, text: '' },
+ { image: `hn1.webp`, text: '' },
+
+ ];
+ const galleryItems = items && items.length ? items : defaultItems;
+ this.mediasImages = galleryItems.concat(galleryItems);
+ this.medias = this.mediasImages.map((data, index) => {
+ return new Media({
+ geometry: this.planeGeometry,
+ gl: this.gl,
+ image: data.image,
+ index,
+ length: this.mediasImages.length,
+ renderer: this.renderer,
+ scene: this.scene,
+ screen: this.screen,
+ text: data.text,
+ viewport: this.viewport,
+ bend,
+ textColor,
+ borderRadius,
+ font
+ });
+ });
+ }
+ onTouchDown(e) {
+ this.isDown = true;
+ this.scroll.position = this.scroll.current;
+ this.start = e.touches ? e.touches[0].clientX : e.clientX;
+ }
+ onTouchMove(e) {
+ if (!this.isDown) return;
+ const x = e.touches ? e.touches[0].clientX : e.clientX;
+ const distance = (this.start - x) * (this.scrollSpeed * 0.025);
+ this.scroll.target = this.scroll.position + distance;
+ }
+ onTouchUp() {
+ this.isDown = false;
+ this.onCheck();
+ }
+ onWheel(e) {
+ const delta = e.deltaY || e.wheelDelta || e.detail;
+ this.scroll.target += (delta > 0 ? this.scrollSpeed : -this.scrollSpeed) * 0.2;
+ this.onCheckDebounce();
+ }
+ onCheck() {
+ if (!this.medias || !this.medias[0]) return;
+ const width = this.medias[0].width;
+ const itemIndex = Math.round(Math.abs(this.scroll.target) / width);
+ const item = width * itemIndex;
+ this.scroll.target = this.scroll.target < 0 ? -item : item;
+ }
+ onResize() {
+ this.screen = {
+ width: this.container.clientWidth,
+ height: this.container.clientHeight
+ };
+ this.renderer.setSize(this.screen.width, this.screen.height);
+ this.camera.perspective({
+ aspect: this.screen.width / this.screen.height
+ });
+ const fov = (this.camera.fov * Math.PI) / 180;
+ const height = 2 * Math.tan(fov / 2) * this.camera.position.z;
+ const width = height * this.camera.aspect;
+ this.viewport = { width, height };
+ if (this.medias) {
+ this.medias.forEach(media => media.onResize({ screen: this.screen, viewport: this.viewport }));
+ }
+ }
+ update() {
+ this.scroll.current = lerp(this.scroll.current, this.scroll.target, this.scroll.ease);
+ const direction = this.scroll.current > this.scroll.last ? 'right' : 'left';
+ if (this.medias) {
+ this.medias.forEach(media => media.update(this.scroll, direction));
+ }
+ this.renderer.render({ scene: this.scene, camera: this.camera });
+ this.scroll.last = this.scroll.current;
+ this.raf = window.requestAnimationFrame(this.update.bind(this));
+ }
+ addEventListeners() {
+ this.boundOnResize = this.onResize.bind(this);
+ this.boundOnWheel = this.onWheel.bind(this);
+ this.boundOnTouchDown = this.onTouchDown.bind(this);
+ this.boundOnTouchMove = this.onTouchMove.bind(this);
+ this.boundOnTouchUp = this.onTouchUp.bind(this);
+ window.addEventListener('resize', this.boundOnResize);
+ window.addEventListener('mousewheel', this.boundOnWheel);
+ window.addEventListener('wheel', this.boundOnWheel);
+ window.addEventListener('mousedown', this.boundOnTouchDown);
+ window.addEventListener('mousemove', this.boundOnTouchMove);
+ window.addEventListener('mouseup', this.boundOnTouchUp);
+ window.addEventListener('touchstart', this.boundOnTouchDown);
+ window.addEventListener('touchmove', this.boundOnTouchMove);
+ window.addEventListener('touchend', this.boundOnTouchUp);
+ }
+ destroy() {
+ window.cancelAnimationFrame(this.raf);
+ window.removeEventListener('resize', this.boundOnResize);
+ window.removeEventListener('mousewheel', this.boundOnWheel);
+ window.removeEventListener('wheel', this.boundOnWheel);
+ window.removeEventListener('mousedown', this.boundOnTouchDown);
+ window.removeEventListener('mousemove', this.boundOnTouchMove);
+ window.removeEventListener('mouseup', this.boundOnTouchUp);
+ window.removeEventListener('touchstart', this.boundOnTouchDown);
+ window.removeEventListener('touchmove', this.boundOnTouchMove);
+ window.removeEventListener('touchend', this.boundOnTouchUp);
+ if (this.renderer && this.renderer.gl && this.renderer.gl.canvas.parentNode) {
+ this.renderer.gl.canvas.parentNode.removeChild(this.renderer.gl.canvas);
+ }
+ }
+}
+
+// Ensure CircularGallery is properly configured and functional
+
+export default function CircularGallery({
+ items,
+ bend = 3,
+ textColor = '#ffffff',
+ borderRadius = 0.05,
+ font = 'bold 30px Figtree',
+ scrollSpeed = 2,
+ scrollEase = 0.05
+}) {
+ const containerRef = useRef(null);
+ useEffect(() => {
+ const app = new App(containerRef.current, { items, bend, textColor, borderRadius, font, scrollSpeed, scrollEase });
+ return () => {
+ app.destroy();
+ };
+ }, [items, bend, textColor, borderRadius, font, scrollSpeed, scrollEase]);
+ return
;
+}
diff --git a/src/components/CompilationBar.jsx b/src/components/CompilationBar.jsx
new file mode 100644
index 0000000..839e485
--- /dev/null
+++ b/src/components/CompilationBar.jsx
@@ -0,0 +1,89 @@
+import React, { useState, useEffect } from 'react';
+
+
+const messages = [
+ { threshold: 98, text: "BUILD SUCCESSFUL. Ready to deploy." },
+ { threshold: 88, text: "Rendering visual assets..." },
+ { threshold: 75, text: "Parsing user_feedback.log..." },
+ { threshold: 55, text: "Fetching legacy_pointers (Ex-Heads)..." },
+ { threshold: 35, text: "Compiling active_initiatives..." },
+ { threshold: 15, text: "Importing
libraries..." },
+ { threshold: 0, text: "Initializing CodeChef_ABESEC.exe..." },
+];
+
+const CompilationBar = () => {
+ const [scrollPercent, setScrollPercent] = useState(0);
+ const [statusMessage, setStatusMessage] = useState(messages[messages.length - 1].text);
+
+ useEffect(() => {
+ let ticking = false;
+
+ const handleScroll = () => {
+
+ if (!ticking) {
+ window.requestAnimationFrame(() => {
+
+ const totalHeight = document.documentElement.scrollHeight - window.innerHeight;
+ const currentScroll = window.scrollY;
+
+ const percent = Math.min((currentScroll / totalHeight) * 100, 100);
+
+ setScrollPercent(percent);
+
+ const currentMsg = messages.find(msg => percent >= msg.threshold);
+ if (currentMsg) {
+ setStatusMessage(currentMsg.text);
+ }
+
+ ticking = false;
+ });
+
+ ticking = true;
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, []);
+
+ return (
+
+
+ {/* 1. Terminal Prompt */}
+
+ ➜
+ ~/codechef-abesec/site
+ |
+ {/* Added a min-width to prevent jitter when text length changes */}
+
+ {statusMessage}
+
+
+
+ {/* 2. Percentage */}
+
+ [ {Math.round(scrollPercent)}% ]
+
+
+ {/* 3. Progress Bar Overlay */}
+
+
+
+
+ );
+};
+
+export default CompilationBar;
\ No newline at end of file
diff --git a/src/components/ExPresident.jsx b/src/components/ExPresident.jsx
index bad6868..1e306d7 100644
--- a/src/components/ExPresident.jsx
+++ b/src/components/ExPresident.jsx
@@ -200,14 +200,16 @@ const ExPresident = () => {
transformStyle: "preserve-3d",
}}
>
-
+
-
+
+
+
{president.name}
diff --git a/src/components/FloatingNavbar.jsx b/src/components/FloatingNavbar.jsx
index d2729a6..3a7b77a 100644
--- a/src/components/FloatingNavbar.jsx
+++ b/src/components/FloatingNavbar.jsx
@@ -13,17 +13,17 @@ export const FloatingNav = ({ navItems, className }) => {
const getActiveIndex = () => {
const currentPath = location.pathname;
-
+
// Check for exact match first
const exactIndex = navItems.findIndex(item => item.href === currentPath);
if (exactIndex !== -1) return exactIndex;
-
+
// Check for sub-routes (e.g., /events/sub-event should highlight Events)
const parentIndex = navItems.findIndex(item => {
if (item.href === '/') return false; // Don't match home for sub-routes
return currentPath.startsWith(item.href);
});
-
+
return parentIndex !== -1 ? parentIndex : 0;
};
@@ -50,7 +50,7 @@ export const FloatingNav = ({ navItems, className }) => {
{
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
- className="fixed inset-0 z-40 bg-black/95 md:hidden"
+ className="fixed inset-0 z-40000 bg-black/95 md:hidden"
onClick={() => setIsOpen(false)}
>
+
+
+
-
-
-
-
-
-
-
-
-
-
+
@@ -165,7 +178,7 @@ export default function Landing() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5, duration: 0.6 }}
- className="flex flex-wrap justify-center lg:justify-start gap-4 -mt-10"
+ className="flex flex-wrap justify-center gap-4 -mt-10"
>
-
-
-
-
- {[
- "translateZ(128px)",
- "rotateY(90deg) translateZ(128px)",
- "rotateY(180deg) translateZ(128px)",
- "rotateY(-90deg) translateZ(128px)",
- "rotateX(90deg) translateZ(128px)",
- "rotateX(-90deg) translateZ(128px)",
- ].map((t, i) => (
-
- ))}
-
-
-
);
diff --git a/src/components/LightRays.jsx b/src/components/LightRays.jsx
new file mode 100644
index 0000000..59386b5
--- /dev/null
+++ b/src/components/LightRays.jsx
@@ -0,0 +1,399 @@
+import { useRef, useEffect, useState } from 'react';
+import { Renderer, Program, Triangle, Mesh } from 'ogl';
+
+const DEFAULT_COLOR = '#ffffff';
+
+const hexToRgb = hex => {
+ const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1];
+};
+
+const getAnchorAndDir = (origin, w, h) => {
+ const outside = 0.2;
+ switch (origin) {
+ case 'top-left':
+ return { anchor: [0, -outside * h], dir: [0, 1] };
+ case 'top-right':
+ return { anchor: [w, -outside * h], dir: [0, 1] };
+ case 'left':
+ return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };
+ case 'right':
+ return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };
+ case 'bottom-left':
+ return { anchor: [0, (1 + outside) * h], dir: [0, -1] };
+ case 'bottom-center':
+ return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };
+ case 'bottom-right':
+ return { anchor: [w, (1 + outside) * h], dir: [0, -1] };
+ default: // "top-center"
+ return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };
+ }
+};
+
+const LightRays = ({
+ raysOrigin = 'top-center',
+ raysColor = DEFAULT_COLOR,
+ raysSpeed = 1,
+ lightSpread = 1,
+ rayLength = 2,
+ pulsating = false,
+ fadeDistance = 1.0,
+ saturation = 1.0,
+ followMouse = true,
+ mouseInfluence = 0.1,
+ noiseAmount = 0.0,
+ distortion = 0.0,
+ className = ''
+}) => {
+ const containerRef = useRef(null);
+ const uniformsRef = useRef(null);
+ const rendererRef = useRef(null);
+ const mouseRef = useRef({ x: 0.5, y: 0.5 });
+ const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });
+ const animationIdRef = useRef(null);
+ const meshRef = useRef(null);
+ const cleanupFunctionRef = useRef(null);
+ const [isVisible, setIsVisible] = useState(false);
+ const observerRef = useRef(null);
+
+ useEffect(() => {
+ if (!containerRef.current) return;
+
+ observerRef.current = new IntersectionObserver(
+ entries => {
+ const entry = entries[0];
+ setIsVisible(entry.isIntersecting);
+ },
+ { threshold: 0.1 }
+ );
+
+ observerRef.current.observe(containerRef.current);
+
+ return () => {
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ observerRef.current = null;
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!isVisible || !containerRef.current) return;
+
+ if (cleanupFunctionRef.current) {
+ cleanupFunctionRef.current();
+ cleanupFunctionRef.current = null;
+ }
+
+ const initializeWebGL = async () => {
+ if (!containerRef.current) return;
+
+ await new Promise(resolve => setTimeout(resolve, 10));
+
+ if (!containerRef.current) return;
+
+ const renderer = new Renderer({
+ dpr: Math.min(window.devicePixelRatio, 2),
+ alpha: true
+ });
+ rendererRef.current = renderer;
+
+ const gl = renderer.gl;
+ gl.canvas.style.width = '100%';
+ gl.canvas.style.height = '100%';
+
+ while (containerRef.current.firstChild) {
+ containerRef.current.removeChild(containerRef.current.firstChild);
+ }
+ containerRef.current.appendChild(gl.canvas);
+
+ const vert = `
+attribute vec2 position;
+varying vec2 vUv;
+void main() {
+ vUv = position * 0.5 + 0.5;
+ gl_Position = vec4(position, 0.0, 1.0);
+}`;
+
+ const frag = `precision highp float;
+
+uniform float iTime;
+uniform vec2 iResolution;
+
+uniform vec2 rayPos;
+uniform vec2 rayDir;
+uniform vec3 raysColor;
+uniform float raysSpeed;
+uniform float lightSpread;
+uniform float rayLength;
+uniform float pulsating;
+uniform float fadeDistance;
+uniform float saturation;
+uniform vec2 mousePos;
+uniform float mouseInfluence;
+uniform float noiseAmount;
+uniform float distortion;
+
+varying vec2 vUv;
+
+float noise(vec2 st) {
+ return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
+}
+
+float rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord,
+ float seedA, float seedB, float speed) {
+ vec2 sourceToCoord = coord - raySource;
+ vec2 dirNorm = normalize(sourceToCoord);
+ float cosAngle = dot(dirNorm, rayRefDirection);
+
+ float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;
+
+ float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));
+
+ float distance = length(sourceToCoord);
+ float maxDistance = iResolution.x * rayLength;
+ float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
+
+ float fadeFalloff = clamp((iResolution.x * fadeDistance - distance) / (iResolution.x * fadeDistance), 0.5, 1.0);
+ float pulse = pulsating > 0.5 ? (0.8 + 0.2 * sin(iTime * speed * 3.0)) : 1.0;
+
+ float baseStrength = clamp(
+ (0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +
+ (0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),
+ 0.0, 1.0
+ );
+
+ return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
+}
+
+void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+ vec2 coord = vec2(fragCoord.x, iResolution.y - fragCoord.y);
+
+ vec2 finalRayDir = rayDir;
+ if (mouseInfluence > 0.0) {
+ vec2 mouseScreenPos = mousePos * iResolution.xy;
+ vec2 mouseDirection = normalize(mouseScreenPos - rayPos);
+ finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));
+ }
+
+ vec4 rays1 = vec4(1.0) *
+ rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349,
+ 1.5 * raysSpeed);
+ vec4 rays2 = vec4(1.0) *
+ rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234,
+ 1.1 * raysSpeed);
+
+ fragColor = rays1 * 0.5 + rays2 * 0.4;
+
+ if (noiseAmount > 0.0) {
+ float n = noise(coord * 0.01 + iTime * 0.1);
+ fragColor.rgb *= (1.0 - noiseAmount + noiseAmount * n);
+ }
+
+ float brightness = 1.0 - (coord.y / iResolution.y);
+ fragColor.x *= 0.1 + brightness * 0.8;
+ fragColor.y *= 0.3 + brightness * 0.6;
+ fragColor.z *= 0.5 + brightness * 0.5;
+
+ if (saturation != 1.0) {
+ float gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114));
+ fragColor.rgb = mix(vec3(gray), fragColor.rgb, saturation);
+ }
+
+ fragColor.rgb *= raysColor;
+}
+
+void main() {
+ vec4 color;
+ mainImage(color, gl_FragCoord.xy);
+ gl_FragColor = color;
+}`;
+
+ const uniforms = {
+ iTime: { value: 0 },
+ iResolution: { value: [1, 1] },
+
+ rayPos: { value: [0, 0] },
+ rayDir: { value: [0, 1] },
+
+ raysColor: { value: hexToRgb(raysColor) },
+ raysSpeed: { value: raysSpeed },
+ lightSpread: { value: lightSpread },
+ rayLength: { value: rayLength },
+ pulsating: { value: pulsating ? 1.0 : 0.0 },
+ fadeDistance: { value: fadeDistance },
+ saturation: { value: saturation },
+ mousePos: { value: [0.5, 0.5] },
+ mouseInfluence: { value: mouseInfluence },
+ noiseAmount: { value: noiseAmount },
+ distortion: { value: distortion }
+ };
+ uniformsRef.current = uniforms;
+
+ const geometry = new Triangle(gl);
+ const program = new Program(gl, { vertex: vert, fragment: frag, uniforms });
+ const mesh = new Mesh(gl, { geometry, program });
+ meshRef.current = mesh;
+
+ const updatePlacement = () => {
+ if (!containerRef.current || !renderer) return;
+
+ renderer.dpr = Math.min(window.devicePixelRatio, 2);
+
+ const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;
+ renderer.setSize(wCSS, hCSS);
+
+ const dpr = renderer.dpr;
+ const w = wCSS * dpr;
+ const h = hCSS * dpr;
+
+ uniforms.iResolution.value = [w, h];
+
+ const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h);
+ uniforms.rayPos.value = anchor;
+ uniforms.rayDir.value = dir;
+ };
+
+ const loop = t => {
+ if (!rendererRef.current || !uniformsRef.current || !meshRef.current) {
+ return;
+ }
+
+ uniforms.iTime.value = t * 0.001;
+
+ if (followMouse && mouseInfluence > 0.0) {
+ const smoothing = 0.92;
+
+ smoothMouseRef.current.x = smoothMouseRef.current.x * smoothing + mouseRef.current.x * (1 - smoothing);
+ smoothMouseRef.current.y = smoothMouseRef.current.y * smoothing + mouseRef.current.y * (1 - smoothing);
+
+ uniforms.mousePos.value = [smoothMouseRef.current.x, smoothMouseRef.current.y];
+ }
+
+ try {
+ renderer.render({ scene: mesh });
+ animationIdRef.current = requestAnimationFrame(loop);
+ } catch (error) {
+ console.warn('WebGL rendering error:', error);
+ return;
+ }
+ };
+
+ window.addEventListener('resize', updatePlacement);
+ updatePlacement();
+ animationIdRef.current = requestAnimationFrame(loop);
+
+ cleanupFunctionRef.current = () => {
+ if (animationIdRef.current) {
+ cancelAnimationFrame(animationIdRef.current);
+ animationIdRef.current = null;
+ }
+
+ window.removeEventListener('resize', updatePlacement);
+
+ if (renderer) {
+ try {
+ const canvas = renderer.gl.canvas;
+ const loseContextExt = renderer.gl.getExtension('WEBGL_lose_context');
+ if (loseContextExt) {
+ loseContextExt.loseContext();
+ }
+
+ if (canvas && canvas.parentNode) {
+ canvas.parentNode.removeChild(canvas);
+ }
+ } catch (error) {
+ console.warn('Error during WebGL cleanup:', error);
+ }
+ }
+
+ rendererRef.current = null;
+ uniformsRef.current = null;
+ meshRef.current = null;
+ };
+ };
+
+ initializeWebGL();
+
+ return () => {
+ if (cleanupFunctionRef.current) {
+ cleanupFunctionRef.current();
+ cleanupFunctionRef.current = null;
+ }
+ };
+ }, [
+ isVisible,
+ raysOrigin,
+ raysColor,
+ raysSpeed,
+ lightSpread,
+ rayLength,
+ pulsating,
+ fadeDistance,
+ saturation,
+ followMouse,
+ mouseInfluence,
+ noiseAmount,
+ distortion
+ ]);
+
+ useEffect(() => {
+ if (!uniformsRef.current || !containerRef.current || !rendererRef.current) return;
+
+ const u = uniformsRef.current;
+ const renderer = rendererRef.current;
+
+ u.raysColor.value = hexToRgb(raysColor);
+ u.raysSpeed.value = raysSpeed;
+ u.lightSpread.value = lightSpread;
+ u.rayLength.value = rayLength;
+ u.pulsating.value = pulsating ? 1.0 : 0.0;
+ u.fadeDistance.value = fadeDistance;
+ u.saturation.value = saturation;
+ u.mouseInfluence.value = mouseInfluence;
+ u.noiseAmount.value = noiseAmount;
+ u.distortion.value = distortion;
+
+ const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;
+ const dpr = renderer.dpr;
+ const { anchor, dir } = getAnchorAndDir(raysOrigin, wCSS * dpr, hCSS * dpr);
+ u.rayPos.value = anchor;
+ u.rayDir.value = dir;
+ }, [
+ raysColor,
+ raysSpeed,
+ lightSpread,
+ raysOrigin,
+ rayLength,
+ pulsating,
+ fadeDistance,
+ saturation,
+ mouseInfluence,
+ noiseAmount,
+ distortion
+ ]);
+
+ useEffect(() => {
+ const handleMouseMove = e => {
+ if (!containerRef.current || !rendererRef.current) return;
+ const rect = containerRef.current.getBoundingClientRect();
+ const x = (e.clientX - rect.left) / rect.width;
+ const y = (e.clientY - rect.top) / rect.height;
+ mouseRef.current = { x, y };
+ };
+
+ if (followMouse) {
+ window.addEventListener('mousemove', handleMouseMove);
+ return () => window.removeEventListener('mousemove', handleMouseMove);
+ }
+ }, [followMouse]);
+
+ return (
+
+ );
+};
+
+export default LightRays;
diff --git a/src/components/Offering.jsx b/src/components/Offering.jsx
index f0b58ef..264f659 100644
--- a/src/components/Offering.jsx
+++ b/src/components/Offering.jsx
@@ -1,99 +1,336 @@
-import React from "react";
+import React, { useState, useEffect, useRef } from "react";
import "../App.css";
-import { PinContainer } from "./PinContainer";
const Offering = () => {
+ const [activeIndex, setActiveIndex] = useState(0);
+ const totalItems = 4;
+ const baseRotation = -5;
+ const itemSpacing = 12;
+ const rotateVal = -(activeIndex * itemSpacing);
+
+ const contentData = [
+ {
+ title: "Coding\nWorkSpaces",
+ desc: "Immersive coding environments designed for focus and productivity. We host competitive programming workshops, hackathons, and hands-on bootcamps to sharpen your skills.",
+ img: "/OUAC2.webp"
+ },
+ {
+ title: "Connect With\nCool Mentors",
+ desc: "Bridge the gap between learning and industry. Connect with experienced developers, alumni, and tech leaders who provide guidance, code reviews, and career advice.",
+ img: "/rust7.webp"
+ },
+ {
+ title: "Innovation\nHub",
+ desc: "A collaborative space where wild ideas turn into reality. We provide the resources, peer support, and brainstorming sessions needed to launch your next big project.",
+ img: "/COC3.JPG"
+ },
+ {
+ title: "Community\nEvents",
+ desc: "Join a thriving network of tech enthusiasts. From casual meetups to tech talks and networking nights, we foster a culture of sharing knowledge and growing together.",
+ img: "/COC1.JPG"
+ },
+ ];
+
+ // useEffect(() => {
+ // const interval = setInterval(() => {
+ // setActiveIndex((prev) => (prev + 1) % totalItems);
+ // }, 4000);
+ // return () => clearInterval(interval);
+ // }, []);
+
+ const [slideAmount, setSlideAmount] = useState(300);
+ const [hoveredIndex, setHoveredIndex] = useState(null);
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
+
+ const [isMobile, setIsMobile] = useState(false);
+ const [wheelConfig, setWheelConfig] = useState({
+ radius: 510,
+ rotateOffset: -250,
+ scale: 0.9
+ });
+
+ const handleMouseMove = (e) => {
+ setMousePosition({ x: e.clientX, y: e.clientY });
+ };
+
+ useEffect(() => {
+ const handleResize = () => {
+ if (window.innerWidth >= 1024) {
+ setSlideAmount(632);
+ setIsMobile(false);
+ setWheelConfig({ radius: 510, rotateOffset: -250, scale: 0.9 });
+ } else if (window.innerWidth >= 768) {
+ setSlideAmount(375);
+ setIsMobile(false);
+ setWheelConfig({ radius: 450, rotateOffset: -200, scale: 0.8 });
+ } else {
+ setSlideAmount(200);
+ setIsMobile(true);
+ setWheelConfig({ radius: 250, rotateOffset: -50, scale: 0.55 });
+ }
+ };
+
+
+ handleResize();
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ const handleItemClick = (index) => {
+ setActiveIndex(index);
+ };
+
+ const handleNext = () => {
+ setActiveIndex((prev) => (prev + 1) % totalItems);
+ };
+
+ const handlePrev = () => {
+ setActiveIndex((prev) => (prev - 1 + totalItems) % totalItems);
+ };
+
return (
-
-
-
-
-
-
- Our Initiatives
-
-
-
- Empowering students to code, collaborate, and create impactful tech solutions.
-
- We are building a community where curiosity meets execution. Through hands on programs, mentorship, and innovation driven events, we help students transform ideas into real world solutions.
-
-
-
-
-
-
-
-
-
-
- Coding WorkSpaces & Upskill
-
-
-
-
- We host coding workshops for hands on practice for competitive programming.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Connect With Cool Mentors
-
-
-
-
- Connect with Experienced mentors, seniors and explore new perspectives.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Innovation Hub
-
-
-
-
- Unlocking the potential of innovations and collaborating ideas and minds.
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {["Upskill", "Mentors", "Innovation", "Community"].map((item, idx) => {
+ return (
+
handleItemClick(idx)}
+ onMouseEnter={() => setHoveredIndex(idx)}
+ onMouseLeave={() => setHoveredIndex(null)}
+ className={`absolute left-[calc(50%+50px)] top-1/2 text-[12px] sm:text-[24px] md:text-[30px] lg:text-[38px] font-montserrat font-medium transition-all duration-1000 z-[100] group flex items-center gap-1 sm:gap-2 ${activeIndex === idx
+ ? "text-white"
+ : "text-white/50 hover:text-white/80"
+ }`}
+ style={{
+ cursor: 'url("/wheel/hand.svg"), pointer',
+ transform: `rotate(${-8 + idx * 12}deg) translateX(${wheelConfig.radius}px) translateY(-50%)`,
+ transformOrigin: "left center",
+ }}
+ >
+
+ {item}
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ Initiatives
+
+
+
+
+
+ {contentData.map((content, idx) => (
+
+
+ {content.title}
+
+
+ {content.desc}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ {
+ isMobile ? (
+
+
+
+ ) : (
+ hoveredIndex !== null && contentData[hoveredIndex] && (
+
+
+
+ )
+ )
+ }
+
);
};
diff --git a/src/components/PinContainer.jsx b/src/components/PinContainer.jsx
deleted file mode 100644
index 6c93a86..0000000
--- a/src/components/PinContainer.jsx
+++ /dev/null
@@ -1,186 +0,0 @@
-"use client";
-import React, { useState, useEffect } from "react";
-import { motion } from "motion/react";
-import { cn } from "../lib/utils";
-
-export const PinContainer = ({
- children,
- title,
- href,
- className,
- containerClassName,
-}) => {
- // 1. Add state to track mobile view
- const [isMobile, setIsMobile] = useState(false);
- const [transform, setTransform] = useState(
- "translate(-50%,-50%) rotateX(25deg) scale(0.72)"
- );
-
- // 2. Check screen size on mount and resize
- useEffect(() => {
- const checkMobile = () => {
- const isTouch = window.innerWidth < 768; // Tailwind 'md' breakpoint
- setIsMobile(isTouch);
-
- // Force the correct initial transform based on screen size
- if (isTouch) {
- setTransform("translate(-50%,-50%) rotateX(0deg) scale(1)");
- } else {
- setTransform("translate(-50%,-50%) rotateX(25deg) scale(0.72)");
- }
- };
-
- checkMobile();
- window.addEventListener("resize", checkMobile);
- return () => window.removeEventListener("resize", checkMobile);
- }, []);
-
- const onMouseEnter = () => {
- // Only apply the 3D hover effect on Desktop
- if (!isMobile) {
- setTransform("translate(-50%,-50%) rotateX(0deg) scale(0.9)");
- }
- };
-
- const onMouseLeave = () => {
- // Reset to appropriate resting state based on device
- if (!isMobile) {
- setTransform("translate(-50%,-50%) rotateX(25deg) scale(0.72)");
- } else {
- setTransform("translate(-50%,-50%) rotateX(0deg) scale(1)");
- }
- };
-
- return (
-
-
-
-
- );
-};
-
-export const PinPerspective = ({ title, href }) => {
- return (
-
-
-
-
-
- <>
-
-
-
- >
-
-
- <>
-
-
-
-
- >
-
-
- );
-};
\ No newline at end of file
diff --git a/src/components/RippleGridBackground.jsx b/src/components/RippleGridBackground.jsx
new file mode 100644
index 0000000..2140129
--- /dev/null
+++ b/src/components/RippleGridBackground.jsx
@@ -0,0 +1,148 @@
+import React, { useEffect, useRef } from 'react';
+
+// Helper for smooth animations
+const lerp = (start, end, amt) => (1 - amt) * start + amt * end;
+
+const RippleGridBackground = () => {
+ const canvasRef = useRef(null);
+
+ const config = {
+ // VISUAL SETTINGS
+ gridSize: 40, // Slightly larger squares for a more modern look
+ gap: 0, // No gap = seamless grid (looks more like a surface)
+
+ // COLOR GRADIENT (HSL)
+ // Base: The resting state (Almost invisible)
+ baseHue: 120, // CodeChef Greenish
+ baseSat: 15, // Very desaturated
+ baseLight: 2, // 2% lightness (Very, very dark)
+
+ // Active: The ripple state (Subtle glow)
+ activeHue: 150, // Shifts to Teal/Cyan when hovered
+ activeSat: 30, // Slightly more color
+ activeLight: 10, // Only goes up to 10% brightness (Subtle!)
+
+ // ANIMATION PHYSICS
+ mouseRadius: 400, // Larger radius for a smoother gradient spread
+ lerpFactor: 0.05, // 0.05 is very slow and smooth (liquid feel)
+ };
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ const ctx = canvas.getContext('2d');
+ let animationFrameId;
+ let width, height, rows, cols;
+
+ let grid = [];
+ let mouse = { x: -1000, y: -1000 };
+
+ const resize = () => {
+ width = window.innerWidth;
+ height = window.innerHeight;
+ canvas.width = width;
+ canvas.height = height;
+
+ const cellSize = config.gridSize + config.gap;
+ cols = Math.ceil(width / cellSize);
+ rows = Math.ceil(height / cellSize);
+
+ grid = [];
+ for (let i = 0; i < cols; i++) {
+ grid[i] = [];
+ for (let j = 0; j < rows; j++) {
+ grid[i][j] = {
+ intensity: 0, // 0 = Base State, 1 = Active State
+ };
+ }
+ }
+ };
+
+ const handleMouseMove = (e) => {
+ mouse.x = e.clientX;
+ mouse.y = e.clientY;
+ };
+
+ const handleMouseLeave = () => {
+ mouse.x = -1000;
+ mouse.y = -1000;
+ };
+
+ window.addEventListener('resize', resize);
+ window.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseleave', handleMouseLeave);
+
+ resize();
+
+ const render = () => {
+ // Clear background with the base color
+ ctx.fillStyle = `hsl(${config.baseHue}, ${config.baseSat}%, ${config.baseLight}%)`;
+ ctx.fillRect(0, 0, width, height);
+
+ const cellSize = config.gridSize + config.gap;
+
+ for (let i = 0; i < cols; i++) {
+ for (let j = 0; j < rows; j++) {
+ const cell = grid[i][j];
+
+ const cellX = i * cellSize;
+ const cellY = j * cellSize;
+ const centerX = cellX + config.gridSize / 2;
+ const centerY = cellY + config.gridSize / 2;
+
+ const dist = Math.hypot(mouse.x - centerX, mouse.y - centerY);
+
+ // Calculate Target Intensity
+ let target = 0;
+ if (dist < config.mouseRadius) {
+ const normDist = dist / config.mouseRadius;
+ // Cubic easing (1 - d)^3 for a much softer, gradient-like falloff
+ target = Math.pow(1 - normDist, 3);
+ }
+
+ // Smooth interpolation
+ cell.intensity = lerp(cell.intensity, target, config.lerpFactor);
+
+ // Optimization: Only draw cells that aren't purely base state
+ if (cell.intensity > 0.001) {
+ // Interpolate Colors (Create the gradient transition)
+ const h = lerp(config.baseHue, config.activeHue, cell.intensity);
+ const s = lerp(config.baseSat, config.activeSat, cell.intensity);
+ const l = lerp(config.baseLight, config.activeLight, cell.intensity);
+
+ ctx.fillStyle = `hsl(${h}, ${s}%, ${l}%)`;
+
+ // No scale effect - simpler, cleaner, less distracting
+ ctx.fillRect(cellX, cellY, config.gridSize, config.gridSize);
+ }
+ }
+ }
+ animationFrameId = requestAnimationFrame(render);
+ };
+
+ render();
+
+ return () => {
+ window.removeEventListener('resize', resize);
+ window.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseleave', handleMouseLeave);
+ cancelAnimationFrame(animationFrameId);
+ };
+ }, []);
+
+ return (
+
+ );
+};
+
+export default RippleGridBackground;
\ No newline at end of file
diff --git a/src/components/ShuffleText2.jsx b/src/components/ShuffleText2.jsx
index 531c099..7ff9291 100644
--- a/src/components/ShuffleText2.jsx
+++ b/src/components/ShuffleText2.jsx
@@ -91,7 +91,8 @@ const ShuffleText2 = ({ text, className }) => {
opacity: 1,
y: 0,
transition: {
- duration: 0.3
+ duration: 0.5,
+ ease: "easeOut"
}
}}
style={{ display: 'inline-block', whiteSpace: 'pre' }}
diff --git a/src/components/TeamGrid.jsx b/src/components/TeamGrid.jsx
index be9008e..3b1dd34 100644
--- a/src/components/TeamGrid.jsx
+++ b/src/components/TeamGrid.jsx
@@ -1,5 +1,6 @@
import { useEffect, useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
+import { motion } from "framer-motion";
import {
IconHome,
IconCalendar,
@@ -18,23 +19,23 @@ export default function TeamGrid() {
const navigate = useNavigate();
const teamMembers = [
- { name: 'Vishesh Dudeja', role: 'Advisory', row: 0, col: 0, transformOrigin: 'right bottom',img:'1.webp', linkedin: 'https://linkedin.com/in/vishesh-dudeja-b62a79242', tech: ['Python', 'Java', 'SQL'] },
- { name: 'Sai Aryan Goswami', role: 'Core Team', row: 0, col: 2, transformOrigin: 'left bottom',img:'2.webp', linkedin: 'https://linkedin.com/in/saiaryangoswami', tech: ['UI/UX', 'Next.js', 'Full Stack'] },
- { name: 'Vidhi Gandhi', role: 'President', row: 1, col: 1, transformOrigin: 'left bottom',img:'vidhi-didi.jpg', linkedin: 'https://linkedin.com/in/vidhi-gandhi-640806296', tech: ['React.js', 'HTML', 'CSS'] },
- { name: 'Rohit Bhardwaj', role: 'Vice-president', row: 2, col: 0, transformOrigin: 'right bottom',img:'rohit-bhaiya.jpg', linkedin: 'https://linkedin.com/in/dev-rohitbhardwaj', tech: ['Solidity', 'Web3', 'Open Source'] },
- { name: 'Lavish Aggarwal', role: 'Vice-president', row: 2, col: 3, transformOrigin: 'left bottom', img:'lavish-bhaiya.jpg', linkedin: 'https://linkedin.com/in/lavishagrwl', tech: ['JavaScript', 'Python', 'React.js'] },
- { name: 'Abhinav Vishwakarma', role: 'Development Lead', row: 3, col: 1, transformOrigin: 'left bottom',img:'abhinav-bhaiya.jpg', linkedin: 'https://linkedin.com/in/abhinav-vishwakarma-fsd', tech: ['Canva', 'Python', 'Video Editing'] },
- { name: 'Ramyak Jain', role: 'Event Lead', row: 3, col: 2, transformOrigin: 'right bottom', img:'ramayk bhaiya.jpg', linkedin: 'https://linkedin.com/in/ramyak-jain', tech: ['Graphic Designer', 'Content Writing', 'Wed Designer'] },
- { name: 'Utkarsh Saxena', role: 'Cp Lead',row: 4, col: 0, transformOrigin: 'left bottom',img:'utkarsh-bhaiya.jpg', linkedin: 'https://linkedin.com/in/utkarsh-saxena-91005a290', tech: ['React.js', 'Javascript', 'DSA'] },
- { name: 'Deepanshu', role: 'Graphics Lead', row: 4, col: 3, transformOrigin: 'left bottom',img:'deepanshu bhaiya.jpg', linkedin: 'https://linkedin.com/in/deepanshu-kaushik-174059297', tech: ['UI/UX Designer', 'DSA', 'Javascript'] },
- { name: 'Swati Mittal', role: 'Pr Lead', row: 5, col: 1, transformOrigin: 'left bottom',img:'swati-didi.jpg', linkedin: 'https://linkedin.com/in/swati-mittal24', tech: ['MongoDB', 'Node.js', 'express.js'] },
- { name: 'Ananya', role: 'Content Lead', row: 6, col: 1, transformOrigin: 'left bottom',img:'ananya-didi.jpg', linkedin: 'https://linkedin.com/in/', tech: ['Copywriting', 'SEO', 'Content Strategy'] },
- { name: 'Sakhi Vishnoi', role: 'Graphics Lead', row: 6, col: 3, transformOrigin: 'left bottom', img:'sakshi didi.jpg', linkedin: 'https://linkedin.com/in/sakshi-vishnoi-7770b2315', tech: ['Flutter', 'DSA', 'C++'] },
- { name: 'Anvesh Srivastava ', role: 'Backend Developer', row: 7, col: 0, transformOrigin: 'right bottom',img:'anvesh-bhaiya.jpg', linkedin: 'https://linkedin.com/in/anvesh-srivastava', tech: ['Node.js', 'RestAPI', 'MongoDB'] },
- { name: 'kaif azmi', role: 'Frontend Developer', row: 7, col: 2, transformOrigin: 'left bottom',img:'kaif-bhaiya.jpg', linkedin: 'https://linkedin.com/in/kaifazmi', tech: ['React', 'CSS', 'JavaScript'] },
- { name: "Bhaskar Dwivedi", role: 'Mobile Developer', row: 8, col: 1, transformOrigin: 'left bottom',img:'bhaskar-bhaiya.jpg', linkedin: 'https://linkedin.com/in/bhaskar-dwi', tech: ['React Native', 'Flutter', 'TailwidCSS'] },
- { name: 'Dhruv Khare', role: 'Design Systems', row: 9, col: 0, transformOrigin: 'right bottom',img:'dhruv-bhaiya.jpg', linkedin: 'https://linkedin.com/in/dhruvkhare-softwaredev', tech: ['RestAPI', 'MonogoDB', 'Express.js'] },
- { name: 'Amit Gupta', role: 'Product Analyst', row: 9, col: 3, transformOrigin: 'left bottom', img:'amit bhaiya.png', linkedin: 'linkedin.com/in/amitguptadev', tech: ['Node.js', 'AI/ML', 'Typescript'] }
+ { name: 'Vishesh Dudeja', role: 'Advisory', row: 0, col: 0, transformOrigin: 'right bottom', img: '1.webp', linkedin: 'https://linkedin.com/in/vishesh-dudeja-b62a79242', tech: ['Python', 'Java', 'SQL'] },
+ { name: 'Sai Aryan Goswami', role: 'Core Team', row: 0, col: 2, transformOrigin: 'left bottom', img: '2.webp', linkedin: 'https://linkedin.com/in/saiaryangoswami', tech: ['UI/UX', 'Next.js', 'Full Stack'] },
+ { name: 'Vidhi Gandhi', role: 'President', row: 1, col: 1, transformOrigin: 'left bottom', img: 'vidhi-didi.jpg', linkedin: 'https://linkedin.com/in/vidhi-gandhi-640806296', tech: ['React.js', 'HTML', 'CSS'] },
+ { name: 'Rohit Bhardwaj', role: 'Vice-president', row: 2, col: 0, transformOrigin: 'right bottom', img: 'rohit-bhaiya.jpg', linkedin: 'https://linkedin.com/in/dev-rohitbhardwaj', tech: ['Solidity', 'Web3', 'Open Source'] },
+ { name: 'Lavish Aggarwal', role: 'Vice-president', row: 2, col: 3, transformOrigin: 'left bottom', img: 'lavish-bhaiya.jpg', linkedin: 'https://linkedin.com/in/lavishagrwl', tech: ['JavaScript', 'Python', 'React.js'] },
+ { name: 'Abhinav Vishwakarma', role: 'Development Lead', row: 3, col: 1, transformOrigin: 'left bottom', img: 'abhinav-bhaiya.jpg', linkedin: 'https://linkedin.com/in/abhinav-vishwakarma-fsd', tech: ['Canva', 'Python', 'Video Editing'] },
+ { name: 'Ramyak Jain', role: 'Event Lead', row: 3, col: 2, transformOrigin: 'right bottom', img: 'ramayk bhaiya.jpg', linkedin: 'https://linkedin.com/in/ramyak-jain', tech: ['Graphic Designer', 'Content Writing', 'Wed Designer'] },
+ { name: 'Utkarsh Saxena', role: 'Cp Lead', row: 4, col: 0, transformOrigin: 'left bottom', img: 'utkarsh-bhaiya.jpg', linkedin: 'https://linkedin.com/in/utkarsh-saxena-91005a290', tech: ['React.js', 'Javascript', 'DSA'] },
+ { name: 'Deepanshu', role: 'Graphics Lead', row: 4, col: 3, transformOrigin: 'left bottom', img: 'deepanshu bhaiya.jpg', linkedin: 'https://linkedin.com/in/deepanshu-kaushik-174059297', tech: ['UI/UX Designer', 'DSA', 'Javascript'] },
+ { name: 'Swati Mittal', role: 'Pr Lead', row: 5, col: 1, transformOrigin: 'left bottom', img: 'swati-didi.jpg', linkedin: 'https://linkedin.com/in/swati-mittal24', tech: ['MongoDB', 'Node.js', 'express.js'] },
+ { name: 'Ananya', role: 'Content Lead', row: 6, col: 1, transformOrigin: 'left bottom', img: 'ananya-didi.jpg', linkedin: 'https://linkedin.com/in/', tech: ['Copywriting', 'SEO', 'Content Strategy'] },
+ { name: 'Sakhi Vishnoi', role: 'Graphics Lead', row: 6, col: 3, transformOrigin: 'left bottom', img: 'sakshi didi.jpg', linkedin: 'https://linkedin.com/in/sakshi-vishnoi-7770b2315', tech: ['Flutter', 'DSA', 'C++'] },
+ { name: 'Anvesh Srivastava ', role: 'Backend Developer', row: 7, col: 0, transformOrigin: 'right bottom', img: 'anvesh-bhaiya.jpg', linkedin: 'https://linkedin.com/in/anvesh-srivastava', tech: ['Node.js', 'RestAPI', 'MongoDB'] },
+ { name: 'kaif azmi', role: 'Frontend Developer', row: 7, col: 2, transformOrigin: 'left bottom', img: 'kaif-bhaiya.jpg', linkedin: 'https://linkedin.com/in/kaifazmi', tech: ['React', 'CSS', 'JavaScript'] },
+ { name: "Bhaskar Dwivedi", role: 'Mobile Developer', row: 8, col: 1, transformOrigin: 'left bottom', img: 'bhaskar-bhaiya.jpg', linkedin: 'https://linkedin.com/in/bhaskar-dwi', tech: ['React Native', 'Flutter', 'TailwidCSS'] },
+ { name: 'Dhruv Khare', role: 'Design Systems', row: 9, col: 0, transformOrigin: 'right bottom', img: 'dhruv-bhaiya.jpg', linkedin: 'https://linkedin.com/in/dhruvkhare-softwaredev', tech: ['RestAPI', 'MonogoDB', 'Express.js'] },
+ { name: 'Amit Gupta', role: 'Product Analyst', row: 9, col: 3, transformOrigin: 'left bottom', img: 'amit bhaiya.png', linkedin: 'linkedin.com/in/amitguptadev', tech: ['Node.js', 'AI/ML', 'Typescript'] }
];
useEffect(() => {
@@ -139,7 +140,7 @@ export default function TeamGrid() {
onClick: () => navigate("/admin"),
});
}
-
+
navLinks.push({
title: "Logout",
icon:
,
@@ -153,17 +154,36 @@ export default function TeamGrid() {
-
-
- scroll down to meet the teams
-
-
-
-
-
Teams
-
+
+
+ {"Meet our Bawarchis".split("").map((char, index) => (
+
+ {char}
+
+ ))}
+
+
-
+
{grid.map((row, rowIndex) => (
{row.map((member, colIndex) => (
@@ -183,9 +203,8 @@ export default function TeamGrid() {
>
@@ -194,11 +213,10 @@ export default function TeamGrid() {
flex flex-col items-center justify-center
gap-2 sm:gap-3
transition-opacity duration-300
- ${
- hoveredIndex === member.index
- ? "opacity-100"
- : "opacity-0 pointer-events-none"
- }
+ ${hoveredIndex === member.index
+ ? "opacity-100"
+ : "opacity-0 pointer-events-none"
+ }
`}
>
@@ -239,8 +257,6 @@ export default function TeamGrid() {
))}
-
-
);
diff --git a/src/components/eventsGalary.jsx b/src/components/eventsGalary.jsx
new file mode 100644
index 0000000..546b775
--- /dev/null
+++ b/src/components/eventsGalary.jsx
@@ -0,0 +1,20 @@
+import CircularGallery from './CircularGallery';
+
+export default function EventsGalary() {
+ return (
+
+
+ Memories that we cooked together
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/requestAnimationFrame.jsx b/src/components/requestAnimationFrame.jsx
new file mode 100644
index 0000000..98b6d21
--- /dev/null
+++ b/src/components/requestAnimationFrame.jsx
@@ -0,0 +1,93 @@
+import React, { useState, useEffect } from 'react';
+
+// 1. Move static data OUTSIDE the component.
+// Defined in reverse order (highest threshold first) to make the logic faster/simpler.
+const messages = [
+ { threshold: 98, text: "BUILD SUCCESSFUL. Ready to deploy." },
+ { threshold: 88, text: "Rendering visual assets..." },
+ { threshold: 75, text: "Parsing user_feedback.log..." },
+ { threshold: 55, text: "Fetching legacy_pointers (Ex-Heads)..." },
+ { threshold: 35, text: "Compiling active_initiatives..." },
+ { threshold: 15, text: "Importing libraries..." },
+ { threshold: 0, text: "Initializing CodeChef_ABESEC.exe..." },
+];
+
+const CompilationBar = () => {
+ const [scrollPercent, setScrollPercent] = useState(0);
+ const [statusMessage, setStatusMessage] = useState(messages[messages.length - 1].text);
+
+ useEffect(() => {
+ let ticking = false;
+
+ const handleScroll = () => {
+ // 2. Use 'ticking' to prevent calculations if the browser is busy
+ if (!ticking) {
+ window.requestAnimationFrame(() => {
+
+ const totalHeight = document.documentElement.scrollHeight - window.innerHeight;
+ const currentScroll = window.scrollY;
+ // Calculate percent
+ const percent = Math.min((currentScroll / totalHeight) * 100, 100);
+
+ setScrollPercent(percent);
+
+ // 3. Optimized search: Find the first message where percent > threshold
+ // Since array is reversed, this finds the highest applicable threshold immediately
+ const currentMsg = messages.find(msg => percent >= msg.threshold);
+ if (currentMsg) {
+ setStatusMessage(currentMsg.text);
+ }
+
+ ticking = false;
+ });
+
+ ticking = true;
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, []);
+
+ return (
+
+
+ {/* 1. Terminal Prompt */}
+
+ ➜
+ ~/codechef-abesec/site
+ |
+ {/* Added a min-width to prevent jitter when text length changes */}
+
+ {statusMessage}
+
+
+
+ {/* 2. Percentage */}
+
+ [ {Math.round(scrollPercent)}% ]
+
+
+ {/* 3. Progress Bar Overlay */}
+
+
+ {/* Optional: A thin bright line at the very top of the bar for extra "tech" feel */}
+
+
+ );
+};
+
+export default CompilationBar;
\ No newline at end of file
diff --git a/src/components/testimonial.tsx b/src/components/testimonial.tsx
new file mode 100644
index 0000000..bf5bb1b
--- /dev/null
+++ b/src/components/testimonial.tsx
@@ -0,0 +1,35 @@
+import { TestimonialsCard } from "./ui/testimonials-card"
+
+const testimonials = [
+{
+ id: 1,
+ title: "Sarah Wilson",
+ description: "Incredible product! It exceeded all my expectations.",
+ image: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop",
+},
+{
+ id: 2,
+ title: "David Chen",
+ description: "The best investment I've made this year.",
+ image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=400&fit=crop",
+},
+{
+ id: 3,
+ title: "Emma Rodriguez",
+ description: "Outstanding service and amazing results!",
+ image: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=400&h=400&fit=crop",
+},
+];
+
+export default function AnimatedTestimonialsDemo() {
+return (
+
+
+ Served Hot: Words from Our Bawarchis
+
+
+
+
+
+);
+}
\ No newline at end of file
diff --git a/src/components/ui/infinite-menu.tsx b/src/components/ui/infinite-menu.tsx
new file mode 100644
index 0000000..a238932
--- /dev/null
+++ b/src/components/ui/infinite-menu.tsx
@@ -0,0 +1,377 @@
+// @ts-check
+import React, { useCallback, useMemo, useState, memo, useEffect } from "react";
+import {
+ View,
+ StyleSheet,
+ Dimensions,
+ Animated as RNAnimated,
+} from "react-native";
+import { Canvas, Circle, Group, Image, Skia } from "@shopify/react-native-skia";
+import { useSharedValue, useFrameCallback } from "react-native-reanimated";
+import {
+ Gesture,
+ GestureDetector,
+ GestureHandlerRootView,
+} from "react-native-gesture-handler";
+import { useLoadImages } from "./hooks";
+import { Quat, Vec3 } from "./maths-type";
+import type { IDisc, IDiscComponent, IInfiniteMenu, IMenuItem } from "./types";
+import { generateIcosahedronVertices } from "./helpers";
+import {
+ projectToSphere,
+ quatConjugate,
+ quatFromVectors,
+ quatMultiply,
+ quatNormalize,
+ quatRotateVec3,
+ quatSlerp,
+ vec3Normalize,
+} from "./maths";
+import { scheduleOnRN } from "react-native-worklets";
+
+const DiscComponent: React.FC = memo(
+ ({
+ x,
+ y,
+ radius,
+ alpha,
+ image,
+ }: IDiscComponent): React.ReactElement | null => {
+ const clipPath = useMemo(() => {
+ const path = Skia.Path.Make();
+ path.addCircle(x, y, radius);
+ return path;
+ }, [x, y, radius]);
+
+ if (radius < 1) return null;
+
+ if (!image) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ },
+);
+
+export const InfiniteMenu: React.FC &
+ React.FunctionComponent = memo(
+ ({ items, scale = 1, backgroundColor = "#000000" }: IInfiniteMenu) => {
+ const { width: screenWidth, height: screenHeight } =
+ Dimensions.get("window");
+ const centerX = screenWidth / 2;
+ const centerY = screenHeight / 2;
+
+ const imageUrls = useMemo(
+ () => items.map((item) => item.image),
+ [items],
+ );
+ const loadedImages = useLoadImages(imageUrls);
+
+ const [activeItem, setActiveItem] = useState(
+ items[0] || null,
+ );
+ const [isMoving, setIsMoving] = useState(false);
+ const [discData, setDiscData] = useState([]);
+ const SPHERE_RADIUS = 2 * scale;
+ const DISC_BASE_SCALE = 0.25;
+ const CAMERA_Z = 3 * scale;
+ const PROJECTION_SCALE = 150;
+ const sphereVertices = useMemo(
+ () => generateIcosahedronVertices(1, SPHERE_RADIUS),
+ [SPHERE_RADIUS],
+ );
+
+ const verticesRef = useMemo(() => [...sphereVertices], [sphereVertices]);
+
+ const qx = useSharedValue(0);
+ const qy = useSharedValue(0);
+ const qz = useSharedValue(0);
+ const qw = useSharedValue(1);
+
+ const prx = useSharedValue(0);
+ const pry = useSharedValue(0);
+ const prz = useSharedValue(0);
+ const prw = useSharedValue(1);
+
+ const rotVelocity = useSharedValue(0);
+ const isDown = useSharedValue(false);
+ const prevX = useSharedValue(0);
+ const prevY = useSharedValue(0);
+ const camZ = useSharedValue(CAMERA_Z);
+ const activeIdx = useSharedValue(0);
+
+ const updateActiveItem = useCallback(
+ (index: number) => {
+ if (items.length === 0) return;
+ const itemIndex = index % items.length;
+ setActiveItem(items[itemIndex]);
+ },
+ [items],
+ );
+
+ const updateIsMoving = useCallback((moving: boolean) => {
+ setIsMoving(moving);
+ }, []);
+
+ const updateDiscData = useCallback((data: IDisc[]) => {
+ setDiscData(data);
+ }, []);
+
+ const lastMoving = useSharedValue(false);
+ const frameSkip = useSharedValue(0);
+
+ useFrameCallback((info) => {
+ "worklet";
+ // Clamp dt to prevent physics jumps after idle
+ const rawDt = info.timeSincePreviousFrame || 16;
+ const dt = Math.min(rawDt, 50);
+ const ts = dt / 16 + 0.0001;
+ const IDENTITY: Quat = { x: 0, y: 0, z: 0, w: 1 };
+
+ const orientation: Quat = {
+ x: qx.value,
+ y: qy.value,
+ z: qz.value,
+ w: qw.value,
+ };
+ const pointerRot: Quat = {
+ x: prx.value,
+ y: pry.value,
+ z: prz.value,
+ w: prw.value,
+ };
+
+ const dampIntensity = 0.1 * ts;
+ const dampenedPR = quatSlerp(pointerRot, IDENTITY, dampIntensity);
+ prx.value = dampenedPR.x;
+ pry.value = dampenedPR.y;
+ prz.value = dampenedPR.z;
+ prw.value = dampenedPR.w;
+
+ let snapRot: Quat = IDENTITY;
+
+ if (!isDown.value) {
+ const snapDir: Vec3 = { x: 0, y: 0, z: -1 };
+ const invOrientation = quatConjugate(orientation);
+ const transformedSnapDir = quatRotateVec3(invOrientation, snapDir);
+
+ let maxDot = -Infinity;
+ let nearestIdx = 0;
+
+ for (let i = 0; i < verticesRef.length; i++) {
+ const v = verticesRef[i];
+ const dot =
+ transformedSnapDir.x * v.x +
+ transformedSnapDir.y * v.y +
+ transformedSnapDir.z * v.z;
+ if (dot > maxDot) {
+ maxDot = dot;
+ nearestIdx = i;
+ }
+ }
+
+ const nearestV = verticesRef[nearestIdx];
+ const worldV = quatRotateVec3(orientation, nearestV);
+ const targetDir = vec3Normalize(worldV);
+
+ const sqrDist =
+ (targetDir.x - snapDir.x) ** 2 +
+ (targetDir.y - snapDir.y) ** 2 +
+ (targetDir.z - snapDir.z) ** 2;
+ const distFactor = Math.max(0.1, 1 - sqrDist * 10);
+ const snapIntensity = 0.2 * ts * distFactor;
+ snapRot = quatFromVectors(targetDir, snapDir, snapIntensity);
+
+ const itemLen = Math.max(1, items.length);
+ const itemIdx = nearestIdx % itemLen;
+ if (activeIdx.value !== itemIdx) {
+ activeIdx.value = itemIdx;
+ scheduleOnRN(updateActiveItem, itemIdx);
+ }
+ }
+
+ const combined = quatMultiply(snapRot, dampenedPR);
+ const newOrientation = quatNormalize(quatMultiply(combined, orientation));
+ qx.value = newOrientation.x;
+ qy.value = newOrientation.y;
+ qz.value = newOrientation.z;
+ qw.value = newOrientation.w;
+
+ const rad = Math.acos(Math.min(1, Math.max(-1, combined.w))) * 2;
+ const rv = rad / (2 * Math.PI);
+ rotVelocity.value += (rv - rotVelocity.value) * 0.5 * ts;
+
+ const targetZ = isDown.value
+ ? CAMERA_Z + rotVelocity.value * 80 + 2.5
+ : CAMERA_Z;
+ const damping = isDown.value ? 7 / ts : 5 / ts;
+ camZ.value += (targetZ - camZ.value) / damping;
+
+ const moving = isDown.value || Math.abs(rotVelocity.value) > 0.005;
+ if (moving !== lastMoving.value) {
+ lastMoving.value = moving;
+ scheduleOnRN(updateIsMoving, moving);
+ }
+
+ if (!moving && !isDown.value && Math.abs(rotVelocity.value) < 0.001) {
+ frameSkip.value++;
+ if (frameSkip.value > 5) {
+ return;
+ }
+ } else {
+ frameSkip.value = 0;
+ }
+
+ const discs: IDisc[] = [];
+ const currentCamZ = camZ.value;
+ const itemLen = Math.max(1, items.length);
+
+ for (let i = 0; i < verticesRef.length; i++) {
+ const v = verticesRef[i];
+ const worldPos = quatRotateVec3(newOrientation, v);
+
+ const perspective = currentCamZ / (currentCamZ - worldPos.z);
+ const sx = centerX + worldPos.x * perspective * PROJECTION_SCALE;
+ const sy = centerY - worldPos.y * perspective * PROJECTION_SCALE;
+
+ const zFactor = (Math.abs(worldPos.z) / SPHERE_RADIUS) * 0.6 + 0.4;
+ const baseRadius =
+ zFactor * DISC_BASE_SCALE * perspective * PROJECTION_SCALE;
+
+ const alpha = Math.max(0.1, (worldPos.z / SPHERE_RADIUS) * 0.45 + 0.55);
+
+ discs.push({
+ screenX: sx,
+ screenY: sy,
+ radius: baseRadius,
+ alpha: alpha,
+ z: worldPos.z,
+ itemIndex: i % itemLen,
+ });
+ }
+ discs.sort((a, b) => a.z - b.z);
+ scheduleOnRN(updateDiscData, discs);
+ });
+
+ const panGesture = Gesture.Pan()
+ .onBegin((e) => {
+ "worklet";
+ prevX.value = e.x;
+ prevY.value = e.y;
+ isDown.value = true;
+ })
+ .onUpdate((e) => {
+ "worklet";
+ const intensity = 0.3;
+ const amplification = 5;
+
+ const midX = prevX.value + (e.x - prevX.value) * intensity;
+ const midY = prevY.value + (e.y - prevY.value) * intensity;
+
+ const dx = midX - prevX.value;
+ const dy = midY - prevY.value;
+
+ if (dx * dx + dy * dy > 0.1) {
+ const p = projectToSphere(midX, midY);
+ const q = projectToSphere(prevX.value, prevY.value);
+ const newRot = quatFromVectors(p, q, amplification);
+
+ prx.value = newRot.x;
+ pry.value = newRot.y;
+ prz.value = newRot.z;
+ prw.value = newRot.w;
+
+ prevX.value = midX;
+ prevY.value = midY;
+ }
+ })
+ .onEnd(() => {
+ "worklet";
+ isDown.value = false;
+ })
+ .onFinalize(() => {
+ "worklet";
+ isDown.value = false;
+ });
+
+ const fadeAnim = useMemo(
+ () => new RNAnimated.Value(1),
+ [],
+ );
+ const scaleAnim = useMemo(
+ () => new RNAnimated.Value(1),
+ [],
+ );
+
+ useEffect(() => {
+ RNAnimated.parallel([
+ RNAnimated.timing(fadeAnim, {
+ toValue: isMoving ? 0 : 1,
+ duration: isMoving ? 100 : 500,
+ useNativeDriver: true,
+ }),
+ RNAnimated.timing(scaleAnim, {
+ toValue: isMoving ? 0 : 1,
+ duration: isMoving ? 100 : 500,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ }, [isMoving, fadeAnim, scaleAnim]);
+
+ return (
+
+
+
+
+ {discData.map((disc, idx) => (
+
+ ))}
+
+
+
+
+ );
+ },
+);
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ canvas: {
+ flex: 1,
+ },
+});
+
+export default memo<
+ React.FC & React.FunctionComponent
+>(InfiniteMenu);
+
+export type { IMenuItem, IInfiniteMenu, IDisc, IDiscComponent };
\ No newline at end of file
diff --git a/src/components/ui/testimonials-card.tsx b/src/components/ui/testimonials-card.tsx
new file mode 100644
index 0000000..68692bb
--- /dev/null
+++ b/src/components/ui/testimonials-card.tsx
@@ -0,0 +1,153 @@
+import React, { useState, useMemo } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { cn } from "../../lib/utils";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+
+
+/**
+ * @typedef {Object} TestimonialItem
+ * @property {string|number} id
+ * @property {string} title
+ * @property {string} description
+ * @property {string} image
+ */
+
+/**
+ * @typedef {Object} TestimonialsCardProps
+ * @property {TestimonialItem[]} items
+ * @property {string} [className]
+ * @property {number} [width=400]
+ * @property {boolean} [showNavigation=true]
+ * @property {boolean} [showCounter=true]
+ * @property {boolean} [autoPlay=false]
+ * @property {number} [autoPlayInterval=3000]
+ */
+
+export function TestimonialsCard({
+items,
+className,
+width = 400,
+showNavigation = true,
+showCounter = true,
+autoPlay = false,
+autoPlayInterval = 3000,
+}) {
+const [activeIndex, setActiveIndex] = useState(0);
+const [direction, setDirection] = useState(1);
+const activeItem = items[activeIndex];
+
+React.useEffect(() => {
+ if (!autoPlay || items.length <= 1) return;
+ const interval = setInterval(() => {
+ setDirection(1);
+ setActiveIndex((prev) => (prev + 1) % items.length);
+ }, autoPlayInterval);
+ return () => clearInterval(interval);
+}, [autoPlay, autoPlayInterval, items.length]);
+
+const handleNext = () => {
+ if (activeIndex < items.length - 1) {
+ setDirection(1);
+ setActiveIndex(activeIndex + 1);
+ }
+};
+
+const handlePrev = () => {
+ if (activeIndex > 0) {
+ setDirection(-1);
+ setActiveIndex(activeIndex - 1);
+ }
+};
+
+const rotations = useMemo(() => [4, -2, -9, 7], []);
+
+if (!items || items.length === 0) return null;
+
+return (
+
+
+ {showCounter && (
+
+ {activeIndex + 1} / {items.length}
+
+ )}
+
+
+
+ {items.map((item, index) => {
+ const isActive = index === activeIndex;
+ const offset = index - activeIndex;
+ return (
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ {activeItem.title}
+ {activeItem.description}
+
+
+
+
+ {showNavigation && items.length > 1 && (
+
+ )}
+
+
+);
+}
+
+export default TestimonialsCard;
\ No newline at end of file
diff --git a/src/main.jsx b/src/main.jsx
index e7ec7be..744fbfe 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -2,6 +2,7 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
+import { Toaster } from 'react-hot-toast';
import './index.css'
import 'lenis/dist/lenis.css';
import ScrollToTop from "./components/ScrollToTop";
@@ -11,6 +12,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
+
diff --git a/src/pages/AdminDashboard.jsx b/src/pages/AdminDashboard.jsx
index ffab87c..4f97a9d 100644
--- a/src/pages/AdminDashboard.jsx
+++ b/src/pages/AdminDashboard.jsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
+import { toast } from 'react-hot-toast';
import { FloatingNav } from '../components/FloatingNavbar';
import {
IconHome,
@@ -116,7 +117,7 @@ export default function AdminDashboard() {
await axios.delete(`${API_URL}/admin/users/${id}`, getAuthHeader());
fetchData();
} catch (error) {
- alert(error.response?.data?.message || 'Failed to delete user');
+ toast.error(error.response?.data?.message || 'Failed to delete user');
}
};
@@ -127,7 +128,7 @@ export default function AdminDashboard() {
await axios.delete(`${API_URL}/events/${id}`, getAuthHeader());
fetchData();
} catch (error) {
- alert('Failed to delete event');
+ toast.error('Failed to delete event');
}
};
@@ -136,7 +137,7 @@ export default function AdminDashboard() {
await axios.put(`${API_URL}/admin/registrations/${id}/status`, { status }, getAuthHeader());
fetchData();
} catch (error) {
- alert('Failed to update status');
+ toast.error('Failed to update status');
}
};
@@ -147,7 +148,7 @@ export default function AdminDashboard() {
await axios.delete(`${API_URL}/admin/registrations/${id}`, getAuthHeader());
fetchData();
} catch (error) {
- alert('Failed to delete registration');
+ toast.error('Failed to delete registration');
}
};
@@ -421,7 +422,7 @@ function CreateEventModal({ onClose }) {
'Content-Type': 'multipart/form-data'
}
});
- alert('Event created successfully!');
+ toast.success('Event created successfully!');
onClose();
} catch (error) {
setError(error.response?.data?.message || 'Failed to create event');
diff --git a/src/pages/ContactPage.jsx b/src/pages/ContactPage.jsx
index 931307d..32c7bf9 100644
--- a/src/pages/ContactPage.jsx
+++ b/src/pages/ContactPage.jsx
@@ -175,44 +175,26 @@ export default function ContactPage() {
}
};
- const scrollToContact = () => {
- document.getElementById("contact-section").scrollIntoView({
- behavior: "smooth",
- });
- };
-
return (
-
-
+
+
-
-
+ {/* Single Scrollable Contact Section */}
+
+
+ {/* Heading */}
+
Get In Touch
+
+ {/* Subheading */}
+
+ Have a question or want to work together? We'd love to hear from you.
+
-
- Send Message
-
-
-
-