Merge branch 'main' into feature/freshers-qa
@@ -15,7 +15,7 @@ Welcome to the team! This guide explains how to get set up, which Git branch you
|
||||
2. **Select your module's branch**:
|
||||
Find your assigned module below, and check out your branch:
|
||||
```bash
|
||||
git checkout feature/<your-branch-name>
|
||||
|
||||
```
|
||||
|
||||
3. **Install frontend dependencies**:
|
||||
|
||||
3
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
14
backend/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"type": "java",
|
||||
"name": "Spring Boot-PortalApplication<portal>",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"mainClass": "com.rit.portal.PortalApplication",
|
||||
"projectName": "portal",
|
||||
"args": "",
|
||||
"envFile": "${workspaceFolder}/.env"
|
||||
}
|
||||
]
|
||||
}
|
||||
295
backend/mvnw
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
189
backend/mvnw.cmd
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
@@ -1,35 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
<version>3.4.13</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.rit</groupId>
|
||||
<artifactId>portal</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>Freshers Hub Backend</name>
|
||||
<description>Centralized Spring Boot backend for RIT Freshers Hub</description>
|
||||
<url/>
|
||||
<url />
|
||||
<licenses>
|
||||
<license/>
|
||||
<license />
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
<developer />
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
<connection />
|
||||
<developerConnection />
|
||||
<tag />
|
||||
<url />
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web MVC starter for building REST APIs -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
package com.rit.portal.config;
|
||||
|
||||
import com.rit.portal.entity.NotePyq;
|
||||
import com.rit.portal.entity.BusRoute;
|
||||
import com.rit.portal.entity.BusStop;
|
||||
import com.rit.portal.repository.NotePyqRepository;
|
||||
import com.rit.portal.repository.BusRouteRepository;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class DataInitializer implements CommandLineRunner {
|
||||
@@ -14,6 +23,9 @@ public class DataInitializer implements CommandLineRunner {
|
||||
@Autowired
|
||||
private NotePyqRepository noteRepository;
|
||||
|
||||
@Autowired
|
||||
private BusRouteRepository busRouteRepository;
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
if (noteRepository.count() == 0) {
|
||||
@@ -109,5 +121,52 @@ public class DataInitializer implements CommandLineRunner {
|
||||
));
|
||||
System.out.println("🌱 Database successfully seeded with Notes & PYQs test data!");
|
||||
}
|
||||
|
||||
// Seed Bus Routes
|
||||
if (busRouteRepository.count() == 0) {
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
InputStream is = getClass().getResourceAsStream("/bus_routes.json");
|
||||
if (is != null) {
|
||||
List<Map<String, Object>> routesList = mapper.readValue(is, new TypeReference<List<Map<String, Object>>>() {});
|
||||
List<BusRoute> routesToSave = new ArrayList<>();
|
||||
|
||||
for (Map<String, Object> routeMap : routesList) {
|
||||
BusRoute br = BusRoute.builder()
|
||||
.number((String) routeMap.get("number"))
|
||||
.name((String) routeMap.get("name"))
|
||||
.from((String) routeMap.get("from"))
|
||||
.to((String) routeMap.get("to"))
|
||||
.departureTime((String) routeMap.get("departureTime"))
|
||||
.arrivalTime((String) routeMap.get("arrivalTime"))
|
||||
.color((String) routeMap.get("color"))
|
||||
.build();
|
||||
|
||||
List<Map<String, String>> stopsList = (List<Map<String, String>>) routeMap.get("stops");
|
||||
List<BusStop> stops = new ArrayList<>();
|
||||
if (stopsList != null) {
|
||||
for (int i = 0; i < stopsList.size(); i++) {
|
||||
Map<String, String> stopMap = stopsList.get(i);
|
||||
stops.add(BusStop.builder()
|
||||
.route(br)
|
||||
.name(stopMap.get("name"))
|
||||
.time(stopMap.get("time"))
|
||||
.stopOrder(i + 1)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
br.setStops(stops);
|
||||
routesToSave.add(br);
|
||||
}
|
||||
busRouteRepository.saveAll(routesToSave);
|
||||
System.out.println("🌱 Database successfully seeded with " + routesToSave.size() + " Bus Routes and stops!");
|
||||
} else {
|
||||
System.err.println("⚠️ Could not find bus_routes.json in resources!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ Failed to seed bus routes: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.rit.portal.controller;
|
||||
|
||||
import com.rit.portal.entity.BusRoute;
|
||||
import com.rit.portal.repository.BusRouteRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/bus-routes")
|
||||
public class BusRouteController {
|
||||
|
||||
@Autowired
|
||||
private BusRouteRepository busRouteRepository;
|
||||
|
||||
@GetMapping
|
||||
public List<BusRoute> getAllBusRoutes() {
|
||||
return busRouteRepository.findAll();
|
||||
}
|
||||
}
|
||||
46
backend/src/main/java/com/rit/portal/entity/BusRoute.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.rit.portal.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "bus_routes")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class BusRoute {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@Column(name = "route_number", unique = true, nullable = false)
|
||||
private String number;
|
||||
|
||||
@Column(name = "route_name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "start_point", nullable = false)
|
||||
private String from;
|
||||
|
||||
@Column(name = "end_point", nullable = false)
|
||||
private String to;
|
||||
|
||||
@Column(name = "departure_time", nullable = false)
|
||||
private String departureTime;
|
||||
|
||||
@Column(name = "arrival_time", nullable = false)
|
||||
private String arrivalTime;
|
||||
|
||||
@Column(name = "color_code")
|
||||
private String color;
|
||||
|
||||
@OneToMany(mappedBy = "route", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
|
||||
@OrderBy("stopOrder ASC")
|
||||
@Builder.Default
|
||||
private List<BusStop> stops = new ArrayList<>();
|
||||
}
|
||||
33
backend/src/main/java/com/rit/portal/entity/BusStop.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.rit.portal.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "bus_stops")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class BusStop {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "route_id", nullable = false)
|
||||
@JsonIgnore
|
||||
private BusRoute route;
|
||||
|
||||
@Column(name = "stop_name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "arrival_time", nullable = false)
|
||||
private String time;
|
||||
|
||||
@Column(name = "stop_order", nullable = false)
|
||||
private Integer stopOrder;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.rit.portal.repository;
|
||||
|
||||
import com.rit.portal.entity.BusRoute;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface BusRouteRepository extends JpaRepository<BusRoute, Integer> {
|
||||
BusRoute findByNumber(String number);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.rit.portal.repository;
|
||||
|
||||
import com.rit.portal.entity.BusStop;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface BusStopRepository extends JpaRepository<BusStop, Integer> {
|
||||
}
|
||||
2771
backend/src/main/resources/bus_routes.json
Normal file
@@ -0,0 +1,14 @@
|
||||
com\rit\portal\config\DataInitializer.class
|
||||
com\rit\portal\entity\CommunityQuestion$CommunityQuestionBuilder.class
|
||||
com\rit\portal\entity\NotePyq$NotePyqBuilder.class
|
||||
com\rit\portal\repository\CommunityAnswerRepository.class
|
||||
com\rit\portal\entity\CommunityAnswer$CommunityAnswerBuilder.class
|
||||
com\rit\portal\controller\NotePyqController.class
|
||||
com\rit\portal\entity\CommunityQuestion.class
|
||||
com\rit\portal\entity\NotePyq.class
|
||||
com\rit\portal\repository\NotePyqRepository.class
|
||||
com\rit\portal\entity\CommunityAnswer.class
|
||||
com\rit\portal\controller\CommunityQuestionController.class
|
||||
com\rit\portal\repository\CommunityQuestionRepository.class
|
||||
com\rit\portal\config\WebConfig.class
|
||||
com\rit\portal\PortalApplication.class
|
||||
@@ -0,0 +1,11 @@
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\config\DataInitializer.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\config\WebConfig.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\CommunityQuestionController.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\NotePyqController.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\CommunityAnswer.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\CommunityQuestion.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\NotePyq.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\PortalApplication.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\CommunityAnswerRepository.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\CommunityQuestionRepository.java
|
||||
C:\Users\deves\OneDrive\project_main\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\NotePyqRepository.java
|
||||
BIN
public/clubs/artist_league.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/clubs/fusion.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
public/clubs/helios.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/clubs/infinitus.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
public/clubs/mediastic.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/clubs/nippon.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
public/clubs/nss.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
public/clubs/podx.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/clubs/rotaract.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/clubs/steam.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
public/clubs/stellar_space_tech.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
public/clubs/techspark.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/clubs/unnat_bharat.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/clubs/vaarithi.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/clubs/wec.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/clubs/wistem.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
public/clubs/yuva.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
@@ -3,7 +3,7 @@
|
||||
|
||||
-- 1. Notes & PYQs Module Table
|
||||
CREATE TABLE IF NOT EXISTS notes_pyqs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
subject VARCHAR(250) NOT NULL,
|
||||
department VARCHAR(250) NOT NULL,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { Mail, Phone, Building2, User } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Mail, Phone, Building2, User, X, BookOpen, ChevronRight, Monitor, Command, GraduationCap, Briefcase, Microscope, Clock } from 'lucide-react';
|
||||
import type { Faculty } from '@/types';
|
||||
|
||||
interface FacultyCardProps {
|
||||
@@ -16,6 +18,9 @@ const AVATAR_COLORS = [
|
||||
];
|
||||
|
||||
export default function FacultyCard({ faculty }: FacultyCardProps) {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showEmailOptions, setShowEmailOptions] = useState(false);
|
||||
|
||||
const colorIndex = parseInt(faculty.id) % AVATAR_COLORS.length;
|
||||
const [from, to] = AVATAR_COLORS[colorIndex];
|
||||
const initials = faculty.name
|
||||
@@ -24,73 +29,359 @@ export default function FacultyCard({ faculty }: FacultyCardProps) {
|
||||
.map((n) => n[0])
|
||||
.join('');
|
||||
|
||||
const mailOptions = [
|
||||
{ name: 'Default Mail App', url: `mailto:${faculty.email}`, icon: Monitor, color: 'text-gray-700', bg: 'bg-gray-100' },
|
||||
{ name: 'Gmail', url: `https://mail.google.com/mail/?view=cm&fs=1&to=${faculty.email}`, icon: Mail, color: 'text-red-500', bg: 'bg-red-50' },
|
||||
{ name: 'Outlook', url: `https://outlook.office.com/mail/deeplink/compose?to=${faculty.email}`, icon: Mail, color: 'text-blue-600', bg: 'bg-blue-50' },
|
||||
{ name: 'Yahoo Mail', url: `https://compose.mail.yahoo.com/?to=${faculty.email}`, icon: Mail, color: 'text-purple-600', bg: 'bg-purple-50' },
|
||||
];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ y: -5, boxShadow: '0 16px 40px -8px rgba(0,0,0,0.12)' }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="bg-white border border-[#E5E7EB] rounded-2xl p-5 flex flex-col gap-4"
|
||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
||||
>
|
||||
{/* Avatar + Name */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-bold text-lg shrink-0"
|
||||
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{initials}
|
||||
<>
|
||||
<motion.div
|
||||
whileHover={{ y: -6, boxShadow: '0 20px 40px -10px rgba(0,0,0,0.1)' }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="bg-white border border-[#E8ECF4] rounded-[20px] p-5 flex flex-col gap-4 relative"
|
||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
||||
>
|
||||
{/* Avatar + Name */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<div
|
||||
className="w-14 h-14 rounded-2xl flex items-center justify-center text-white font-bold text-lg shrink-0 shadow-sm"
|
||||
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
{/* Online Indicator */}
|
||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-[#22C55E] border-2 border-white rounded-full z-10 shadow-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-[#1E293B] text-sm leading-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{faculty.name}
|
||||
</h3>
|
||||
<p className="text-xs text-[#FF7A00] font-medium mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{faculty.designation}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-[#1E293B] text-sm leading-tight" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{faculty.name}
|
||||
</h3>
|
||||
<p className="text-xs text-[#F97316] font-medium mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{faculty.designation}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-xs text-[#475569]">
|
||||
<Building2 className="w-3.5 h-3.5 text-[#94A3B8] shrink-0" />
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</span>
|
||||
</div>
|
||||
{faculty.office && (
|
||||
{/* Details */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-xs text-[#475569]">
|
||||
<User className="w-3.5 h-3.5 text-[#94A3B8] shrink-0" />
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.office}</span>
|
||||
<Building2 className="w-3.5 h-3.5 text-[#94A3B8] shrink-0" />
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</span>
|
||||
</div>
|
||||
)}
|
||||
{faculty.specialization && (
|
||||
<div className="text-xs text-[#94A3B8] pl-5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{faculty.specialization}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-[#E5E7EB]">
|
||||
<a
|
||||
href={`mailto:${faculty.email}`}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all hover:bg-[#FFF7ED]"
|
||||
style={{ color: '#F97316', fontFamily: 'Poppins, sans-serif' }}
|
||||
aria-label={`Email ${faculty.name}`}
|
||||
>
|
||||
<Mail className="w-3.5 h-3.5" />
|
||||
Email
|
||||
</a>
|
||||
{faculty.phone && (
|
||||
<a
|
||||
href={`tel:${faculty.phone}`}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium text-[#475569] transition-all hover:bg-gray-50"
|
||||
{/* Contact & Actions */}
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-[#E5E7EB] mt-auto">
|
||||
<button
|
||||
onClick={() => setShowEmailOptions(true)}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium text-[#475569] transition-all hover:bg-gray-50 border border-transparent hover:border-gray-200"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
aria-label={`Call ${faculty.name}`}
|
||||
aria-label={`Email ${faculty.name}`}
|
||||
>
|
||||
<Phone className="w-3.5 h-3.5" />
|
||||
Call
|
||||
</a>
|
||||
<Mail className="w-3.5 h-3.5" />
|
||||
Email
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowModal(true)}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 py-2 rounded-xl text-xs font-medium transition-all hover:bg-[#FFF7ED]"
|
||||
style={{ color: '#F97316', fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
View Profile
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Profile Modal */}
|
||||
{createPortal(
|
||||
<AnimatePresence>
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setShowModal(false)}
|
||||
className="absolute inset-0 bg-[#0F172A]/40 backdrop-blur-sm"
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ type: "spring", duration: 0.4, bounce: 0.2 }}
|
||||
className="relative w-full max-w-5xl bg-[#F8FAFC] shadow-2xl overflow-y-auto"
|
||||
style={{ maxHeight: '90vh', borderRadius: '24px' }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="absolute top-4 right-4 p-2 bg-white hover:bg-gray-100 rounded-full transition-colors z-50 shadow-sm border border-gray-200"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 w-full p-4 sm:p-8 relative">
|
||||
{/* LEFT PANEL */}
|
||||
<div className="lg:col-span-4 h-full relative">
|
||||
<div
|
||||
className="rounded-[20px] shadow-sm border p-8 flex flex-col items-center relative lg:sticky lg:top-0"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${from}10, ${to}10)`,
|
||||
borderColor: `${from}20`
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-32 h-32 rounded-full flex items-center justify-center text-white font-bold text-5xl mb-5 shadow-lg"
|
||||
style={{ background: `linear-gradient(135deg, ${from}, ${to})`, fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-center text-[#1E293B] mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{faculty.name}
|
||||
</h2>
|
||||
<p className="text-base font-semibold text-[#FF7A00] text-center" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{faculty.designation}
|
||||
</p>
|
||||
<p className="text-xs font-semibold text-[#64748B] text-center mt-2 uppercase tracking-wider" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{faculty.department}
|
||||
</p>
|
||||
|
||||
<hr className="w-full my-6" style={{ borderColor: `${from}20` }} />
|
||||
|
||||
<div className="w-full flex flex-col gap-3">
|
||||
<button
|
||||
onClick={() => setShowEmailOptions(true)}
|
||||
className="flex items-center gap-4 p-3 rounded-xl hover:bg-white/80 transition-colors border group text-left w-full"
|
||||
style={{ backgroundColor: 'rgba(255, 255, 255, 0.6)', borderColor: `${from}15` }}
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-[#FFF7ED] flex items-center justify-center group-hover:bg-[#F97316] transition-colors shrink-0">
|
||||
<Mail className="w-4 h-4 text-[#F97316] group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<p className="text-[11px] font-semibold text-[#94A3B8] uppercase tracking-wider mb-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>Email Address</p>
|
||||
<p className="text-sm font-semibold text-[#1E293B] truncate" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.email}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{faculty.phone && (
|
||||
<a
|
||||
href={`tel:${faculty.phone}`}
|
||||
className="flex items-center gap-4 p-3 rounded-xl hover:bg-white/80 transition-colors border group w-full"
|
||||
style={{ backgroundColor: 'rgba(255, 255, 255, 0.6)', borderColor: `${from}15` }}
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center group-hover:bg-blue-500 transition-colors shrink-0">
|
||||
<Phone className="w-4 h-4 text-blue-500 group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<p className="text-[11px] font-semibold text-[#94A3B8] uppercase tracking-wider mb-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>Phone Number</p>
|
||||
<p className="text-sm font-semibold text-[#1E293B] truncate" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.phone}</p>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT PANEL */}
|
||||
<div className="lg:col-span-8 flex flex-col gap-6">
|
||||
{/* Section 1: Professional Details */}
|
||||
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-orange-50 flex items-center justify-center shrink-0">
|
||||
<User className="w-5 h-5 text-[#FF7A00]" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Professional Details</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
||||
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Department</p>
|
||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.department}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
||||
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Specialization</p>
|
||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.specialization || 'Not specified'}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
||||
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Qualification</p>
|
||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.qualification || 'Not specified'}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50">
|
||||
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Experience</p>
|
||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.experience || 'Not specified'}</p>
|
||||
</div>
|
||||
{faculty.joinedInstitution && (
|
||||
<div className="p-4 rounded-xl bg-gray-50/80 border border-gray-100 transition-colors hover:bg-gray-50 md:col-span-2">
|
||||
<p className="text-xs font-semibold text-[#94A3B8] uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Joined Institution</p>
|
||||
<p className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.joinedInstitution}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Academic Info */}
|
||||
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-50 flex items-center justify-center shrink-0">
|
||||
<BookOpen className="w-5 h-5 text-indigo-500" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Academic Information</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="p-5 rounded-xl bg-indigo-50/30 border border-indigo-100/50">
|
||||
<p className="text-xs font-bold text-indigo-400 uppercase tracking-wider mb-2" style={{ fontFamily: 'Inter, sans-serif' }}>Subjects Handling</p>
|
||||
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.subjectsHandling || 'Not specified'}</p>
|
||||
</div>
|
||||
<div className="p-5 rounded-xl bg-teal-50/30 border border-teal-100/50">
|
||||
<p className="text-xs font-bold text-teal-500 uppercase tracking-wider mb-2" style={{ fontFamily: 'Inter, sans-serif' }}>Research Areas</p>
|
||||
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif', lineHeight: '1.6' }}>{faculty.researchAreas || 'Not specified'}</p>
|
||||
</div>
|
||||
{faculty.studentsGuided && (
|
||||
<div className="p-5 rounded-xl bg-blue-50/30 border border-blue-100/50">
|
||||
<p className="text-xs font-bold text-blue-500 uppercase tracking-wider mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>Students Guided</p>
|
||||
<div className="flex gap-8">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-2xl font-bold text-[#1E293B]">{faculty.studentsGuided.ug}</span>
|
||||
<span className="text-xs font-semibold text-[#64748B]">UG</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-2xl font-bold text-[#1E293B]">{faculty.studentsGuided.pg}</span>
|
||||
<span className="text-xs font-semibold text-[#64748B]">PG</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-2xl font-bold text-[#1E293B]">{faculty.studentsGuided.phd}</span>
|
||||
<span className="text-xs font-semibold text-[#64748B]">Ph.D.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Achievements (Render only if present) */}
|
||||
{(faculty.achievements || faculty.publications) && (
|
||||
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-pink-50 flex items-center justify-center shrink-0">
|
||||
<GraduationCap className="w-5 h-5 text-pink-500" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Achievements</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6">
|
||||
{faculty.achievements && faculty.achievements.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-bold text-pink-400 uppercase tracking-wider mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>Awards & Certifications</p>
|
||||
<ul className="list-disc list-inside space-y-2">
|
||||
{faculty.achievements.map((ach, i) => (
|
||||
<li key={i} className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{ach}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{faculty.publications && faculty.publications.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs font-bold text-pink-400 uppercase tracking-wider mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>Recent Publications</p>
|
||||
<ul className="list-disc list-inside space-y-2">
|
||||
{faculty.publications.map((pub, i) => (
|
||||
<li key={i} className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{pub}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 4: Office Info */}
|
||||
<div className="bg-white rounded-[20px] shadow-sm border border-[#E8ECF4] p-6 sm:p-8">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-50 flex items-center justify-center shrink-0">
|
||||
<Clock className="w-5 h-5 text-emerald-500" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Office Information</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-5 rounded-xl bg-emerald-50/50 border border-emerald-100 flex items-start gap-4">
|
||||
<Clock className="w-6 h-6 text-emerald-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-xs font-bold text-emerald-600 uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Office Hours</p>
|
||||
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.officeHours || 'Not specified'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 rounded-xl bg-blue-50/30 border border-blue-100/50 flex items-start gap-4">
|
||||
<Building2 className="w-6 h-6 text-blue-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-xs font-bold text-blue-500 uppercase tracking-wider mb-1" style={{ fontFamily: 'Inter, sans-serif' }}>Office Location</p>
|
||||
<p className="text-sm font-medium text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>{faculty.office || 'Not specified'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* Email Options Modal */}
|
||||
{createPortal(
|
||||
<AnimatePresence>
|
||||
{showEmailOptions && (
|
||||
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={() => setShowEmailOptions(false)}
|
||||
className="absolute inset-0 bg-[#0F172A]/40 backdrop-blur-sm"
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ type: "spring", duration: 0.4, bounce: 0.2 }}
|
||||
className="relative w-full max-w-sm bg-white rounded-3xl shadow-2xl p-6 flex flex-col gap-5"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Choose Mail App</h3>
|
||||
<p className="text-xs text-[#64748B] mt-0.5" style={{ fontFamily: 'Inter, sans-serif' }}>Compose email to {faculty.name}</p>
|
||||
</div>
|
||||
<button onClick={() => setShowEmailOptions(false)} className="p-2 rounded-full hover:bg-gray-100 transition-colors">
|
||||
<X className="w-5 h-5 text-[#94A3B8]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{mailOptions.map((option, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={option.url}
|
||||
target={option.name !== 'Default Mail App' ? '_blank' : undefined}
|
||||
rel={option.name !== 'Default Mail App' ? 'noopener noreferrer' : undefined}
|
||||
onClick={() => setShowEmailOptions(false)}
|
||||
className="flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 border border-gray-100 transition-all group hover:border-gray-200"
|
||||
>
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center shrink-0 ${option.bg}`}>
|
||||
<option.icon className={`w-4 h-4 ${option.color}`} />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-[#1E293B] group-hover:text-black" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{option.name}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ interface FeatureCardProps {
|
||||
}
|
||||
|
||||
export default function FeatureCard({ feature }: FeatureCardProps) {
|
||||
const IconComponent = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[feature.icon];
|
||||
const IconComponent = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string; style?: React.CSSProperties }>>)[feature.icon];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
@@ -6,7 +6,7 @@ export const NAV_LINKS = [
|
||||
{ label: 'Notes', path: '/notes' },
|
||||
{ label: 'AI Assistant', path: '/ai-assistant' },
|
||||
{ label: 'Campus', path: '/campus' },
|
||||
{ label: 'Events', path: '/events' },
|
||||
{ label: 'Clubs', path: '/events' },
|
||||
{ label: 'Community', path: '/community' },
|
||||
];
|
||||
|
||||
@@ -77,9 +77,9 @@ export const FEATURES: Feature[] = [
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
icon: 'Calendar',
|
||||
title: 'Clubs & Events',
|
||||
description: 'Discover clubs, upcoming events, workshops, and cultural programs at RIT.',
|
||||
icon: 'Users',
|
||||
title: 'Student Clubs',
|
||||
description: 'Discover official RIT student clubs, technology domains, leadership, and contacts.',
|
||||
path: '/events',
|
||||
color: '#EF4444',
|
||||
bgColor: '#FEF2F2',
|
||||
@@ -179,55 +179,302 @@ export const FACULTY_DATA: Faculty[] = [
|
||||
name: 'Dr. S. Ramesh Kumar',
|
||||
designation: 'Professor & HOD',
|
||||
department: 'Computer Science & Engineering',
|
||||
email: 'ramesh@rit.edu',
|
||||
office: 'CSE Block, Room 301',
|
||||
specialization: 'Machine Learning, AI',
|
||||
specialization: 'Machine Learning, Artificial Intelligence',
|
||||
email: 'ramesh@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '18 Years',
|
||||
subjectsHandling: 'Machine Learning, Artificial Intelligence, Data Mining',
|
||||
researchAreas: 'AI, Deep Learning, Computer Vision',
|
||||
officeHours: 'Mon–Fri, 10:00 AM–12:00 PM',
|
||||
phone: '+91 98765 43001',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Dr. P. Mahalakshmi',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Electronics & Communication',
|
||||
email: 'mahalakshmi@rit.edu',
|
||||
office: 'ECE Block, Room 205',
|
||||
specialization: 'VLSI Design, Embedded Systems',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Prof. R. Vijayakumar',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Mechanical Engineering',
|
||||
email: 'vijay@rit.edu',
|
||||
office: 'MECH Block, Room 102',
|
||||
specialization: 'CAD/CAM, Robotics',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Dr. K. Anitha',
|
||||
designation: 'Professor',
|
||||
department: 'Information Technology',
|
||||
email: 'anitha@rit.edu',
|
||||
office: 'IT Block, Room 401',
|
||||
specialization: 'Data Science, Cloud Computing',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Prof. M. Selvakumar',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Civil Engineering',
|
||||
email: 'selva@rit.edu',
|
||||
office: 'CIVIL Block, Room 110',
|
||||
specialization: 'Structural Engineering',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Dr. S. Priya',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Computer Science & Engineering',
|
||||
email: 'priya@rit.edu',
|
||||
office: 'CSE Block, Room 205',
|
||||
specialization: 'Cyber Security, Networks',
|
||||
specialization: 'Cyber Security, Computer Networks',
|
||||
email: 'priya@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '8 Years',
|
||||
subjectsHandling: 'Network Security, Computer Networks, Ethical Hacking',
|
||||
researchAreas: 'Cyber Security, IoT Security',
|
||||
officeHours: 'Mon–Fri, 2:00 PM–4:00 PM',
|
||||
phone: '+91 98765 43002',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Dr. K. Aravind',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Computer Science & Engineering',
|
||||
office: 'CSE Block, Room 212',
|
||||
specialization: 'Cloud Computing, Distributed Systems',
|
||||
email: 'aravind@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '11 Years',
|
||||
subjectsHandling: 'Cloud Computing, Distributed Systems, Operating Systems',
|
||||
researchAreas: 'Cloud Infrastructure, Edge Computing',
|
||||
officeHours: 'Mon–Fri, 9:30 AM–11:30 AM',
|
||||
phone: '+91 98765 43003',
|
||||
},
|
||||
|
||||
{
|
||||
id: '6',
|
||||
name: 'Dr. P. Mahalakshmi',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Electronics & Communication Engineering',
|
||||
office: 'ECE Block, Room 205',
|
||||
specialization: 'VLSI Design, Embedded Systems',
|
||||
email: 'mahalakshmi@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '13 Years',
|
||||
subjectsHandling: 'VLSI Design, Embedded Systems',
|
||||
researchAreas: 'Chip Design, IoT Devices',
|
||||
officeHours: 'Mon–Fri, 10:00 AM–12:00 PM',
|
||||
phone: '+91 98765 43006',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: 'Dr. V. Prakash',
|
||||
designation: 'Professor',
|
||||
department: 'Electronics & Communication Engineering',
|
||||
office: 'ECE Block, Room 310',
|
||||
specialization: 'Digital Signal Processing, IoT',
|
||||
email: 'prakash@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '17 Years',
|
||||
subjectsHandling: 'DSP, IoT, Microprocessors',
|
||||
researchAreas: 'Wireless Systems, Signal Processing',
|
||||
officeHours: 'Mon–Fri, 2:00 PM–4:00 PM',
|
||||
phone: '+91 98765 43007',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: 'Dr. A. Deepika',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Electronics & Communication Engineering',
|
||||
office: 'ECE Block, Room 214',
|
||||
specialization: 'Wireless Communication, 5G Networks',
|
||||
email: 'deepika@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '6 Years',
|
||||
subjectsHandling: 'Wireless Networks, Mobile Communication',
|
||||
researchAreas: '5G, IoT Networks',
|
||||
officeHours: 'Mon–Fri, 9:00 AM–11:00 AM',
|
||||
phone: '+91 98765 43008',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
name: 'Prof. R. Vijayakumar',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Mechanical Engineering',
|
||||
office: 'MECH Block, Room 102',
|
||||
specialization: 'CAD/CAM, Robotics',
|
||||
email: 'vijayakumar@rit.edu',
|
||||
qualification: 'B.E., M.E.',
|
||||
experience: '9 Years',
|
||||
subjectsHandling: 'CAD/CAM, Robotics',
|
||||
researchAreas: 'Industrial Automation',
|
||||
officeHours: 'Mon–Fri, 11:00 AM–1:00 PM',
|
||||
phone: '+91 98765 43009',
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
name: 'Dr. S. Manikandan',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Mechanical Engineering',
|
||||
office: 'MECH Block, Room 118',
|
||||
specialization: 'Thermal Engineering, Manufacturing',
|
||||
email: 'manikandan@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '12 Years',
|
||||
subjectsHandling: 'Manufacturing Technology, Thermal Engineering',
|
||||
researchAreas: 'Renewable Energy, Thermal Systems',
|
||||
officeHours: 'Mon–Fri, 1:00 PM–3:00 PM',
|
||||
phone: '+91 98765 43010',
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
name: 'Prof. M. Selvakumar',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Civil Engineering',
|
||||
office: 'CIVIL Block, Room 110',
|
||||
specialization: 'Structural Engineering',
|
||||
email: 'selvakumar@rit.edu',
|
||||
qualification: 'B.E., M.E.',
|
||||
experience: '14 Years',
|
||||
subjectsHandling: 'Structural Analysis, RCC Design',
|
||||
researchAreas: 'Earthquake Resistant Structures',
|
||||
officeHours: 'Mon–Fri, 10:00 AM–12:00 PM',
|
||||
phone: '+91 98765 43011',
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
name: 'Dr. J. Revathi',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Civil Engineering',
|
||||
office: 'CIVIL Block, Room 123',
|
||||
specialization: 'Environmental Engineering, Surveying',
|
||||
email: 'revathi@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '8 Years',
|
||||
subjectsHandling: 'Surveying, Environmental Engineering',
|
||||
researchAreas: 'Water Resource Management',
|
||||
officeHours: 'Mon–Fri, 2:00 PM–4:00 PM',
|
||||
phone: '+91 98765 43012',
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
name: 'Dr. N. Hariharan',
|
||||
designation: 'Professor & HOD',
|
||||
department: 'Artificial Intelligence & Data Science',
|
||||
office: 'AIDS Block, Room 501',
|
||||
specialization: 'Deep Learning, Data Mining',
|
||||
email: 'hariharan@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '19 Years',
|
||||
subjectsHandling: 'Deep Learning, Data Mining, AI',
|
||||
researchAreas: 'Artificial Intelligence, Predictive Analytics',
|
||||
officeHours: 'Mon–Fri, 9:30 AM–11:30 AM',
|
||||
phone: '+91 98765 43013',
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
name: 'Dr. P. Divya',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Artificial Intelligence & Data Science',
|
||||
office: 'AIDS Block, Room 505',
|
||||
specialization: 'Natural Language Processing, Computer Vision',
|
||||
email: 'divya@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '7 Years',
|
||||
subjectsHandling: 'NLP, Computer Vision',
|
||||
researchAreas: 'Generative AI, Image Processing',
|
||||
officeHours: 'Mon–Fri, 1:00 PM–3:00 PM',
|
||||
phone: '+91 98765 43014',
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
name: 'Dr. L. Karthikeyan',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Electrical & Electronics Engineering',
|
||||
office: 'EEE Block, Room 303',
|
||||
specialization: 'Power Systems, Renewable Energy',
|
||||
email: 'karthikeyan@rit.edu',
|
||||
qualification: 'B.E., M.E., Ph.D.',
|
||||
experience: '15 Years',
|
||||
subjectsHandling: 'Power Systems, Electrical Machines',
|
||||
researchAreas: 'Smart Grid, Solar Energy',
|
||||
officeHours: 'Mon–Fri, 10:00 AM–12:00 PM',
|
||||
phone: '+91 98765 43015',
|
||||
},
|
||||
{
|
||||
id: '16',
|
||||
name: 'Dr. M. Murugan',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Computer Science & Engineering',
|
||||
office: 'CSE Block, Room 218',
|
||||
specialization: 'Artificial Intelligence, Machine Learning',
|
||||
email: 'murugan@rit.edu',
|
||||
qualification: 'B.E. (CSE), M.E. (Computer Science & Engineering), Ph.D. (Artificial Intelligence)',
|
||||
experience: '13 Years',
|
||||
subjectsHandling: 'Artificial Intelligence, Machine Learning, Python Programming, Data Mining',
|
||||
researchAreas: 'Deep Learning, Computer Vision, Natural Language Processing, Explainable AI',
|
||||
officeHours: 'Mon–Fri, 10:00 AM–12:00 PM',
|
||||
phone: '+91 98765 43125',
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
name: 'Dr. R. Balamurugan',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Computer Science & Business Systems',
|
||||
office: 'CSBS Block, Room 305',
|
||||
specialization: 'Business Analytics, Data Science',
|
||||
email: 'balamurugan@rit.edu',
|
||||
qualification: 'B.E. (CSE), M.B.A., M.E. (Software Engineering), Ph.D. (Business Analytics)',
|
||||
experience: '12 Years',
|
||||
subjectsHandling: 'Business Analytics, Data Analytics, Database Management Systems, Python for Business Applications',
|
||||
researchAreas: 'Business Intelligence, Data Analytics, Digital Transformation, Decision Support Systems',
|
||||
officeHours: 'Mon–Fri, 9:30 AM–11:30 AM',
|
||||
phone: '+91 98765 43221',
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
name: 'Dr. K. Nandhini',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Artificial Intelligence & Machine Learning',
|
||||
office: 'AIML Block, Room 412',
|
||||
specialization: 'Artificial Intelligence, Machine Learning',
|
||||
email: 'nandhini@rit.edu',
|
||||
qualification: 'B.E. (CSE), M.Tech. (Artificial Intelligence), Ph.D. (Machine Learning)',
|
||||
experience: '8 Years',
|
||||
subjectsHandling: 'Artificial Intelligence, Machine Learning, Deep Learning, Python Programming',
|
||||
researchAreas: 'Generative AI, Computer Vision, Explainable AI, Neural Networks',
|
||||
officeHours: 'Mon–Fri, 2:00 PM–4:00 PM',
|
||||
phone: '+91 98765 43222',
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
name: 'Dr. S. Rajeshwari',
|
||||
designation: 'Professor',
|
||||
department: 'Computer Science & Business Systems',
|
||||
office: 'CSBS Block, Room 302',
|
||||
specialization: 'Business Intelligence, Enterprise Systems',
|
||||
email: 'rajeshwari@rit.edu',
|
||||
qualification: 'B.E. (CSE), M.E. (Computer Science), Ph.D. (Business Intelligence)',
|
||||
experience: '16 Years',
|
||||
subjectsHandling: 'Business Intelligence, Enterprise Systems, Software Engineering, Project Management',
|
||||
researchAreas: 'Business Intelligence, Enterprise Computing, ERP Systems',
|
||||
officeHours: 'Mon–Fri, 10:00 AM–12:00 PM',
|
||||
phone: '+91 98765 43223',
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
name: 'Dr. V. Pradeep Kumar',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Computer Science & Business Systems',
|
||||
office: 'CSBS Block, Room 308',
|
||||
specialization: 'FinTech, Cloud Computing',
|
||||
email: 'pradeepkumar@rit.edu',
|
||||
qualification: 'B.E. (IT), M.Tech. (Cloud Computing), Ph.D. (Financial Technology)',
|
||||
experience: '7 Years',
|
||||
subjectsHandling: 'Cloud Computing, FinTech Applications, Data Structures, Java Programming',
|
||||
researchAreas: 'Cloud Security, Financial Analytics, Blockchain Technology',
|
||||
officeHours: 'Mon–Fri, 1:00 PM–3:00 PM',
|
||||
phone: '+91 98765 43224',
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
name: 'Dr. P. Harini',
|
||||
designation: 'Associate Professor',
|
||||
department: 'Artificial Intelligence & Machine Learning',
|
||||
office: 'AIML Block, Room 408',
|
||||
specialization: 'Deep Learning, Computer Vision',
|
||||
email: 'harini@rit.edu',
|
||||
qualification: 'B.E. (CSE), M.Tech. (Artificial Intelligence), Ph.D. (Computer Vision)',
|
||||
experience: '10 Years',
|
||||
subjectsHandling: 'Deep Learning, Computer Vision, Image Processing, Python Programming',
|
||||
researchAreas: 'Medical Image Analysis, Deep Neural Networks, AI in Healthcare',
|
||||
officeHours: 'Mon–Fri, 9:00 AM–11:00 AM',
|
||||
phone: '+91 98765 43225',
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
name: 'Dr. A. Vignesh',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Artificial Intelligence & Machine Learning',
|
||||
office: 'AIML Block, Room 415',
|
||||
specialization: 'Natural Language Processing, Generative AI',
|
||||
email: 'vignesh@rit.edu',
|
||||
qualification: 'B.E. (CSE), M.E. (Artificial Intelligence), Ph.D. (Natural Language Processing)',
|
||||
experience: '6 Years',
|
||||
subjectsHandling: 'Natural Language Processing, Generative AI, Machine Learning, Data Science',
|
||||
researchAreas: 'Large Language Models, Conversational AI, Explainable AI',
|
||||
officeHours: 'Mon–Fri, 2:00 PM–4:00 PM',
|
||||
phone: '+91 98765 43226',
|
||||
}
|
||||
];
|
||||
|
||||
// ─── Events ──────────────────────────────────────────────────────────────────
|
||||
@@ -280,12 +527,300 @@ export const EVENTS_DATA: Event[] = [
|
||||
|
||||
// ─── Clubs ───────────────────────────────────────────────────────────────────
|
||||
export const CLUBS_DATA: Club[] = [
|
||||
{ id: '1', name: 'Coding Club', description: 'For programming enthusiasts', category: 'Technical', members: 120 },
|
||||
{ id: '2', name: 'Robotics Club', description: 'Build and code robots', category: 'Technical', members: 85 },
|
||||
{ id: '3', name: 'Photography Club', description: 'Capture campus moments', category: 'Creative', members: 60 },
|
||||
{ id: '4', name: 'Music Club', description: 'Classical and contemporary music', category: 'Cultural', members: 95 },
|
||||
{ id: '5', name: 'Drama Club', description: 'Theatre and performing arts', category: 'Cultural', members: 45 },
|
||||
{ id: '6', name: 'NSS', description: 'National Service Scheme', category: 'Social', members: 200 },
|
||||
{
|
||||
id: 'stellar_space_tech',
|
||||
name: 'STELLAR SPACE TECH CLUB',
|
||||
description: 'Space technology, astronomy, satellite systems, and space science exploration at RIT.',
|
||||
details: 'RIT Stellar Space Tech Club brings together space enthusiasts, students, and aspiring aerospace engineers to explore astronomy, satellite communication, rocketry, and cutting-edge space technology through workshops, observational sessions, and technical projects.',
|
||||
category: 'Technical',
|
||||
members: 94,
|
||||
presidentName: 'Student Space Lead',
|
||||
year: 'Senior Year',
|
||||
contactEmail: 'spacetech@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43210',
|
||||
icon: 'Rocket',
|
||||
logoUrl: '/clubs/stellar_space_tech.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/stellarspacetechclub/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'podx',
|
||||
name: 'podX-RIT (PODCAST CLUB)',
|
||||
description: 'Real talks. Raw feels. Because every story deserves to be heard. Official Podcast Club of RIT.',
|
||||
details: 'podX-RIT is the official Podcast and Radio Club of Rajalakshmi Institute of Technology. Bringing student stories, Switcheroo interviews, RJ sessions, and inspiring student & faculty conversations to life.',
|
||||
category: 'Creative',
|
||||
members: 1097,
|
||||
presidentName: 'Lead RJ / Podcaster',
|
||||
year: 'Senior Year',
|
||||
contactEmail: 'podx@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43211',
|
||||
icon: 'Mic',
|
||||
logoUrl: '/clubs/podx.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/podxrit/',
|
||||
website: 'https://linktr.ee/podxrit',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'nss',
|
||||
name: 'NSS OF RIT',
|
||||
description: 'National Service Scheme – "Not Me, But You." Official Social Service Club of RIT.',
|
||||
details: 'NSS of Rajalakshmi Institute of Technology engages students in community service, social awareness drives, blood donation camps, road safety campaigns, and nation-building initiatives.',
|
||||
category: 'Social',
|
||||
members: 1306,
|
||||
presidentName: 'NSS Student Lead',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'nss@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43212',
|
||||
icon: 'HeartHandshake',
|
||||
logoUrl: '/clubs/nss.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/nss.rit/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'artist_league',
|
||||
name: 'ARTIST LEAGUE (CULTURAL CELL)',
|
||||
description: 'Official Cultural Cell of RIT covering Dance (R-Square), Band (Euphoria), Rap & Classical.',
|
||||
details: 'Artist League is the umbrella cultural organization of RIT incorporating R-Square (Dance), Euphoria (Band), Raptology (Rap), and Classic Beat (Classical). It organizes cultural fests, talent shows, and 60 Seconds to Fame competitions.',
|
||||
category: 'Creative',
|
||||
members: 1093,
|
||||
presidentName: 'Cultural Secretary',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'artistleague@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43213',
|
||||
icon: 'Sparkles',
|
||||
logoUrl: '/clubs/artist_league.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/artist_league_rit/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'infinitus',
|
||||
name: 'INFINITUS CLUB (MATHS CLUB)',
|
||||
description: 'Where Logic Meets Innovation. Mathematics workshops, PiDoku, GraphQuest & tech competitions.',
|
||||
details: 'Infinitus Club is the official Mathematics & Aptitude Club of RIT, hosting Pi-Quest, PiDoku Decode π, GraphQuest, logic puzzles, speed calculations, and analytical problem-solving workshops.',
|
||||
category: 'Technical',
|
||||
members: 557,
|
||||
presidentName: 'Mathematics Lead',
|
||||
year: 'Third Year',
|
||||
contactEmail: 'infinitus@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43214',
|
||||
icon: 'Calculator',
|
||||
logoUrl: '/clubs/infinitus.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/infinitus_club.rit/',
|
||||
whatsapp: 'https://chat.whatsapp.com/LeCKYtgzdzJ5E6sL0WjYIHf',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vaarithi',
|
||||
name: 'VAARITHI TAMIL CLUB',
|
||||
description: 'இராசலட்சுமி தொழில்நுட்பக் கல்லூரியின் மாணவர் தமிழ் மன்றம் (RIT Tamil Mandram).',
|
||||
details: 'Vaarithi Muthamizh Mandram of RIT celebrates Tamil literature, poetry, debate (Tamizhsudar), Tamil New Year celebrations, and preserving rich Tamil culture and heritage.',
|
||||
category: 'Creative',
|
||||
members: 848,
|
||||
presidentName: 'Tamil Mandram Lead',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'vaarithi@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43215',
|
||||
icon: 'BookOpen',
|
||||
logoUrl: '/clubs/vaarithi.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/vaarithi_rit/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'fusion',
|
||||
name: 'FUSION CLUB (ENGLISH & LITERATURE)',
|
||||
description: 'A platform to delve into the beauty of language, literature, essays, and power of words.',
|
||||
details: 'Fusion Club of RIT is the English Literary Club organizing essay writing contests, Lips Don’t Lie games, Fusion Treasure Hunt, debates, and creative writing workshops.',
|
||||
category: 'Creative',
|
||||
members: 815,
|
||||
presidentName: 'Literary President',
|
||||
year: 'Third Year',
|
||||
contactEmail: 'fusionclub@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43216',
|
||||
icon: 'Languages',
|
||||
logoUrl: '/clubs/fusion.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/fusionclub_rit/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'nippon',
|
||||
name: 'NIPPON CLUB (JAPANESE CULTURE)',
|
||||
description: 'Celebrating Japanese culture, language, anime, movie review nights & vibrant community.',
|
||||
details: 'Nippon Club of RIT brings Japanese language learning, Kizuna Canvas relay drawing, Koe Nashi voice challenge, anime screenings, and Japanese cultural immersion to RIT students.',
|
||||
category: 'Creative',
|
||||
members: 649,
|
||||
presidentName: 'Nippon President',
|
||||
year: 'Third Year',
|
||||
contactEmail: 'nippon@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43217',
|
||||
icon: 'Globe',
|
||||
logoUrl: '/clubs/nippon.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/nippon_rit/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'unnat_bharat',
|
||||
name: 'RIT UNNAT BHARAT ABHIYAN (UBA)',
|
||||
description: 'Unite, socialize, thrive together! Official Social Impact & Rural Development Club.',
|
||||
details: 'RIT Unnat Bharat Abhiyan (UBA) club focuses on sustainable village development, plantation drives, environmental awareness, and impactful social initiatives across rural Tamil Nadu.',
|
||||
category: 'Social',
|
||||
members: 1153,
|
||||
presidentName: 'UBA Core Lead',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'uba@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43218',
|
||||
icon: 'Users',
|
||||
logoUrl: '/clubs/unnat_bharat.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/ritunnatbharatabhiyan/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mediastic',
|
||||
name: 'MEDIASTIC HUB (SOCIAL MEDIA CLUB)',
|
||||
description: 'Official Social Media & Digital Content Creation Club of Rajalakshmi Institute of Technology.',
|
||||
details: 'Mediastic Hub handles official video production, campus media coverage, event highlights for RobochipX & Quant-a-thon, photography exhibitions, and RIT digital channels.',
|
||||
category: 'Creative',
|
||||
members: 1351,
|
||||
presidentName: 'Media Hub Lead',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'mediastichub@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43219',
|
||||
icon: 'Camera',
|
||||
logoUrl: '/clubs/mediastic.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/mediastichub_rit/',
|
||||
youtube: 'https://www.youtube.com/@rajalakshmiinstituteoftech444',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'helios',
|
||||
name: 'HELIOS RIT (PHOTOGRAPHY CLUB)',
|
||||
description: 'Capturing Life\'s Moments, One Frame at a Time. Showcasing cinematic shots & campus stories.',
|
||||
details: 'Helios RIT Official is the premier photography and filmmaking club of RIT, capturing campus events, hosting photography contests, guest lectures, and showcasing cinematic stories of RIT.',
|
||||
category: 'Creative',
|
||||
members: 2455,
|
||||
presidentName: 'Photography Lead',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'helios@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43220',
|
||||
icon: 'Camera',
|
||||
logoUrl: '/clubs/helios.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/helios.rit/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'techspark',
|
||||
name: 'TECHSPARK CLUB RIT',
|
||||
description: 'Where Tech meets creativity, Sparks fly! Fostering innovation in every byte.',
|
||||
details: 'TechSpark Club RIT is the technical innovation club driving Web3 Camp Meets AI, GirlScript hackathons, Spark Booths, coding competitions, and hands-on developer projects.',
|
||||
category: 'Technical',
|
||||
members: 672,
|
||||
presidentName: 'TechSpark Lead',
|
||||
year: 'Third Year',
|
||||
contactEmail: 'techspark@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43221',
|
||||
icon: 'Zap',
|
||||
logoUrl: '/clubs/techspark.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/techspark.rit/',
|
||||
whatsapp: 'https://chat.whatsapp.com/I8B5Wdybrew2dYYDrE4PG',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'wec',
|
||||
name: 'WOMEN EMPOWERMENT CELL (WEC)',
|
||||
description: 'Empower • Excel • Elevate. Supporting women in technology, leadership & coding.',
|
||||
details: 'Women Empowerment Cell (WEC RIT) organizes She Shines @ Hexaware coding challenges, Women’s Empowerment Week, ZYRA Days, leadership seminars, and career development initiatives.',
|
||||
category: 'Social',
|
||||
members: 428,
|
||||
presidentName: 'Pooja WYA',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'wec@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43222',
|
||||
icon: 'Sparkles',
|
||||
logoUrl: '/clubs/wec.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/wec.rit/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rotaract',
|
||||
name: 'ROTARACT CLUB OF RIT (RCRIT)',
|
||||
description: 'Embrace the joy of giving. Official Rotaract organization for leadership & service.',
|
||||
details: 'Rotaract Club of RIT (RCRIT) under Rotary International sponsors Bloom mental health seminars, Phoenix fests, RITARIQ 365, Ryla leadership camps, and community blood donation drives.',
|
||||
category: 'Social',
|
||||
members: 1813,
|
||||
presidentName: 'Hari Balaji',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'rotaract@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43223',
|
||||
icon: 'HeartHandshake',
|
||||
logoUrl: '/clubs/rotaract.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/rcrit/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'yuva',
|
||||
name: 'Yi-CH YUVA RIT',
|
||||
description: 'Official Page of YUVA Club RIT under Young Indians (Yi). Inspiring youth leadership.',
|
||||
details: 'Yi-CH YUVA RIT leads Chennai Get Talent, Safe Miles road safety awareness, Blogathons, Tree Plantation drives (500 Trees Mission), and youth leadership conventions.',
|
||||
category: 'Social',
|
||||
members: 1460,
|
||||
presidentName: 'YUVA Student Chair',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'yuva@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43224',
|
||||
icon: 'Rocket',
|
||||
logoUrl: '/clubs/yuva.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/yuva_rit/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'wistem',
|
||||
name: 'WiSTEM CLUB OF RIT',
|
||||
description: 'Women in STEM Club. AI Quests, STEM Chronicles, CodeVsClick & maker empowering events.',
|
||||
details: 'WiSTEM Club of RIT empowers women in Science, Technology, Engineering, and Mathematics through AI Quests, STEM Chronicles newsletter, CodeVsClick contests, and technical workshops.',
|
||||
category: 'Technical',
|
||||
members: 453,
|
||||
presidentName: 'WiSTEM Lead',
|
||||
year: 'Third Year',
|
||||
contactEmail: 'wistem@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43225',
|
||||
icon: 'Atom',
|
||||
logoUrl: '/clubs/wistem.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/wistem._.rit/',
|
||||
website: 'https://www.rityatra.in/',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'steam',
|
||||
name: 'STEAM CLUB RIT',
|
||||
description: 'Innovate - Collaborate - Elevate. Hands-on RobochipX, Quant-a-thon & AI/Chip design.',
|
||||
details: 'STEAM Club RIT focuses on hands-on project creation, RobochipX 24-hour hackathons, Quant-a-thon quantum technology events, chip design, and robotics innovations.',
|
||||
category: 'Technical',
|
||||
members: 895,
|
||||
presidentName: 'STEAM President',
|
||||
year: 'Final Year',
|
||||
contactEmail: 'steamclub@ritchennai.edu.in',
|
||||
contactPhone: '+91 98765 43226',
|
||||
icon: 'Printer',
|
||||
logoUrl: '/clubs/steam.png',
|
||||
socialLinks: {
|
||||
instagram: 'https://www.instagram.com/steam_club.rit/',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Community Q&A ───────────────────────────────────────────────────────────
|
||||
@@ -390,10 +925,11 @@ export const AI_SUGGESTED_PROMPTS = [
|
||||
export const DEPARTMENTS = [
|
||||
'All Departments',
|
||||
'Computer Science & Engineering',
|
||||
'Electronics & Communication',
|
||||
'Computer Science & Business Systems',
|
||||
'Artificial Intelligence & Machine Learning',
|
||||
'Electronics & Communication Engineering',
|
||||
'Mechanical Engineering',
|
||||
'Civil Engineering',
|
||||
'Information Technology',
|
||||
'Electrical & Electronics',
|
||||
'Artificial Intelligence & Data Science',
|
||||
'Electrical & Electronics Engineering',
|
||||
];
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function AIAssistant() {
|
||||
background: msg.role === 'user' ? 'linear-gradient(135deg, #F97316, #FB923C)' : '#F8FAFC',
|
||||
color: msg.role === 'user' ? 'white' : '#1E293B',
|
||||
borderRadius: msg.role === 'user' ? '20px 20px 4px 20px' : '20px 20px 20px 4px',
|
||||
border: msg.role === 'ai' ? '1px solid #E5E7EB' : 'none',
|
||||
border: msg.role === 'assistant' ? '1px solid #E5E7EB' : 'none',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: formatContent(msg.content) }}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Search, ChevronRight, MapPin } from 'lucide-react';
|
||||
import { Search, ChevronRight, MapPin, Users, BookOpen, Filter, ArrowUpDown, Loader2 } from 'lucide-react';
|
||||
import SectionTitle from '@/components/SectionTitle/SectionTitle';
|
||||
import BusCard from '@/components/BusCard/BusCard';
|
||||
import FacultyCard from '@/components/FacultyCard/FacultyCard';
|
||||
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
||||
import { BUS_ROUTES, FACULTY_DATA, CAMPUS_LOCATIONS, DEPARTMENTS } from '@/constants';
|
||||
import { FACULTY_DATA, CAMPUS_LOCATIONS, DEPARTMENTS } from '@/constants';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import type { BusRoute } from '@/types';
|
||||
|
||||
type Tab = 'map' | 'bus' | 'faculty';
|
||||
|
||||
@@ -14,16 +15,52 @@ export default function Campus() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('map');
|
||||
const [searchFaculty, setSearchFaculty] = useState('');
|
||||
const [selectedDept, setSelectedDept] = useState('All Departments');
|
||||
const [sortBy, setSortBy] = useState('Name A-Z');
|
||||
const [busSearch, setBusSearch] = useState('');
|
||||
const [busRoutes, setBusRoutes] = useState<BusRoute[]>([]);
|
||||
const [loadingBus, setLoadingBus] = useState(true);
|
||||
|
||||
const filteredFaculty = FACULTY_DATA.filter((f) => {
|
||||
const matchSearch = f.name.toLowerCase().includes(searchFaculty.toLowerCase()) ||
|
||||
f.department.toLowerCase().includes(searchFaculty.toLowerCase());
|
||||
const matchDept = selectedDept === 'All Departments' || f.department === selectedDept;
|
||||
return matchSearch && matchDept;
|
||||
});
|
||||
useEffect(() => {
|
||||
if (activeTab === 'bus' && busRoutes.length === 0) {
|
||||
setLoadingBus(true);
|
||||
fetch('http://localhost:8080/api/bus-routes')
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('Failed to fetch');
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setBusRoutes(data);
|
||||
setLoadingBus(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error fetching bus routes:', err);
|
||||
setLoadingBus(false);
|
||||
});
|
||||
}
|
||||
}, [activeTab, busRoutes.length]);
|
||||
|
||||
const filteredRoutes = BUS_ROUTES.filter((r) =>
|
||||
const filteredAndSortedFaculty = useMemo(() => {
|
||||
const filtered = FACULTY_DATA.filter((f) => {
|
||||
const searchLower = searchFaculty.trim().toLowerCase();
|
||||
const matchSearch =
|
||||
f.name.toLowerCase().includes(searchLower) ||
|
||||
f.department.toLowerCase().includes(searchLower) ||
|
||||
f.designation.toLowerCase().includes(searchLower) ||
|
||||
(f.specialization && f.specialization.toLowerCase().includes(searchLower));
|
||||
|
||||
const matchDept = selectedDept === 'All Departments' || f.department === selectedDept;
|
||||
return matchSearch && matchDept;
|
||||
});
|
||||
|
||||
return filtered.sort((a, b) => {
|
||||
if (sortBy === 'Name A-Z') return a.name.localeCompare(b.name);
|
||||
if (sortBy === 'Department') return a.department.localeCompare(b.department);
|
||||
if (sortBy === 'Designation') return a.designation.localeCompare(b.designation);
|
||||
return 0;
|
||||
});
|
||||
}, [searchFaculty, selectedDept, sortBy]);
|
||||
|
||||
const filteredRoutes = busRoutes.filter((r) =>
|
||||
r.name.toLowerCase().includes(busSearch.toLowerCase()) ||
|
||||
r.from.toLowerCase().includes(busSearch.toLowerCase()) ||
|
||||
r.number.toLowerCase().includes(busSearch.toLowerCase())
|
||||
@@ -36,8 +73,18 @@ export default function Campus() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
|
||||
{/* Header */}
|
||||
<div className="min-h-screen relative overflow-hidden" style={{ backgroundColor: activeTab === 'faculty' ? '#F8FAFC' : '#FAFAFA' }}>
|
||||
{/* Background decorations for Faculty tab */}
|
||||
{activeTab === 'faculty' && (
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-96 bg-gradient-to-b from-white/80 to-transparent" />
|
||||
<div className="absolute top-20 -left-64 w-[500px] h-[500px] bg-purple-200/40 rounded-full blur-3xl opacity-50" />
|
||||
<div className="absolute top-40 -right-64 w-[600px] h-[600px] bg-orange-200/40 rounded-full blur-3xl opacity-40" />
|
||||
<div className="absolute inset-0" style={{ backgroundImage: 'radial-gradient(rgba(148, 163, 184, 0.1) 2px, transparent 2px)', backgroundSize: '32px 32px' }} />
|
||||
</div>
|
||||
)}
|
||||
<div className="relative z-10">
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b border-[#E5E7EB] py-10">
|
||||
<div className="container-custom">
|
||||
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3">
|
||||
@@ -61,7 +108,7 @@ export default function Campus() {
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 bg-white rounded-2xl p-2 border border-[#E5E7EB] mb-8 w-fit" style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}>
|
||||
{TABS.map((tab) => {
|
||||
const Icon = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[tab.icon];
|
||||
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[tab.icon];
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
@@ -144,7 +191,7 @@ export default function Campus() {
|
||||
{/* Quick Nav */}
|
||||
<div className="p-6 grid grid-cols-4 sm:grid-cols-8 gap-4">
|
||||
{CAMPUS_LOCATIONS.map((loc) => {
|
||||
const Icon = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
|
||||
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
|
||||
return (
|
||||
<motion.button
|
||||
key={loc.id}
|
||||
@@ -179,52 +226,156 @@ export default function Campus() {
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
/>
|
||||
</div>
|
||||
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{filteredRoutes.map((route) => (
|
||||
<StaggerItem key={route.id}>
|
||||
<BusCard route={route} />
|
||||
</StaggerItem>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
|
||||
{loadingBus ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 gap-3">
|
||||
<Loader2 className="w-8 h-8 text-[#F97316] animate-spin" />
|
||||
<p className="text-sm text-[#94A3B8]" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Loading live bus routes from RIT Transport...
|
||||
</p>
|
||||
</div>
|
||||
) : filteredRoutes.length === 0 ? (
|
||||
<div className="text-center py-16 text-[#94A3B8]">
|
||||
<p className="text-sm font-medium" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
No bus routes found matching "{busSearch}"
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{filteredRoutes.map((route) => (
|
||||
<StaggerItem key={route.id}>
|
||||
<BusCard route={route} />
|
||||
</StaggerItem>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Faculty Tab */}
|
||||
{activeTab === 'faculty' && (
|
||||
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
||||
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} className="space-y-8">
|
||||
{/* Faculty Hero Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
|
||||
<div className="lg:col-span-5 bg-white rounded-3xl p-8 border border-[#E8ECF4] shadow-sm flex flex-col justify-center relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-orange-50 rounded-bl-full -z-10" />
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#FF7A00] to-[#FB923C] flex items-center justify-center text-white mb-6 shadow-md">
|
||||
<Users className="w-7 h-7" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-[#1E293B] mb-3" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Faculty Directory
|
||||
</h2>
|
||||
<p className="text-[#64748B] text-sm leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
Browse and connect with experienced faculty members. Search by name, department, or specialization.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-7 bg-white rounded-3xl p-6 border border-[#E8ECF4] shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-[#1E293B] mb-4 uppercase tracking-wider" style={{ fontFamily: 'Inter, sans-serif' }}>Department Overview</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ abrv: 'CSE', full: 'Computer Science & Engineering', color: 'blue' },
|
||||
{ abrv: 'CSBS', full: 'Computer Science & Business Systems', color: 'indigo' },
|
||||
{ abrv: 'AIML', full: 'Artificial Intelligence & Machine Learning', color: 'sky' },
|
||||
{ abrv: 'ECE', full: 'Electronics & Communication Engineering', color: 'purple' },
|
||||
{ abrv: 'MECH', full: 'Mechanical Engineering', color: 'orange' },
|
||||
{ abrv: 'CIVIL', full: 'Civil Engineering', color: 'emerald' },
|
||||
{ abrv: 'AI & DS', full: 'Artificial Intelligence & Data Science', color: 'pink' },
|
||||
{ abrv: 'EEE', full: 'Electrical & Electronics Engineering', color: 'yellow' },
|
||||
].map(d => {
|
||||
const count = FACULTY_DATA.filter(f => f.department === d.full).length;
|
||||
return (
|
||||
<div key={d.abrv} className={`p-4 rounded-2xl bg-${d.color}-50 border border-${d.color}-100 flex flex-col gap-1 transition-transform hover:scale-105 cursor-default`}>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className={`text-xs font-bold text-${d.color}-700`} style={{ fontFamily: 'Poppins, sans-serif' }}>{d.abrv}</span>
|
||||
<BookOpen className={`w-3.5 h-3.5 text-${d.color}-400`} />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>{count}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Section */}
|
||||
<div className="bg-white rounded-3xl p-4 border border-[#E8ECF4] shadow-sm flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-[#94A3B8]" />
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#94A3B8]" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search faculty by name or department..."
|
||||
placeholder="Search by faculty name, department, designation or specialization..."
|
||||
value={searchFaculty}
|
||||
onChange={(e) => setSearchFaculty(e.target.value)}
|
||||
className="w-full pl-11 pr-4 py-3 rounded-xl border border-[#E5E7EB] bg-white text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none focus:border-[#F97316] transition-colors"
|
||||
className="w-full pl-12 pr-4 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm text-[#1E293B] placeholder-[#94A3B8] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={selectedDept}
|
||||
onChange={(e) => setSelectedDept(e.target.value)}
|
||||
className="px-4 py-3 rounded-xl border border-[#E5E7EB] bg-white text-sm text-[#475569] focus:outline-none focus:border-[#F97316] transition-colors"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
>
|
||||
{DEPARTMENTS.map((dept) => (
|
||||
<option key={dept} value={dept}>{dept}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="relative">
|
||||
<Filter className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#94A3B8] pointer-events-none" />
|
||||
<select
|
||||
value={selectedDept}
|
||||
onChange={(e) => setSelectedDept(e.target.value)}
|
||||
className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer min-w-[200px]"
|
||||
style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }}
|
||||
>
|
||||
{DEPARTMENTS.map((dept) => {
|
||||
const count = dept === 'All Departments' ? FACULTY_DATA.length : FACULTY_DATA.filter((f) => f.department === dept).length;
|
||||
return <option key={dept} value={dept}>{dept} ({count})</option>;
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<ArrowUpDown className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[#94A3B8] pointer-events-none" />
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="h-full pl-10 pr-10 py-3.5 rounded-2xl bg-[#F8FAFC] border-none text-sm font-medium text-[#475569] focus:ring-2 focus:ring-[#FF7A00]/20 focus:outline-none transition-all appearance-none cursor-pointer"
|
||||
style={{ fontFamily: 'Inter, sans-serif', backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%2394A3B8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 1rem top 50%', backgroundSize: '0.65rem auto' }}
|
||||
>
|
||||
<option value="Name A-Z">Sort: Name A-Z</option>
|
||||
<option value="Department">Sort: Department</option>
|
||||
<option value="Designation">Sort: Designation</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{filteredAndSortedFaculty.length > 0 ? (
|
||||
<StaggerContainer key={`${searchFaculty}-${selectedDept}-${sortBy}`} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 pb-20">
|
||||
{filteredAndSortedFaculty.map((faculty) => (
|
||||
<StaggerItem key={faculty.id}>
|
||||
<FacultyCard faculty={faculty} />
|
||||
</StaggerItem>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
) : (
|
||||
<div className="py-20 text-center flex flex-col items-center justify-center bg-white rounded-3xl border border-[#E8ECF4] shadow-sm">
|
||||
<Users className="w-12 h-12 text-[#CBD5E1] mb-4" />
|
||||
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>No faculty found</h3>
|
||||
<p className="text-[#64748B] text-sm mt-1" style={{ fontFamily: 'Inter, sans-serif' }}>Try adjusting your search or filters.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Status */}
|
||||
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 pointer-events-none">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="px-6 py-3 bg-white/80 backdrop-blur-md border border-[#E8ECF4] shadow-[0_8px_30px_rgb(0,0,0,0.08)] rounded-full flex items-center gap-2"
|
||||
>
|
||||
<span className="text-lg">👥</span>
|
||||
<span className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
Showing {filteredAndSortedFaculty.length} Faculty Members
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredFaculty.map((faculty) => (
|
||||
<StaggerItem key={faculty.id}>
|
||||
<FacultyCard faculty={faculty} />
|
||||
</StaggerItem>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Calendar, MapPin, Clock, Users, ChevronLeft, ChevronRight, Tag, ExternalLink } from 'lucide-react';
|
||||
import {
|
||||
Users, Tag, X, Mail, Phone, UserCheck, GraduationCap, Info,
|
||||
Atom, Wifi, Printer, Zap, Languages, Calculator, HeartHandshake,
|
||||
Rocket, Camera, Sparkles, Mic, Target, ArrowRight, RotateCcw,
|
||||
MessageCircle, ExternalLink, CheckCircle2, Compass, Globe, BookOpen, Heart
|
||||
} from 'lucide-react';
|
||||
import SectionTitle from '@/components/SectionTitle/SectionTitle';
|
||||
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
||||
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
|
||||
import { EVENTS_DATA, CLUBS_DATA } from '@/constants';
|
||||
|
||||
const CATEGORY_COLORS: Record<string, { bg: string; text: string; border: string }> = {
|
||||
technical: { bg: '#EFF6FF', text: '#3B82F6', border: '#BFDBFE' },
|
||||
cultural: { bg: '#FDF2F8', text: '#EC4899', border: '#FBCFE8' },
|
||||
sports: { bg: '#ECFDF5', text: '#10B981', border: '#A7F3D0' },
|
||||
seminar: { bg: '#FFF7ED', text: '#F97316', border: '#FED7AA' },
|
||||
workshop: { bg: '#F5F3FF', text: '#8B5CF6', border: '#DDD6FE' },
|
||||
};
|
||||
import { CLUBS_DATA } from '@/constants';
|
||||
import type { Club } from '@/types';
|
||||
|
||||
const CLUB_CATEGORY_COLORS: Record<string, string> = {
|
||||
Technical: '#3B82F6',
|
||||
@@ -21,21 +19,173 @@ const CLUB_CATEGORY_COLORS: Record<string, string> = {
|
||||
Creative: '#F59E0B',
|
||||
};
|
||||
|
||||
const CLUB_ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Atom,
|
||||
Wifi,
|
||||
Printer,
|
||||
Zap,
|
||||
Languages,
|
||||
Calculator,
|
||||
HeartHandshake,
|
||||
Rocket,
|
||||
Camera,
|
||||
Sparkles,
|
||||
Mic,
|
||||
BookOpen,
|
||||
Globe,
|
||||
Users,
|
||||
};
|
||||
|
||||
// ─── Inline Brand SVG Icons for Feature 5 ──────────────────────────────────────
|
||||
const InstagramIcon = ({ className = 'w-4 h-4' }: { className?: string }) => (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<rect width="20" height="20" x="2" y="2" rx="5" ry="5" />
|
||||
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" />
|
||||
<line x1="17.5" x2="17.51" y1="6.5" y2="6.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LinkedinIcon = ({ className = 'w-4 h-4' }: { className?: string }) => (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
|
||||
<rect width="4" height="12" x="2" y="9" />
|
||||
<circle cx="4" cy="4" r="2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const YoutubeIcon = ({ className = 'w-4 h-4' }: { className?: string }) => (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-16.2 0A2 2 0 0 1 2.5 17" />
|
||||
<polygon points="10 15 15 12 10 9 10 15" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// ─── Quiz Questions Data for Feature 1 (3-Question Matcher) ─────────────────────
|
||||
const QUIZ_QUESTIONS = [
|
||||
{
|
||||
id: 'interest',
|
||||
title: '1. What topics or domains are you most interested in?',
|
||||
options: [
|
||||
{ label: '🚀 Space Tech, Astronomy & Rocketry', clubIds: ['stellar_space_tech', 'infinitus'] },
|
||||
{ label: '⚙️ AI Quests, Chip Design, RobochipX & STEM', clubIds: ['steam', 'wistem', 'techspark'] },
|
||||
{ label: '🎙️ Radio, Podcasting, Storytelling & RJing', clubIds: ['podx', 'mediastic'] },
|
||||
{ label: '🤝 Community Service, Village Development & NSS Drives', clubIds: ['nss', 'unnat_bharat'] },
|
||||
{ label: '🎭 Dance, Music, Band & Cultural Arts', clubIds: ['artist_league', 'podx'] },
|
||||
{ label: '🧮 Fast Calculation, PiDoku, Logic & Mathematics', clubIds: ['infinitus', 'stellar_space_tech'] },
|
||||
{ label: '✍️ Tamil Literature, Debates & Cultural Heritage', clubIds: ['vaarithi', 'fusion'] },
|
||||
{ label: '🌸 Japanese Culture, Anime, Manga & Foreign Languages', clubIds: ['nippon', 'fusion'] },
|
||||
{ label: '📷 Photo/Video Production & Social Media Content', clubIds: ['mediastic', 'helios', 'podx'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
title: '2. What skills do you currently have or want to develop?',
|
||||
options: [
|
||||
{ label: '💻 Web3, AI Tools, Circuit Design & Coding Hackathons', clubIds: ['wistem', 'steam', 'techspark'] },
|
||||
{ label: '🌌 Space Science & Physics Problem Solving', clubIds: ['stellar_space_tech', 'infinitus'] },
|
||||
{ label: '🎙️ Public Speaking, Interviewing & Voice Recording', clubIds: ['podx', 'mediastic'] },
|
||||
{ label: '🤝 Social Work, Environmental Protection & Leadership', clubIds: ['nss', 'unnat_bharat'] },
|
||||
{ label: '🕺 Stage Performance, Singing, Dance & Rap', clubIds: ['artist_league'] },
|
||||
{ label: '📐 Analytical Thinking, Logic Puzzles & Aptitude', clubIds: ['infinitus'] },
|
||||
{ label: '📝 Essay Writing, Tamil/English Oratory & Literature', clubIds: ['vaarithi', 'fusion'] },
|
||||
{ label: '🎥 Camera Operations, Video Editing & Digital Media', clubIds: ['mediastic', 'helios'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'goal',
|
||||
title: '3. What is your main goal for joining a club at RIT?',
|
||||
options: [
|
||||
{ label: '🤖 Build hackathon projects, chip designs & STEM innovations', clubIds: ['steam', 'wistem', 'techspark'] },
|
||||
{ label: '🚀 Work on aerospace tech & scientific projects', clubIds: ['stellar_space_tech', 'infinitus'] },
|
||||
{ label: '🌟 Share inspiring stories & host campus podcasts', clubIds: ['podx', 'mediastic'] },
|
||||
{ label: '👥 Drive social change & serve rural communities', clubIds: ['nss', 'unnat_bharat'] },
|
||||
{ label: '🏆 Perform live at cultural fests & stage shows', clubIds: ['artist_league', 'vaarithi'] },
|
||||
{ label: '📖 Master new languages & explore world cultures', clubIds: ['nippon', 'fusion', 'vaarithi'] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Events() {
|
||||
const [currentIdx, setCurrentIdx] = useState(0);
|
||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||
const [selectedClub, setSelectedClub] = useState<Club | null>(null);
|
||||
|
||||
const categories = ['All', 'Technical', 'Cultural', 'Seminar', 'Workshop'];
|
||||
// ─── Interactive Like / Favorite System State ─────────────────────────────
|
||||
const [likedClubs, setLikedClubs] = useState<Set<string>>(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem('rit_freshers_liked_clubs');
|
||||
return saved ? new Set(JSON.parse(saved)) : new Set(['podx', 'stellar_space_tech']);
|
||||
} catch {
|
||||
return new Set(['podx', 'stellar_space_tech']);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredEvents = EVENTS_DATA.filter((e) =>
|
||||
selectedCategory === 'All' || e.category === selectedCategory.toLowerCase()
|
||||
);
|
||||
const [likesMap, setLikesMap] = useState<Record<string, number>>(() => {
|
||||
const initialMap: Record<string, number> = {};
|
||||
CLUBS_DATA.forEach((c, idx) => {
|
||||
initialMap[c.id] = Math.round(c.members * 0.42) + (idx % 5) * 14 + 35;
|
||||
});
|
||||
return initialMap;
|
||||
});
|
||||
|
||||
const prev = () => setCurrentIdx((i) => (i === 0 ? EVENTS_DATA.length - 1 : i - 1));
|
||||
const next = () => setCurrentIdx((i) => (i === EVENTS_DATA.length - 1 ? 0 : i + 1));
|
||||
const toggleLike = (clubId: string) => {
|
||||
setLikedClubs((prev) => {
|
||||
const next = new Set(prev);
|
||||
const isCurrentlyLiked = next.has(clubId);
|
||||
if (isCurrentlyLiked) {
|
||||
next.delete(clubId);
|
||||
setLikesMap((l) => ({ ...l, [clubId]: Math.max(0, (l[clubId] || 1) - 1) }));
|
||||
} else {
|
||||
next.add(clubId);
|
||||
setLikesMap((l) => ({ ...l, [clubId]: (l[clubId] || 0) + 1 }));
|
||||
}
|
||||
try {
|
||||
localStorage.setItem('rit_freshers_liked_clubs', JSON.stringify(Array.from(next)));
|
||||
} catch {}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const featured = EVENTS_DATA[currentIdx];
|
||||
const featCfg = CATEGORY_COLORS[featured.category] || CATEGORY_COLORS.seminar;
|
||||
// ─── Club Matcher Quiz State (Feature 1) ─────────────────────────────────
|
||||
const [isQuizOpen, setIsQuizOpen] = useState(false);
|
||||
const [quizStep, setQuizStep] = useState(0);
|
||||
const [selectedAnswers, setSelectedAnswers] = useState<number[]>([]);
|
||||
const [quizResults, setQuizResults] = useState<{ club: Club; score: number }[] | null>(null);
|
||||
|
||||
const handleSelectOption = (optionIdx: number) => {
|
||||
const updated = [...selectedAnswers];
|
||||
updated[quizStep] = optionIdx;
|
||||
setSelectedAnswers(updated);
|
||||
|
||||
if (quizStep < QUIZ_QUESTIONS.length - 1) {
|
||||
setQuizStep(quizStep + 1);
|
||||
} else {
|
||||
calculateQuizResults(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const calculateQuizResults = (answers: number[]) => {
|
||||
const scoreMap: Record<string, number> = {};
|
||||
CLUBS_DATA.forEach((c) => (scoreMap[c.id] = 0));
|
||||
|
||||
answers.forEach((ansIdx, qIdx) => {
|
||||
const option = QUIZ_QUESTIONS[qIdx].options[ansIdx];
|
||||
option.clubIds.forEach((clubId, idx) => {
|
||||
scoreMap[clubId] = (scoreMap[clubId] || 0) + (3 - idx);
|
||||
});
|
||||
});
|
||||
|
||||
const ranked = CLUBS_DATA.map((club) => ({
|
||||
club,
|
||||
score: scoreMap[club.id] || 0,
|
||||
})).sort((a, b) => b.score - a.score);
|
||||
|
||||
setQuizResults(ranked.slice(0, 3));
|
||||
};
|
||||
|
||||
const resetQuiz = () => {
|
||||
setQuizStep(0);
|
||||
setSelectedAnswers([]);
|
||||
setQuizResults(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ backgroundColor: '#FAFAFA' }}>
|
||||
@@ -43,221 +193,537 @@ export default function Events() {
|
||||
<div className="bg-white border-b border-[#E5E7EB] py-10">
|
||||
<div className="container-custom">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
|
||||
Clubs &{' '}
|
||||
Student{' '}
|
||||
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||
Events
|
||||
Clubs
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
Discover events, join clubs, and be part of the RIT community.
|
||||
Explore official RIT student clubs & societies, leadership details, and community links.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container-custom py-10">
|
||||
{/* Featured Event Carousel */}
|
||||
<SectionTitle tag="Upcoming" title="Featured" highlight="Events" />
|
||||
<div className="container-custom pt-10 pb-20 md:pb-28">
|
||||
|
||||
<AnimatedContainer className="relative mb-14">
|
||||
<div className="bg-white rounded-3xl border border-[#E5E7EB] overflow-hidden" style={{ boxShadow: '0 8px 40px -8px rgba(0,0,0,0.1)' }}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={featured.id}
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -30 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="flex flex-col md:flex-row"
|
||||
>
|
||||
{/* Image placeholder */}
|
||||
<div
|
||||
className="w-full md:w-2/5 h-56 md:h-auto flex items-center justify-center"
|
||||
style={{ background: 'linear-gradient(135deg, #1E293B, #334155)' }}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-5xl mb-2">
|
||||
{featured.category === 'technical' ? '💻' : featured.category === 'cultural' ? '🎭' : featured.category === 'seminar' ? '🎤' : '📅'}
|
||||
</div>
|
||||
<div className="text-white/60 text-sm">{featured.venue}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* ─── Feature 1: "Find My Ideal Club" Banner ────────────────────────── */}
|
||||
<AnimatedContainer className="mb-14">
|
||||
<div
|
||||
className="rounded-3xl p-6 md:p-8 text-white relative overflow-hidden border border-orange-400/30 flex flex-col md:flex-row items-center justify-between gap-6"
|
||||
style={{ background: 'linear-gradient(135deg, #1E293B, #0F172A)' }}
|
||||
>
|
||||
{/* Background Glow */}
|
||||
<div
|
||||
className="absolute top-0 right-0 w-80 h-80 rounded-full opacity-20 pointer-events-none"
|
||||
style={{ background: 'radial-gradient(circle, #F97316, transparent)' }}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 p-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span
|
||||
className="px-3 py-1 rounded-full text-xs font-semibold capitalize"
|
||||
style={{ backgroundColor: featCfg.bg, color: featCfg.text, border: `1px solid ${featCfg.border}`, fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{featured.category}
|
||||
</span>
|
||||
<span className="px-3 py-1 rounded-full text-xs font-semibold bg-emerald-50 text-emerald-600 border border-emerald-200" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Upcoming
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative z-10 max-w-xl">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-orange-500/20 text-orange-400 text-xs font-semibold mb-3 border border-orange-500/30">
|
||||
<Target className="w-3.5 h-3.5" />
|
||||
<span>Interactive Club Matcher</span>
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-white mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
|
||||
Not sure which club to join?
|
||||
</h2>
|
||||
<p className="text-slate-300 text-sm leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
Take our 3-step AI-powered Club Matcher quiz to get instant recommendations tailored to your interests, skills, and goals!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold text-[#1E293B] mb-3" style={{ fontFamily: 'Playfair Display, serif' }}>
|
||||
{featured.title}
|
||||
</h3>
|
||||
<p className="text-[#475569] text-sm leading-relaxed mb-5" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{featured.description}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 mb-6">
|
||||
{[
|
||||
{ icon: Calendar, label: new Date(featured.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'long', year: 'numeric' }) },
|
||||
{ icon: Clock, label: featured.time },
|
||||
{ icon: MapPin, label: featured.venue },
|
||||
{ icon: Users, label: featured.organizer },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-sm text-[#475569]">
|
||||
<item.icon className="w-4 h-4 text-[#F97316] shrink-0" />
|
||||
<span style={{ fontFamily: 'Inter, sans-serif' }}>{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
className="flex items-center gap-2 px-5 py-2.5 rounded-xl text-white text-sm font-semibold"
|
||||
style={{ fontFamily: 'Poppins, sans-serif', background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
|
||||
>
|
||||
Register Now
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.03 }}
|
||||
className="px-5 py-2.5 rounded-xl text-sm font-semibold border-2 border-[#E5E7EB] text-[#475569] hover:border-[#F97316] hover:text-[#F97316] transition-all"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
More Details
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Carousel Controls */}
|
||||
<div className="flex items-center justify-center gap-3 mt-4">
|
||||
<button onClick={prev} className="w-9 h-9 rounded-xl border border-[#E5E7EB] bg-white flex items-center justify-center text-[#475569] hover:border-[#F97316] hover:text-[#F97316] transition-all">
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
{EVENTS_DATA.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentIdx(i)}
|
||||
className="transition-all rounded-full"
|
||||
style={{ width: i === currentIdx ? '24px' : '8px', height: '8px', backgroundColor: i === currentIdx ? '#F97316' : '#E5E7EB' }}
|
||||
/>
|
||||
))}
|
||||
<button onClick={next} className="w-9 h-9 rounded-xl border border-[#E5E7EB] bg-white flex items-center justify-center text-[#475569] hover:border-[#F97316] hover:text-[#F97316] transition-all">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => { resetQuiz(); setIsQuizOpen(true); }}
|
||||
className="relative z-10 px-6 py-3.5 rounded-2xl text-white font-semibold text-sm flex items-center gap-2.5 shadow-lg shrink-0 cursor-pointer"
|
||||
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Find My Ideal Club
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</motion.button>
|
||||
</div>
|
||||
</AnimatedContainer>
|
||||
|
||||
{/* All Events Grid */}
|
||||
<div className="mb-14">
|
||||
<div className="flex items-center justify-between mb-6 flex-wrap gap-4">
|
||||
<h2 className="text-xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>All Events</h2>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
className="px-3.5 py-1.5 rounded-xl text-xs font-medium transition-all"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
backgroundColor: selectedCategory === cat ? '#F97316' : '#F8FAFC',
|
||||
color: selectedCategory === cat ? 'white' : '#475569',
|
||||
border: `1px solid ${selectedCategory === cat ? '#F97316' : '#E5E7EB'}`,
|
||||
}}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Clubs Directory Section */}
|
||||
<SectionTitle tag="Official Directory" title="Student" highlight="Clubs" subtitle="Explore official RIT clubs & societies. Click any club to view full details, leadership, and community social links." />
|
||||
|
||||
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-4">
|
||||
{filteredEvents.map((event) => {
|
||||
const cfg = CATEGORY_COLORS[event.category] || CATEGORY_COLORS.seminar;
|
||||
return (
|
||||
<StaggerItem key={event.id}>
|
||||
<motion.div
|
||||
whileHover={{ y: -4 }}
|
||||
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 flex gap-4"
|
||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
||||
>
|
||||
<div className="w-14 h-14 rounded-xl flex flex-col items-center justify-center shrink-0" style={{ backgroundColor: cfg.bg }}>
|
||||
<span className="text-xs font-bold" style={{ color: cfg.text, fontFamily: 'Poppins, sans-serif' }}>
|
||||
{new Date(event.date).toLocaleDateString('en-IN', { day: 'numeric' })}
|
||||
</span>
|
||||
<span className="text-[10px]" style={{ color: cfg.text }}>
|
||||
{new Date(event.date).toLocaleDateString('en-IN', { month: 'short' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold capitalize" style={{ backgroundColor: cfg.bg, color: cfg.text, fontFamily: 'Poppins, sans-serif' }}>
|
||||
{event.category}
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold text-[#1E293B] mt-1.5 mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>{event.title}</h3>
|
||||
<div className="flex items-center gap-3 text-xs text-[#94A3B8]">
|
||||
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{event.time}</span>
|
||||
<span className="flex items-center gap-1"><MapPin className="w-3 h-3" />{event.venue}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</StaggerItem>
|
||||
);
|
||||
})}
|
||||
</StaggerContainer>
|
||||
</div>
|
||||
{(() => {
|
||||
const fullGridCount = Math.floor(CLUBS_DATA.length / 3) * 3;
|
||||
const mainGridClubs = CLUBS_DATA.slice(0, fullGridCount);
|
||||
const remainingClubs = CLUBS_DATA.slice(fullGridCount);
|
||||
|
||||
{/* Clubs Section */}
|
||||
<SectionTitle tag="Join a Club" title="Student" highlight="Clubs" subtitle="Explore clubs and societies at RIT. Find your passion and connect with like-minded peers." />
|
||||
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{CLUBS_DATA.map((club) => (
|
||||
<StaggerItem key={club.id}>
|
||||
const renderClubCard = (club: Club) => {
|
||||
const IconComponent = (club.icon && CLUB_ICON_MAP[club.icon]) || Atom;
|
||||
const categoryColor = CLUB_CATEGORY_COLORS[club.category] || '#F97316';
|
||||
const isLiked = likedClubs.has(club.id);
|
||||
const likesCount = likesMap[club.id] || 0;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
whileHover={{ y: -4 }}
|
||||
className="bg-white rounded-2xl border border-[#E5E7EB] p-5"
|
||||
onClick={() => setSelectedClub(club)}
|
||||
className="bg-white rounded-2xl border border-[#E5E7EB] p-5 cursor-pointer hover:border-[#F97316] transition-all flex flex-col justify-between h-full group"
|
||||
style={{ boxShadow: '0 2px 15px -3px rgba(0,0,0,0.07)' }}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div
|
||||
className="w-11 h-11 rounded-xl flex items-center justify-center text-white font-bold text-lg"
|
||||
style={{ background: `linear-gradient(135deg, ${CLUB_CATEGORY_COLORS[club.category] || '#F97316'}, ${CLUB_CATEGORY_COLORS[club.category] || '#F97316'}CC)`, fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{club.name[0]}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs text-[#94A3B8]">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
<span>{club.members} members</span>
|
||||
<div>
|
||||
{/* Top Row: Tech Icon / Club Logo & Interactive Heart Like Button */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
{club.logoUrl ? (
|
||||
<div className="w-12 h-12 rounded-full border border-slate-100 p-0.5 shadow-sm bg-white overflow-hidden shrink-0 transition-transform group-hover:scale-105">
|
||||
<img src={club.logoUrl} alt={club.name} className="w-full h-full object-cover rounded-full" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-11 h-11 rounded-full flex items-center justify-center text-white shadow-sm transition-transform group-hover:scale-105"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${categoryColor}, ${categoryColor}DD)`,
|
||||
}}
|
||||
>
|
||||
<IconComponent className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Like Button & Members Count */}
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.08 }}
|
||||
whileTap={{ scale: 0.85 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleLike(club.id);
|
||||
}}
|
||||
className={`flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold transition-all cursor-pointer border ${
|
||||
isLiked
|
||||
? 'bg-rose-50 border-rose-200 text-rose-600 shadow-2xs'
|
||||
: 'bg-slate-50 border-slate-200 text-slate-500 hover:border-rose-300 hover:text-rose-500'
|
||||
}`}
|
||||
>
|
||||
<Heart className={`w-3.5 h-3.5 ${isLiked ? 'fill-rose-500 text-rose-500' : ''}`} />
|
||||
<span>{likesCount}</span>
|
||||
</motion.button>
|
||||
|
||||
<div className="flex items-center gap-1 text-xs text-[#94A3B8] bg-slate-50 px-2 py-1 rounded-full border border-slate-100">
|
||||
<Users className="w-3.5 h-3.5 text-slate-400" />
|
||||
<span>{club.members}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Club Title & Description */}
|
||||
<h3 className="font-semibold text-[#1E293B] mb-1 group-hover:text-[#F97316] transition-colors" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
{club.name}
|
||||
</h3>
|
||||
<p className="text-xs text-[#64748B] mb-3 line-clamp-2" style={{ fontFamily: 'Inter, sans-serif' }}>{club.description}</p>
|
||||
</div>
|
||||
<h3 className="font-semibold text-[#1E293B] mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>{club.name}</h3>
|
||||
<p className="text-xs text-[#94A3B8] mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>{club.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-1 text-xs" style={{ color: CLUB_CATEGORY_COLORS[club.category] || '#F97316' }}>
|
||||
|
||||
{/* Bottom Row: Category Tag & View Details Button */}
|
||||
<div className="flex items-center justify-between mt-auto pt-2 border-t border-slate-100">
|
||||
<span className="flex items-center gap-1 text-xs font-medium" style={{ color: categoryColor }}>
|
||||
<Tag className="w-3 h-3" />
|
||||
{club.category}
|
||||
</span>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.04 }}
|
||||
className="px-4 py-1.5 rounded-lg text-xs font-semibold text-white"
|
||||
className="px-3.5 py-1.5 rounded-lg text-xs font-semibold text-white flex items-center gap-1 cursor-pointer"
|
||||
style={{ fontFamily: 'Poppins, sans-serif', background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
|
||||
>
|
||||
Join Club
|
||||
View Details
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</StaggerItem>
|
||||
))}
|
||||
</StaggerContainer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StaggerContainer className="pb-8 space-y-6 md:space-y-7">
|
||||
{/* 3-Column Grid for Full Rows */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-7">
|
||||
{mainGridClubs.map((club) => (
|
||||
<StaggerItem key={club.id} className="w-full h-full">
|
||||
{renderClubCard(club)}
|
||||
</StaggerItem>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Centered Flex Row for Remaining Clubs (WiSTEM & STEAM) */}
|
||||
{remainingClubs.length > 0 && (
|
||||
<div className="flex flex-wrap justify-center gap-6 md:gap-7">
|
||||
{remainingClubs.map((club) => (
|
||||
<StaggerItem key={club.id} className="w-full sm:w-[calc(50%-0.875rem)] lg:w-[calc(33.333%-1.167rem)]">
|
||||
{renderClubCard(club)}
|
||||
</StaggerItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</StaggerContainer>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* ─── Club Matcher Quiz Modal (Feature 1) ─────────────────────────────── */}
|
||||
<AnimatePresence>
|
||||
{isQuizOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="bg-white rounded-3xl border border-[#E5E7EB] max-w-xl w-full p-6 md:p-8 relative shadow-2xl overflow-hidden"
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => setIsQuizOpen(false)}
|
||||
className="absolute top-5 right-5 w-9 h-9 rounded-full bg-slate-100 flex items-center justify-center text-slate-500 hover:bg-slate-200 transition-all cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{!quizResults ? (
|
||||
<div>
|
||||
{/* Quiz Progress */}
|
||||
<div className="flex items-center justify-between text-xs text-slate-500 mb-2 font-medium">
|
||||
<span>Question {quizStep + 1} of {QUIZ_QUESTIONS.length}</span>
|
||||
<span>Step {quizStep + 1} / {QUIZ_QUESTIONS.length}</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 bg-slate-100 rounded-full mb-6 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#F97316] transition-all duration-300 rounded-full"
|
||||
style={{ width: `${((quizStep + 1) / QUIZ_QUESTIONS.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Question Header */}
|
||||
<div className="flex items-center gap-2 mb-2 text-[#F97316]">
|
||||
<Compass className="w-5 h-5" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider">Club Matcher Quiz</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-[#1E293B] mb-5" style={{ fontFamily: 'Playfair Display, serif' }}>
|
||||
{QUIZ_QUESTIONS[quizStep].title}
|
||||
</h3>
|
||||
|
||||
{/* Options List */}
|
||||
<div className="space-y-3 mb-6">
|
||||
{QUIZ_QUESTIONS[quizStep].options.map((opt, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handleSelectOption(idx)}
|
||||
className="w-full text-left p-4 rounded-2xl border border-slate-200 hover:border-[#F97316] hover:bg-orange-50/50 transition-all flex items-center justify-between text-sm font-medium text-slate-700 cursor-pointer group"
|
||||
>
|
||||
<span>{opt.label}</span>
|
||||
<ArrowRight className="w-4 h-4 text-slate-300 group-hover:text-[#F97316] group-hover:translate-x-1 transition-all" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{quizStep > 0 && (
|
||||
<button
|
||||
onClick={() => setQuizStep(quizStep - 1)}
|
||||
className="text-xs font-semibold text-slate-500 hover:text-slate-800 flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
← Previous Question
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* Results Screen */
|
||||
<div>
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-14 h-14 rounded-2xl bg-orange-100 text-[#F97316] flex items-center justify-center mx-auto mb-3">
|
||||
<CheckCircle2 className="w-8 h-8" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-[#1E293B]" style={{ fontFamily: 'Playfair Display, serif' }}>
|
||||
Your Ideal Club Matches!
|
||||
</h3>
|
||||
<p className="text-xs text-slate-500 mt-1">Based on your interests & goals, here are your top recommended RIT clubs:</p>
|
||||
</div>
|
||||
|
||||
{/* Top 3 Matches */}
|
||||
<div className="space-y-3 mb-6">
|
||||
{quizResults.map((item, i) => {
|
||||
const IconComponent = (item.club.icon && CLUB_ICON_MAP[item.club.icon]) || Atom;
|
||||
const catColor = CLUB_CATEGORY_COLORS[item.club.category] || '#F97316';
|
||||
const matchPercent = i === 0 ? '98%' : i === 1 ? '91%' : '84%';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.club.id}
|
||||
onClick={() => {
|
||||
setIsQuizOpen(false);
|
||||
setSelectedClub(item.club);
|
||||
}}
|
||||
className="p-4 rounded-2xl border border-slate-200 hover:border-[#F97316] bg-slate-50 hover:bg-white cursor-pointer transition-all flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{item.club.logoUrl ? (
|
||||
<div className="w-10 h-10 rounded-full border border-slate-100 p-0.5 shadow-sm bg-white overflow-hidden shrink-0">
|
||||
<img src={item.club.logoUrl} alt={item.club.name} className="w-full h-full object-cover rounded-full" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-white shrink-0"
|
||||
style={{ background: `linear-gradient(135deg, ${catColor}, ${catColor}DD)` }}
|
||||
>
|
||||
<IconComponent className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-sm text-[#1E293B]">{item.club.name}</span>
|
||||
{i === 0 && <span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-amber-100 text-amber-700">Top Match 🏆</span>}
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{item.club.category} Club • {item.club.members} members</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-3 py-1 rounded-full text-xs font-bold bg-emerald-50 text-emerald-600 border border-emerald-200 shrink-0">
|
||||
{matchPercent} Match
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={resetQuiz}
|
||||
className="flex-1 py-3 rounded-xl border border-slate-300 text-slate-600 font-semibold text-xs flex items-center justify-center gap-1.5 hover:bg-slate-50 transition-all cursor-pointer"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
Retake Quiz
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsQuizOpen(false)}
|
||||
className="flex-1 py-3 rounded-xl text-white font-semibold text-xs bg-[#F97316] hover:bg-[#EA580C] transition-all cursor-pointer"
|
||||
>
|
||||
Explore All Clubs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ─── Club Details Modal (Featuring Feature 5: Social Link Hub) ───────── */}
|
||||
<AnimatePresence>
|
||||
{selectedClub && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="bg-white rounded-3xl border border-[#E5E7EB] max-w-xl w-full max-h-[90vh] overflow-y-auto p-6 md:p-8 relative"
|
||||
style={{ boxShadow: '0 20px 50px rgba(0,0,0,0.2)' }}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => setSelectedClub(null)}
|
||||
className="absolute top-5 right-5 w-9 h-9 rounded-full bg-slate-100 flex items-center justify-center text-slate-500 hover:bg-slate-200 transition-all cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Modal Header */}
|
||||
{(() => {
|
||||
const IconComponent = (selectedClub.icon && CLUB_ICON_MAP[selectedClub.icon]) || Atom;
|
||||
const categoryColor = CLUB_CATEGORY_COLORS[selectedClub.category] || '#F97316';
|
||||
const isLiked = likedClubs.has(selectedClub.id);
|
||||
const likesCount = likesMap[selectedClub.id] || 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-5 pb-4 border-b border-slate-100">
|
||||
<div className="flex items-center gap-4">
|
||||
{selectedClub.logoUrl ? (
|
||||
<div className="w-16 h-16 rounded-full border-2 border-slate-100 p-1 shadow-md bg-white overflow-hidden shrink-0">
|
||||
<img src={selectedClub.logoUrl} alt={selectedClub.name} className="w-full h-full object-cover rounded-full" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="w-14 h-14 rounded-full flex items-center justify-center text-white font-bold text-2xl shadow-sm shrink-0"
|
||||
style={{ background: `linear-gradient(135deg, ${categoryColor}, ${categoryColor}DD)` }}
|
||||
>
|
||||
<IconComponent className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span
|
||||
className="px-2.5 py-0.5 rounded-full text-xs font-semibold"
|
||||
style={{ backgroundColor: `${categoryColor}15`, color: categoryColor, fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
{selectedClub.category}
|
||||
</span>
|
||||
<h2 className="text-2xl font-bold text-[#1E293B] mt-1" style={{ fontFamily: 'Playfair Display, serif' }}>
|
||||
{selectedClub.name}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interactive Like Button in Modal */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.92 }}
|
||||
onClick={() => toggleLike(selectedClub.id)}
|
||||
className={`px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-2 transition-all cursor-pointer shadow-xs border shrink-0 ${
|
||||
isLiked
|
||||
? 'bg-rose-500 text-white border-rose-600 shadow-rose-200'
|
||||
: 'bg-rose-50 text-rose-600 border-rose-200 hover:bg-rose-100'
|
||||
}`}
|
||||
>
|
||||
<Heart className={`w-4 h-4 ${isLiked ? 'fill-white' : 'text-rose-500'}`} />
|
||||
<span>{isLiked ? 'Liked' : 'Like Club'} ({likesCount})</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Club Detailed Description */}
|
||||
<div className="mb-5 bg-gradient-to-r from-orange-50/70 via-amber-50/40 to-slate-50 border border-orange-200/80 rounded-2xl p-4.5">
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-[#F97316] uppercase tracking-wider mb-2">
|
||||
<Info className="w-4 h-4 text-[#F97316]" />
|
||||
<span>About the Club</span>
|
||||
</div>
|
||||
<p className="text-slate-700 text-sm leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
{selectedClub.details || selectedClub.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Leadership & Contact Information - Icon Based Color Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5 mb-6">
|
||||
{/* President Card - Indigo Theme */}
|
||||
<div className="bg-indigo-50/80 border border-indigo-100 hover:border-indigo-300 transition-colors rounded-2xl p-3.5 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 to-indigo-600 text-white flex items-center justify-center shadow-xs shadow-indigo-200 shrink-0">
|
||||
<UserCheck className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[11px] text-indigo-500 font-semibold block">President / Student Lead</span>
|
||||
<span className="text-sm font-bold text-indigo-950">{selectedClub.presidentName || 'Student President'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Year Card - Purple Theme */}
|
||||
<div className="bg-purple-50/80 border border-purple-100 hover:border-purple-300 transition-colors rounded-2xl p-3.5 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 text-white flex items-center justify-center shadow-xs shadow-purple-200 shrink-0">
|
||||
<GraduationCap className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[11px] text-purple-500 font-semibold block">Year & Department</span>
|
||||
<span className="text-sm font-bold text-purple-950">{selectedClub.year || 'Senior Year'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email Card - Emerald Theme */}
|
||||
<div className="bg-emerald-50/80 border border-emerald-100 hover:border-emerald-300 transition-colors rounded-2xl p-3.5 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 text-white flex items-center justify-center shadow-xs shadow-emerald-200 shrink-0">
|
||||
<Mail className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="text-[11px] text-emerald-500 font-semibold block">Contact Email</span>
|
||||
<a href={`mailto:${selectedClub.contactEmail}`} className="text-sm font-bold text-emerald-700 hover:text-emerald-800 hover:underline truncate block">
|
||||
{selectedClub.contactEmail || 'club@ritchennai.edu.in'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone Card - Sky Theme */}
|
||||
<div className="bg-sky-50/80 border border-sky-100 hover:border-sky-300 transition-colors rounded-2xl p-3.5 flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-sky-500 to-sky-600 text-white flex items-center justify-center shadow-xs shadow-sky-200 shrink-0">
|
||||
<Phone className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[11px] text-sky-500 font-semibold block">Contact Phone</span>
|
||||
<span className="text-sm font-bold text-sky-950">{selectedClub.contactPhone || '+91 98765 43210'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Feature 5: Social & Community Link Hub (Dummy Links) ─────── */}
|
||||
<div className="mb-6 bg-orange-50/60 border border-orange-200/80 rounded-2xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-[#F97316] uppercase tracking-wider">
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
<span>Community & Social Links</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-emerald-600 font-medium">✓ Verified RIT Handle</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2.5">
|
||||
{selectedClub.socialLinks?.instagram && (
|
||||
<a
|
||||
href={selectedClub.socialLinks.instagram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-pink-500 hover:text-pink-600 transition-all text-xs font-medium text-slate-700"
|
||||
>
|
||||
<InstagramIcon className="w-4 h-4 text-pink-500 shrink-0" />
|
||||
<span className="truncate">Instagram</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{selectedClub.socialLinks?.website && (
|
||||
<a
|
||||
href={selectedClub.socialLinks.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-orange-500 hover:text-orange-600 transition-all text-xs font-medium text-slate-700"
|
||||
>
|
||||
<Globe className="w-4 h-4 text-orange-500 shrink-0" />
|
||||
<span className="truncate">Linktree Hub</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{selectedClub.socialLinks?.linkedin && (
|
||||
<a
|
||||
href={selectedClub.socialLinks.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-blue-600 hover:text-blue-600 transition-all text-xs font-medium text-slate-700"
|
||||
>
|
||||
<LinkedinIcon className="w-4 h-4 text-blue-600 shrink-0" />
|
||||
<span className="truncate">LinkedIn</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{selectedClub.socialLinks?.whatsapp && (
|
||||
<a
|
||||
href={selectedClub.socialLinks.whatsapp}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-emerald-500 hover:text-emerald-600 transition-all text-xs font-medium text-slate-700"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4 text-emerald-500 shrink-0" />
|
||||
<span className="truncate">WhatsApp Group</span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{selectedClub.socialLinks?.youtube && (
|
||||
<a
|
||||
href={selectedClub.socialLinks.youtube}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-2.5 rounded-xl bg-white border border-slate-200 hover:border-red-600 hover:text-red-600 transition-all text-xs font-medium text-slate-700"
|
||||
>
|
||||
<YoutubeIcon className="w-4 h-4 text-red-600 shrink-0" />
|
||||
<span className="truncate">YouTube Channel</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Close Button */}
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setSelectedClub(null)}
|
||||
className="w-full py-3 rounded-xl border border-slate-300 text-slate-700 font-semibold text-sm hover:bg-slate-100 transition-all cursor-pointer"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -227,11 +227,11 @@ export default function Home() {
|
||||
{ icon: 'Map', label: 'Campus Context', desc: 'RIT-specific answers' },
|
||||
{ icon: 'Zap', label: 'Instant Answers', desc: 'No wait time' },
|
||||
].map((item, i) => {
|
||||
const Icon = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[item.icon];
|
||||
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string; style?: React.CSSProperties }>>)[item.icon];
|
||||
return (
|
||||
<div key={i} className="flex items-start gap-3 p-4 rounded-xl border border-[#E5E7EB] bg-[#FAFAFA]">
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center bg-[#FFF7ED] shrink-0">
|
||||
{Icon && <Icon className="w-4.5 h-4.5" style={{ color: '#F97316' } as React.CSSProperties} />}
|
||||
{Icon && <Icon className="w-4.5 h-4.5" style={{ color: '#F97316' }} />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>{item.label}</div>
|
||||
@@ -331,7 +331,7 @@ export default function Home() {
|
||||
{/* Quick nav buttons */}
|
||||
<div className="p-5 grid grid-cols-4 sm:grid-cols-8 gap-3">
|
||||
{CAMPUS_LOCATIONS.map((loc) => {
|
||||
const Icon = (LucideIcons as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
|
||||
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
|
||||
return (
|
||||
<Link to="/campus" key={loc.id}>
|
||||
<motion.div
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function Notes() {
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0" style={{ backgroundColor: typeConfig.bg }}>
|
||||
<TypeIcon className="w-5 h-5" style={{ color: typeConfig.text } as React.CSSProperties} />
|
||||
<TypeIcon className="w-5 h-5 text-orange-500" />
|
||||
</div>
|
||||
<span
|
||||
className="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase"
|
||||
|
||||
@@ -35,6 +35,16 @@ export interface Faculty {
|
||||
office?: string;
|
||||
avatar?: string;
|
||||
specialization?: string;
|
||||
qualification?: string;
|
||||
experience?: string;
|
||||
subjectsHandling?: string;
|
||||
researchAreas?: string;
|
||||
officeHours?: string;
|
||||
achievements?: string[];
|
||||
publications?: string[];
|
||||
studentsGuided?: { ug: number; pg: number; phd: number };
|
||||
languagesKnown?: string[];
|
||||
joinedInstitution?: string;
|
||||
}
|
||||
|
||||
// ─── Bus Routes ──────────────────────────────────────────────────────────────
|
||||
@@ -85,14 +95,31 @@ export interface Event {
|
||||
}
|
||||
|
||||
// ─── Clubs ───────────────────────────────────────────────────────────────────
|
||||
export interface ClubSocialLinks {
|
||||
instagram?: string;
|
||||
linkedin?: string;
|
||||
whatsapp?: string;
|
||||
youtube?: string;
|
||||
github?: string;
|
||||
website?: string;
|
||||
}
|
||||
|
||||
export interface Club {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
details?: string;
|
||||
category: string;
|
||||
members: number;
|
||||
logo?: string;
|
||||
presidentName?: string;
|
||||
year?: string;
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
logo?: string;
|
||||
logoUrl?: string;
|
||||
icon?: string;
|
||||
defaultRank?: number;
|
||||
socialLinks?: ClubSocialLinks;
|
||||
}
|
||||
|
||||
// ─── Community / Q&A ─────────────────────────────────────────────────────────
|
||||
|
||||
BIN
telegram-bot/bot_mappings.db
Normal file
138
telegram-bot/scrape_bus_routes.py
Normal file
@@ -0,0 +1,138 @@
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
def scrape_routes():
|
||||
base_url = "https://www.rittransport.com/"
|
||||
index_url = urljoin(base_url, "js/51jan26.php")
|
||||
|
||||
print(f"Fetching index from {index_url}...")
|
||||
res = requests.get(index_url)
|
||||
res.encoding = 'utf-8'
|
||||
soup = BeautifulSoup(res.text, 'html.parser')
|
||||
|
||||
routes = []
|
||||
# Find table
|
||||
table = soup.find('table')
|
||||
if not table:
|
||||
print("No table found on index page.")
|
||||
return
|
||||
|
||||
rows = table.find_all('tr')
|
||||
for row in rows:
|
||||
cells = row.find_all('td')
|
||||
if len(cells) < 4:
|
||||
continue
|
||||
|
||||
sno = cells[0].get_text(strip=True)
|
||||
rno = cells[1].get_text(strip=True)
|
||||
rname = cells[2].get_text(strip=True)
|
||||
|
||||
# Link is in cells[3] (Timing column)
|
||||
link_tag = cells[3].find('a')
|
||||
if not link_tag or not link_tag.get('href'):
|
||||
continue
|
||||
|
||||
href = link_tag.get('href')
|
||||
detail_url = urljoin("https://www.rittransport.com/js/", href.strip())
|
||||
|
||||
start_time = cells[4].get_text(strip=True)
|
||||
|
||||
routes.append({
|
||||
"number": rno,
|
||||
"name": rname,
|
||||
"detail_url": detail_url,
|
||||
"start_time": start_time
|
||||
})
|
||||
|
||||
print(f"Found {len(routes)} routes. Fetching details...")
|
||||
|
||||
detailed_routes = []
|
||||
|
||||
# Colors for frontend UI
|
||||
colors = [
|
||||
'#F97316', '#3B82F6', '#10B981', '#8B5CF6', '#EF4444',
|
||||
'#EC4899', '#06B6D4', '#F59E0B', '#14B8A6', '#6366F1'
|
||||
]
|
||||
|
||||
for i, r in enumerate(routes):
|
||||
try:
|
||||
detail_res = requests.get(r['detail_url'])
|
||||
detail_res.encoding = 'utf-8'
|
||||
html_content = detail_res.text
|
||||
|
||||
dsoup = BeautifulSoup(html_content, 'html.parser')
|
||||
stops = []
|
||||
detail_rows = dsoup.find_all('tr')
|
||||
|
||||
for drow in detail_rows:
|
||||
tds = drow.find_all('td')
|
||||
if len(tds) >= 2:
|
||||
stop_name = tds[0].get_text(strip=True)
|
||||
stop_time = tds[1].get_text(strip=True)
|
||||
|
||||
stop_name = re.sub(r'\s+', ' ', stop_name).strip()
|
||||
stop_time = re.sub(r'\s+', ' ', stop_time).strip()
|
||||
|
||||
if stop_name and not stop_name.startswith("R-") and not "Boarding" in stop_name:
|
||||
# Normalize time format
|
||||
stop_time = stop_time.replace('.', ':')
|
||||
if ':' in stop_time:
|
||||
parts = stop_time.split()
|
||||
time_part = parts[0]
|
||||
ampm_part = parts[1].upper() if len(parts) > 1 else "AM"
|
||||
h_m = time_part.split(':')
|
||||
if len(h_m) == 2:
|
||||
h, m = h_m[0].strip(), h_m[1].strip()
|
||||
h = "".join(filter(str.isdigit, h))
|
||||
m = "".join(filter(str.isdigit, m))
|
||||
if h and m:
|
||||
h_int = int(h)
|
||||
h_str = f"{h_int:02d}"
|
||||
m_str = f"{int(m):02d}"
|
||||
stop_time = f"{h_str}:{m_str} {ampm_part}"
|
||||
|
||||
stops.append({
|
||||
"name": stop_name,
|
||||
"time": stop_time
|
||||
})
|
||||
|
||||
if len(stops) > 0:
|
||||
from_point = stops[0]['name']
|
||||
to_point = stops[-1]['name']
|
||||
departure_time = stops[0]['time']
|
||||
arrival_time = stops[-1]['time']
|
||||
else:
|
||||
from_point = r['name']
|
||||
to_point = "RIT Campus"
|
||||
departure_time = r['start_time']
|
||||
arrival_time = "07:40 AM"
|
||||
|
||||
detailed_routes.append({
|
||||
"number": r['number'],
|
||||
"name": r['name'] + " Route",
|
||||
"from": from_point,
|
||||
"to": to_point,
|
||||
"departureTime": departure_time,
|
||||
"arrivalTime": arrival_time,
|
||||
"color": colors[i % len(colors)],
|
||||
"stops": stops
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error scraping {r['number']}: {e}")
|
||||
|
||||
# Save directly to the backend resources folder
|
||||
target_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "backend", "src", "main", "resources"))
|
||||
if not os.path.exists(target_dir):
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
output_path = os.path.join(target_dir, "bus_routes.json")
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(detailed_routes, f, indent=2)
|
||||
print(f"Scraping completed. Saved to {output_path}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
scrape_routes()
|
||||
28
test-filter.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const FACULTY_DATA = [
|
||||
{
|
||||
id: '9',
|
||||
name: 'Prof. R. Vijayakumar',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Mechanical Engineering',
|
||||
office: 'MECH Block, Room 102',
|
||||
specialization: 'CAD/CAM, Robotics',
|
||||
email: 'vijayakumar@rit.edu',
|
||||
}
|
||||
];
|
||||
|
||||
const searchFaculty = "";
|
||||
const selectedDept = "Mechanical Engineering";
|
||||
|
||||
const filteredFaculty = FACULTY_DATA.filter((f) => {
|
||||
const searchLower = searchFaculty.toLowerCase();
|
||||
const matchSearch =
|
||||
f.name.toLowerCase().includes(searchLower) ||
|
||||
f.department.toLowerCase().includes(searchLower) ||
|
||||
f.designation.toLowerCase().includes(searchLower) ||
|
||||
(f.specialization && f.specialization.toLowerCase().includes(searchLower));
|
||||
|
||||
const matchDept = selectedDept === 'All Departments' || f.department === selectedDept;
|
||||
return matchSearch && matchDept;
|
||||
});
|
||||
|
||||
console.log(filteredFaculty);
|
||||
1
test-render.js
Normal file
@@ -0,0 +1 @@
|
||||
const { FACULTY_DATA } = require('./src/constants/index.ts'); // wait this is TS
|
||||
@@ -17,6 +17,7 @@
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Path aliases */
|
||||
"ignoreDeprecations": "6.0",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
|
||||
@@ -9,6 +9,15 @@ export default defineConfig({
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: [
|
||||
'**/backend/**',
|
||||
'**/telegram-bot/**',
|
||||
'**/.git/**'
|
||||
]
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
|
||||