Compare commits

..

10 Commits

Author SHA1 Message Date
Shanmuga Krishnan S M
8433fe33b2 docs: add complete VPS operations and maintenance master guide 2026-08-04 09:15:36 +05:30
Shanmuga Krishnan S M
2a86af0113 fix(ui): ensure instant video playback with auto-muted state and high-contrast white text for hero section 2026-08-04 09:08:52 +05:30
Shanmuga Krishnan S M
1852239452 feat(video): add WhatsApp Video asset to public/video for LazyVideoHero background overlay 2026-08-04 09:06:04 +05:30
Shanmuga Krishnan S M
b461c6cf32 fix(ui): activate dark video overlay only when video is playing 2026-08-04 09:03:39 +05:30
Shanmuga Krishnan S M
68902caaa4 feat(ui): add ultra-optimized LazyVideoHero component with IntersectionObserver auto-pause, section-scoping, and CSS layout containment 2026-08-04 08:58:29 +05:30
Shanmuga Krishnan S M
6229128dd2 fix(bot): restrict collab application notifications strictly to author's telegram_chat_id & disable Discord collab functionality 2026-08-04 08:26:41 +05:30
Shanmuga Krishnan S M
7067f7cbad perf and ui: remove heavy video, strip RAG and Gemini labels, and deliver sleek modern UI 2026-08-04 08:06:25 +05:30
Shanmuga Krishnan S M
674eb437b4 fix(bus-routes): normalize route number matching for R01 vs 1 vs R15 formats 2026-08-03 14:44:49 +05:30
Shanmuga Krishnan S M
5676bb0950 feat(driver-app): add continuous 5s periodic uploader timer fallback in TrackingService 2026-08-03 14:41:49 +05:30
Shanmuga Krishnan S M
add0fbb47c fix(driver-app): use Locale.US in NetworkHelper to prevent invalid JSON formatting 2026-08-03 14:36:20 +05:30
12 changed files with 487 additions and 304 deletions

223
VPS_MASTER_GUIDE.md Normal file
View File

@@ -0,0 +1,223 @@
# 🚀 RIT Freshers Hub - Complete VPS Operations & Maintenance Guide
A complete, beginner-friendly guide to managing, updating, monitoring, and troubleshooting the **RIT Freshers Hub** production server on Linux (Ubuntu / Debian).
---
## 📍 Server & Architecture Summary
* **Server IP:** `129.121.126.66`
* **SSH User:** `root`
* **Primary Domain:** `rit-services.in`
* **Backend API Domain:** `api.rit-services.in`
* **Application Root Path:** `/var/www/freshers-hub`
### 🏗️ Microservice Ports & Services
| Service | Technology | Port | Systemd Service Name | Config Path |
| :--- | :--- | :--- | :--- | :--- |
| **Frontend PWA** | React + Vite + Nginx | `80` / `443` | `nginx` | `/etc/nginx/sites-available/default` |
| **Backend API** | Spring Boot (Java 17) | `8085` | `springboot.service` | `/etc/systemd/system/springboot.service` |
| **Go Chatbot** | Go BM25 Engine | `8081` | `chatbot.service` | `/etc/systemd/system/chatbot.service` |
| **Bots (Telegram/Discord)** | Python FastAPI | `8082` | `telegram-bot.service` | `/var/www/freshers-hub/telegram-bot/.env` |
| **Database** | PostgreSQL | `5432` | `postgresql` | DB User: `postgres`, Password: `RITHosting123` |
---
## 🔑 1. How to Connect to the VPS
Open PowerShell, Command Prompt, or Terminal on your computer and run:
```bash
ssh root@129.121.126.66
```
*(Enter your SSH password when prompted)*
---
## ⚡ 2. Daily Commands (Deploying Updates from GitHub)
Whenever you push new code to GitHub and want to update the live website:
### A) Update Everything in 1 Command Block
Copy and paste this into your VPS terminal:
```bash
cd /var/www/freshers-hub && \
git pull && \
npm run build && \
(cd chatbot-service && go build -o chatbot-service main.go) && \
systemctl restart springboot chatbot telegram-bot nginx
```
---
### B) Updating Specific Parts (Step-by-Step)
#### 1. Update Frontend UI & Static Files Only:
```bash
cd /var/www/freshers-hub
git pull
npm run build
```
#### 2. Restart Spring Boot Backend:
```bash
systemctl restart springboot
```
#### 3. Restart Go Chatbot Microservice:
```bash
cd /var/www/freshers-hub/chatbot-service
git pull
go build -o chatbot-service main.go
systemctl restart chatbot
```
#### 4. Restart Telegram & Discord Bot:
```bash
cd /var/www/freshers-hub/telegram-bot
git pull
systemctl restart telegram-bot
```
---
## 📊 3. Health & Status Check Commands
Want to check if all services are running healthy? Run these commands:
### Check Status of All Services:
```bash
systemctl status springboot chatbot telegram-bot nginx postgresql
```
### Check Active Listening Ports:
```bash
ss -tulpn | grep -E '8085|8081|8082|80|443|5432'
```
*(You should see Java on 8085, Go on 8081, Python on 8082, Nginx on 80/443, PostgreSQL on 5432).*
---
## 📜 4. Viewing Live System Logs (Debugging Errors)
If a feature is failing or you want to see live incoming requests:
### 1. View Spring Boot Backend Logs:
```bash
journalctl -u springboot -n 50 --no-pager
```
*To follow logs live in real-time:* `journalctl -u springboot -f`
### 2. View Go Chatbot Logs:
```bash
journalctl -u chatbot -n 50 --no-pager
```
### 3. View Telegram & Discord Bot Logs:
```bash
journalctl -u telegram-bot -n 50 --no-pager
```
### 4. View Nginx Web Server Access & Error Logs:
```bash
# Recent web traffic requests:
tail -n 30 /var/log/nginx/access.log
# Recent web server errors:
tail -n 30 /var/log/nginx/error.log
```
---
## 🛠️ 5. Common Troubleshooting & Emergency Fixes
### 🚨 Problem 1: "502 Bad Gateway" on Website
* **Cause:** The Spring Boot backend or Go chatbot is stopped.
* **Fix:** Restart all services:
```bash
systemctl restart springboot chatbot nginx
```
---
### 🚨 Problem 2: Nginx Configuration Test Fails
* **Check for Nginx syntax errors:**
```bash
sudo nginx -t
```
* **If it says syntax is OK, reload Nginx:**
```bash
sudo systemctl reload nginx
```
---
### 🚨 Problem 3: Check & Renew SSL Certificates (HTTPS)
* **Check all SSL certificates & expiry dates:**
```bash
sudo certbot certificates
```
* **Force renew SSL certificates (if needed):**
```bash
sudo certbot renew --force-renewal
sudo systemctl reload nginx
```
---
### 🚨 Problem 4: PostgreSQL Database Connection Error
* **Check PostgreSQL status:**
```bash
systemctl status postgresql
```
* **Test Database Password manually:**
```bash
PGPASSWORD='RITHosting123' psql -U postgres -h localhost -d freshers_hub_db -c '\dt'
```
---
### 🚨 Problem 5: Full Server Reboot (Emergency Reset)
If the server becomes completely unresponsive or memory is full:
```bash
sudo reboot
```
*Wait 60 seconds, reconnect via SSH, and check services:*
```bash
systemctl status springboot chatbot telegram-bot nginx postgresql
```
---
## 💾 6. Database Backup & Restore
### Backup PostgreSQL Database to a `.sql` File:
```bash
PGPASSWORD='RITHosting123' pg_dump -U postgres -h localhost freshers_hub_db > /root/backup_freshers_hub_$(date +%F).sql
```
### Restore Database from a `.sql` Backup File:
```bash
PGPASSWORD='RITHosting123' psql -U postgres -h localhost freshers_hub_db < /root/backup_freshers_hub_2026-08-04.sql
```
---
## 🛡️ 7. Systemd Service File Reference Paths
For reference, all auto-start systemd service configuration files are stored at:
1. **Spring Boot Backend:** `/etc/systemd/system/springboot.service`
2. **Go Chatbot Engine:** `/etc/systemd/system/chatbot.service`
3. **Telegram & Discord Bot:** `/etc/systemd/system/telegram-bot.service`
4. **Nginx Web Server Config:** `/etc/nginx/sites-available/default`
---
### 🏆 You're all set!
Save this document for your team to easily maintain, update, and manage the server with zero prior Linux experience.

BIN
image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -2,7 +2,7 @@ package com.rit.driver;
public class Config { public class Config {
public static final String BACKEND_URL = "http://10.43.158.201:8085"; public static final String BACKEND_URL = "https://rit-services.in";
public static final String DEFAULT_PIN = "RITDRIVER"; public static final String DEFAULT_PIN = "RITDRIVER";

View File

@@ -36,9 +36,10 @@ public class NetworkHelper {
conn.setConnectTimeout(8000); conn.setConnectTimeout(8000);
conn.setReadTimeout(8000); conn.setReadTimeout(8000);
// Build JSON string payload // Build JSON string payload using Locale.US to ensure dot decimals in JSON
String jsonInputString = String.format( String jsonInputString = String.format(
"{\"latitude\": %f, \"longitude\": %f, \"pin\": \"%s\"}", java.util.Locale.US,
"{\"latitude\": %.6f, \"longitude\": %.6f, \"pin\": \"%s\"}",
lat, lng, pin lat, lng, pin
); );

View File

@@ -97,6 +97,35 @@ public class TrackingService extends Service {
createNotificationChannel(); createNotificationChannel();
} }
private android.os.Handler handler = new android.os.Handler(android.os.Looper.getMainLooper());
private Runnable periodicUploader = new Runnable() {
@Override
public void run() {
try {
Location loc = null;
if (locationManager != null) {
try {
Location gpsLoc = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
Location netLoc = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
loc = (gpsLoc != null) ? gpsLoc : netLoc;
} catch (SecurityException ignored) {}
}
if (loc != null) {
locationListener.onLocationChanged(loc);
} else {
Intent statusIntent = new Intent(ACTION_LOCATION_BROADCAST);
statusIntent.putExtra(EXTRA_STATUS, "Waiting for initial GPS location fix...");
sendBroadcast(statusIntent);
}
} catch (Exception e) {
Log.e(TAG, "Error in periodic uploader: " + e.getMessage());
} finally {
handler.postDelayed(this, 5000);
}
}
};
@SuppressLint("InvalidWakeLockTag") @SuppressLint("InvalidWakeLockTag")
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
@@ -137,39 +166,22 @@ public class TrackingService extends Service {
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
try { try {
if (locationManager != null) { if (locationManager != null) {
// Request GPS updates
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationManager.requestLocationUpdates( locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 3000, 0.0f, locationListener);
LocationManager.GPS_PROVIDER,
5000,
0.0f,
locationListener
);
} }
// Request Network/Wi-Fi location updates (essential indoors)
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.requestLocationUpdates( locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 3000, 0.0f, locationListener);
LocationManager.NETWORK_PROVIDER,
5000,
0.0f,
locationListener
);
} }
// Immediately trigger initial fix if available
Location lastGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
Location lastNet = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
Location bestLocation = (lastGps != null) ? lastGps : lastNet;
if (bestLocation != null) {
locationListener.onLocationChanged(bestLocation);
}
Log.d(TAG, "Location Listeners registered successfully");
} }
} catch (SecurityException e) { } catch (SecurityException e) {
Log.e(TAG, "SecurityException: Location permissions not granted", e); Log.e(TAG, "SecurityException: Location permissions not granted", e);
stopSelf(); stopSelf();
} }
// 4. Start periodic 5-second uploader
handler.removeCallbacks(periodicUploader);
handler.post(periodicUploader);
return START_REDELIVER_INTENT; return START_REDELIVER_INTENT;
} }
@@ -177,6 +189,11 @@ public class TrackingService extends Service {
public void onDestroy() { public void onDestroy() {
Log.d(TAG, "onDestroy: Stopping service cleanups"); Log.d(TAG, "onDestroy: Stopping service cleanups");
// Stop periodic uploader timer
if (handler != null && periodicUploader != null) {
handler.removeCallbacks(periodicUploader);
}
// 1. Remove GPS Listener // 1. Remove GPS Listener
if (locationManager != null) { if (locationManager != null) {
locationManager.removeUpdates(locationListener); locationManager.removeUpdates(locationListener);

View File

@@ -187,7 +187,8 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
/> />
{Object.entries(allLiveLocations).map(([rNum, loc]) => { {Object.entries(allLiveLocations).map(([rNum, loc]) => {
if (selectedRoute && selectedRoute.number !== rNum) return null; const normalizeRouteNum = (str: string) => (str || '').replace(/^[Rr]0*/, '').trim().toUpperCase();
if (selectedRoute && normalizeRouteNum(selectedRoute.number) !== normalizeRouteNum(rNum)) return null;
const isStale = loc.stopped || (loc.lastUpdated ? (new Date().getTime() - new Date(loc.lastUpdated).getTime() > 120000) : false); const isStale = loc.stopped || (loc.lastUpdated ? (new Date().getTime() - new Date(loc.lastUpdated).getTime() > 120000) : false);

View File

@@ -0,0 +1,84 @@
import { useEffect, useRef, useState } from 'react';
interface LazyVideoHeroProps {
children?: React.ReactNode;
}
export default function LazyVideoHero({ children }: LazyVideoHeroProps) {
const containerRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const [isIntersecting, setIsIntersecting] = useState(true);
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
useEffect(() => {
if (videoRef.current) {
videoRef.current.muted = true;
videoRef.current.play().catch(() => {});
}
const container = containerRef.current;
if (!container) return;
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);
if (videoRef.current) {
if (entry.isIntersecting) {
videoRef.current.muted = true;
videoRef.current.play().catch(() => {});
} else {
videoRef.current.pause();
}
}
},
{ threshold: 0.1 }
);
observer.observe(container);
return () => {
observer.disconnect();
};
}, []);
return (
<div ref={containerRef} className="relative w-full overflow-hidden">
{/* ─── Optimized Video Background ────────────────────────────────────────── */}
<div
className="absolute inset-0 w-full h-full z-0 pointer-events-none"
style={{ contain: 'strict', willChange: 'transform' }}
>
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
preload="auto"
className="w-full h-full object-cover transition-opacity duration-500"
style={{
filter: 'contrast(1.1) brightness(0.85) saturate(1.1)',
opacity: isIntersecting ? 1 : 0,
contain: 'strict',
}}
>
<source src="/video/whatsapp-video.mp4" type="video/mp4" />
<source src="/video/whatsapp-video.webm" type="video/webm" />
</video>
{/* High-Contrast Glassmorphic Overlay Gradient */}
<div
className="absolute inset-0"
style={{
background: 'linear-gradient(to bottom, rgba(15, 23, 42, 0.5) 0%, rgba(15, 23, 42, 0.8) 100%)',
}}
/>
</div>
{/* ─── Hero Content ──────────────────────────────────────────────────────── */}
<div className="relative z-10">
{children}
</div>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Bot, Send, Sparkles, RefreshCw, Mic, X, ChevronRight } from 'lucide-react'; import { Bot, Send, Sparkles, RefreshCw, X, ChevronRight, MessageSquare, ShieldCheck, Zap } from 'lucide-react';
import { AI_SUGGESTED_PROMPTS } from '@/constants'; import { AI_SUGGESTED_PROMPTS } from '@/constants';
import type { ChatMessage } from '@/types'; import type { ChatMessage } from '@/types';
import { getChatbotUrl } from '@/lib/utils'; import { getChatbotUrl } from '@/lib/utils';
@@ -9,7 +9,7 @@ const INITIAL_MESSAGES: ChatMessage[] = [
{ {
id: '1', id: '1',
role: 'assistant', role: 'assistant',
content: "👋 Hi there! I'm your **RIT Chatbot**. I have comprehensive knowledge about Rajalakshmi Institute of Technology — from admission procedures to campus facilities.\n\nHow can I help you today?", content: "👋 Hi there! I'm your **RIT Assistant**. I have comprehensive knowledge about Rajalakshmi Institute of Technology — from admissions to campus facilities, bus routes, and academic details.\n\nHow can I help you today?",
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}, },
]; ];
@@ -42,7 +42,8 @@ export default function AIAssistant() {
}; };
setMessages((prev) => [...prev, userMsg]); setMessages((prev) => [...prev, userMsg]);
setInput(''); setInput('');
setIsTyping(true); // Call the Go chatbot microservice setIsTyping(true);
let responseText = ''; let responseText = '';
try { try {
const res = await fetch(getChatbotUrl('/api/chat'), { const res = await fetch(getChatbotUrl('/api/chat'), {
@@ -73,7 +74,7 @@ export default function AIAssistant() {
} else if (lower.includes('library')) { } else if (lower.includes('library')) {
responseText = responses.library; responseText = responses.library;
} else { } else {
responseText = `Thank you for your question about **"${text}"**!\n\nI couldn't reach the chatbot API server. Please make sure the Go service is running on ${getChatbotUrl()}.`; responseText = `Thank you for your question about **"${text}"**!\n\nI couldn't reach the chatbot API server. Please make sure the Go service is running.`;
} }
} }
@@ -100,103 +101,103 @@ export default function AIAssistant() {
}; };
return ( return (
<div className="min-h-screen flex flex-col" style={{ backgroundColor: '#FAFAFA' }}> <div className="min-h-screen flex flex-col bg-[#FAFAFA]">
{/* Header */} {/* Header */}
<div className="bg-white border-b border-[#E5E7EB] py-4 relative z-10"> <div className="bg-white border-b border-[#E5E7EB] py-4 relative z-10 shadow-xs">
<div className="container-custom flex items-center justify-between"> <div className="container-custom flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}> <div className="w-10 h-10 rounded-xl flex items-center justify-center shadow-sm" style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}>
<Bot className="w-5 h-5 text-white" /> <Bot className="w-5.5 h-5.5 text-white" />
</div> </div>
<div> <div>
<h1 className="font-semibold text-[#1E293B] text-sm" style={{ fontFamily: 'Poppins, sans-serif' }}>RIT Chatbot</h1> <h1 className="font-bold text-[#1E293B] text-base" style={{ fontFamily: 'Poppins, sans-serif' }}>RIT Assistant</h1>
<div className="flex items-center gap-1.5 text-xs text-[#94A3B8]"> <div className="flex items-center gap-1.5 text-xs text-[#64748B]">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" /> <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
Campus Knowledge Enabled Campus Knowledge Engine Online
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-2 sm:gap-3">
<a <a
href="https://t.me/Ritchatbot_bot" href="https://t.me/Ritchatbot_bot"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold text-white transition-all shadow-sm hover:brightness-105 active:scale-95" className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-medium text-white transition-all shadow-xs hover:brightness-105 active:scale-95"
style={{ style={{
background: 'linear-gradient(135deg, #0088cc, #24A1DE)', background: 'linear-gradient(135deg, #0088cc, #24A1DE)',
fontFamily: 'Inter, sans-serif' fontFamily: 'Inter, sans-serif'
}} }}
> >
<Send className="w-3.5 h-3.5" /> <Send className="w-3.5 h-3.5" />
Use on Telegram <span className="hidden sm:inline">Telegram Bot</span>
</a> </a>
<a <a
href="https://discord.com/api/oauth2/authorize?client_id=1476942599167414506&permissions=8&scope=bot" href="https://discord.com/api/oauth2/authorize?client_id=1476942599167414506&permissions=8&scope=bot"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold text-white transition-all shadow-sm hover:brightness-105 active:scale-95" className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-medium text-white transition-all shadow-xs hover:brightness-105 active:scale-95"
style={{ style={{
background: 'linear-gradient(135deg, #5865F2, #7289DA)', background: 'linear-gradient(135deg, #5865F2, #7289DA)',
fontFamily: 'Inter, sans-serif' fontFamily: 'Inter, sans-serif'
}} }}
> >
<Bot className="w-3.5 h-3.5" /> <Bot className="w-3.5 h-3.5" />
Use on Discord <span className="hidden sm:inline">Discord Bot</span>
</a> </a>
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.97 }}
onClick={clearChat} onClick={clearChat}
className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs text-[#94A3B8] hover:text-[#475569] hover:bg-gray-100 transition-all border border-[#E5E7EB] bg-white" className="flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-medium text-[#64748B] hover:text-[#1E293B] hover:bg-slate-100 transition-all border border-[#E5E7EB] bg-white shadow-2xs"
> >
<RefreshCw className="w-3.5 h-3.5" /> <RefreshCw className="w-3.5 h-3.5" />
New Chat <span>Reset</span>
</motion.button> </motion.button>
</div> </div>
</div> </div>
</div> </div>
<div className="flex-1 container-custom py-6 flex flex-col gap-4 max-w-4xl mx-auto w-full"> <div className="flex-1 container-custom py-6 flex flex-col gap-4 max-w-4xl mx-auto w-full">
{/* Features strip */} {/* Sleek Badge Strip */}
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
className="flex flex-wrap gap-2" className="flex flex-wrap gap-2"
> >
{[ {[
{ icon: Sparkles, label: 'Gemini AI' }, { icon: Zap, label: 'Instant Responses' },
{ icon: Bot, label: 'RAG Enabled' }, { icon: ShieldCheck, label: 'Verified Campus Info' },
{ icon: ChevronRight, label: 'Campus Knowledge' }, { icon: MessageSquare, label: '24/7 Assistance' },
].map((item, i) => ( ].map((item, i) => (
<div key={i} className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-white border border-[#E5E7EB] text-xs text-[#475569]"> <div key={i} className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-white border border-[#E5E7EB] text-xs font-medium text-[#475569] shadow-2xs">
<item.icon className="w-3.5 h-3.5 text-[#F97316]" /> <item.icon className="w-3.5 h-3.5 text-[#F97316]" />
{item.label} {item.label}
</div> </div>
))} ))}
</motion.div> </motion.div>
{/* Messages */} {/* Chat Container */}
<div <div
className="flex-1 bg-white rounded-3xl border border-[#E5E7EB] p-6 flex flex-col gap-4 overflow-y-auto" className="flex-1 bg-white rounded-3xl border border-[#E5E7EB] p-4 sm:p-6 flex flex-col gap-4 overflow-y-auto"
style={{ minHeight: '400px', maxHeight: '65vh', boxShadow: '0 4px 25px -5px rgba(0,0,0,0.08)' }} style={{ minHeight: '420px', maxHeight: '65vh', boxShadow: '0 8px 30px -6px rgba(0,0,0,0.04)' }}
> >
{messages.map((msg) => ( {messages.map((msg) => (
<motion.div <motion.div
key={msg.id} key={msg.id}
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.25 }}
className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`} className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
> >
{msg.role === 'assistant' && ( {msg.role === 'assistant' && (
<div className="w-8 h-8 rounded-xl flex items-center justify-center shrink-0 mt-1" style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}> <div className="w-8 h-8 rounded-xl flex items-center justify-center shrink-0 mt-1 shadow-xs" style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}>
<Bot className="w-4 h-4 text-white" /> <Bot className="w-4 h-4 text-white" />
</div> </div>
)} )}
<div <div
className="max-w-[80%] px-4 py-3 text-sm leading-relaxed" className="max-w-[85%] sm:max-w-[78%] px-4 py-3 text-sm leading-relaxed"
style={{ style={{
background: msg.role === 'user' ? 'linear-gradient(135deg, #F97316, #FB923C)' : '#F8FAFC', background: msg.role === 'user' ? 'linear-gradient(135deg, #F97316, #EA580C)' : '#F8FAFC',
color: msg.role === 'user' ? 'white' : '#1E293B', color: msg.role === 'user' ? 'white' : '#1E293B',
borderRadius: msg.role === 'user' ? '20px 20px 4px 20px' : '20px 20px 20px 4px', borderRadius: msg.role === 'user' ? '20px 20px 4px 20px' : '20px 20px 20px 4px',
border: msg.role === 'assistant' ? '1px solid #E5E7EB' : 'none', border: msg.role === 'assistant' ? '1px solid #E5E7EB' : 'none',
@@ -207,7 +208,7 @@ export default function AIAssistant() {
</motion.div> </motion.div>
))} ))}
{/* Typing */} {/* Typing Indicator */}
<AnimatePresence> <AnimatePresence>
{isTyping && ( {isTyping && (
<motion.div <motion.div
@@ -216,16 +217,16 @@ export default function AIAssistant() {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="flex gap-3" className="flex gap-3"
> >
<div className="w-8 h-8 rounded-xl flex items-center justify-center shrink-0" style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}> <div className="w-8 h-8 rounded-xl flex items-center justify-center shrink-0 shadow-xs" style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}>
<Bot className="w-4 h-4 text-white" /> <Bot className="w-4 h-4 text-white" />
</div> </div>
<div className="flex items-center gap-1 px-4 py-3 bg-[#F8FAFC] rounded-2xl border border-[#E5E7EB]"> <div className="flex items-center gap-1.5 px-4 py-3 bg-[#F8FAFC] rounded-2xl border border-[#E5E7EB]">
{[0, 1, 2].map((i) => ( {[0, 1, 2].map((i) => (
<motion.span <motion.span
key={i} key={i}
className="w-2 h-2 bg-[#94A3B8] rounded-full" className="w-2 h-2 bg-[#F97316] rounded-full"
animate={{ y: [0, -5, 0] }} animate={{ y: [0, -5, 0] }}
transition={{ duration: 0.8, repeat: Infinity, delay: i * 0.15 }} transition={{ duration: 0.6, repeat: Infinity, delay: i * 0.15 }}
/> />
))} ))}
</div> </div>
@@ -240,10 +241,10 @@ export default function AIAssistant() {
{AI_SUGGESTED_PROMPTS.slice(0, 4).map((prompt, i) => ( {AI_SUGGESTED_PROMPTS.slice(0, 4).map((prompt, i) => (
<motion.button <motion.button
key={i} key={i}
whileHover={{ scale: 1.02, backgroundColor: '#FFF7ED' }} whileHover={{ scale: 1.02, backgroundColor: '#FFF7ED', borderColor: '#FED7AA' }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
onClick={() => sendMessage(prompt)} onClick={() => sendMessage(prompt)}
className="px-3.5 py-2 rounded-xl border border-[#E5E7EB] text-xs text-[#475569] bg-white transition-all hover:border-[#F97316] hover:text-[#F97316]" className="px-3.5 py-2 rounded-xl border border-[#E5E7EB] text-xs text-[#475569] bg-white transition-all hover:text-[#F97316] shadow-2xs font-medium"
style={{ fontFamily: 'Inter, sans-serif' }} style={{ fontFamily: 'Inter, sans-serif' }}
> >
{prompt} {prompt}
@@ -251,16 +252,16 @@ export default function AIAssistant() {
))} ))}
</div> </div>
{/* Input */} {/* Input Field */}
<div className="bg-white rounded-2xl border border-[#E5E7EB] p-3 flex items-center gap-3" style={{ boxShadow: '0 4px 20px -4px rgba(0,0,0,0.08)' }}> <div className="bg-white rounded-2xl border border-[#E5E7EB] p-2.5 flex items-center gap-3 shadow-sm focus-within:border-[#F97316] transition-all">
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Ask anything about RIT..." placeholder="Ask anything about RIT campus, routes, hostel, or exams..."
className="flex-1 text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none bg-transparent" className="flex-1 text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none bg-transparent pl-2"
style={{ fontFamily: 'Inter, sans-serif' }} style={{ fontFamily: 'Inter, sans-serif' }}
/> />
{input && ( {input && (
@@ -274,22 +275,20 @@ export default function AIAssistant() {
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</motion.button> </motion.button>
)} )}
<button className="w-9 h-9 rounded-xl flex items-center justify-center text-[#94A3B8] hover:bg-gray-100 transition-all">
<Mic className="w-4.5 h-4.5" />
</button>
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
onClick={() => sendMessage(input)} onClick={() => sendMessage(input)}
disabled={!input.trim()} disabled={!input.trim()}
className="w-10 h-10 rounded-xl flex items-center justify-center text-white transition-all disabled:opacity-50" className="w-10 h-10 rounded-xl flex items-center justify-center text-white transition-all disabled:opacity-40 shadow-xs cursor-pointer"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }} style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}
> >
<Send className="w-4 h-4" /> <Send className="w-4 h-4" />
</motion.button> </motion.button>
</div> </div>
<p className="text-[11px] text-center text-[#94A3B8] mt-3" style={{ fontFamily: 'Inter, sans-serif' }}>
Chatbot responses may not always be accurate. Please cross-reference critical academic or administrative details with official sources. <p className="text-[11px] text-center text-[#94A3B8]" style={{ fontFamily: 'Inter, sans-serif' }}>
RIT Assistant provides official campus guidance. For administrative requests, visit the department block.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -1,73 +1,51 @@
import { motion, useScroll, useTransform } from 'framer-motion'; import { motion, useScroll, useTransform } from 'framer-motion';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
ArrowRight, Sparkles, BookOpen, Bot, GraduationCap, ArrowRight, BookOpen, Bot, GraduationCap,
CheckCircle, Zap, Shield, Brain, Trophy, Award, Cpu, Users CheckCircle, Zap, Shield, Trophy, Award, Cpu, Users, MapPin, Sparkles
} from 'lucide-react'; } from 'lucide-react';
import SectionTitle from '@/components/SectionTitle/SectionTitle'; import SectionTitle from '@/components/SectionTitle/SectionTitle';
import FeatureCard from '@/components/FeatureCard/FeatureCard'; import FeatureCard from '@/components/FeatureCard/FeatureCard';
import FloatingCard from '@/components/FloatingCard/FloatingCard';
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer'; import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer'; import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
import { FEATURES, CAMPUS_LOCATIONS } from '@/constants'; import { FEATURES, CAMPUS_LOCATIONS } from '@/constants';
import * as LucideIcons from 'lucide-react'; import * as LucideIcons from 'lucide-react';
import LazyVideoHero from '@/components/LazyVideoHero/LazyVideoHero';
export default function Home() { export default function Home() {
const { scrollY } = useScroll(); const { scrollY } = useScroll();
const heroOpacity = useTransform(scrollY, [0, 450], [1, 0]); const heroOpacity = useTransform(scrollY, [0, 450], [1, 0]);
const heroY = useTransform(scrollY, [0, 450], [0, -80]); const heroY = useTransform(scrollY, [0, 450], [0, -60]);
return ( return (
<div className="relative"> <div className="relative bg-[#FAFAFA] min-h-screen overflow-hidden">
{/* Sticky Background Video */} {/* ─── Hero Section with IntersectionObserver Lazy Video ─────────────────── */}
<div className="fixed inset-0 w-full h-full z-0 pointer-events-none"> <LazyVideoHero>
<video <section className="relative pt-20 pb-24 z-10">
autoPlay
muted
loop
playsInline
preload="auto"
className="w-full h-full object-cover"
style={{
filter: 'contrast(1.12) brightness(0.88) saturate(1.08)',
transform: 'translate3d(0, 0, 0)',
willChange: 'transform'
}}
>
<source src="/video/RIT Video.webm" type="video/webm" />
<source src="/video/RIT Video.mp4" type="video/mp4" />
</video>
{/* Dark Overlay (40-50%) */}
<div
className="absolute inset-0"
style={{ background: 'linear-gradient(to bottom, rgba(15, 23, 42, 0.4), rgba(15, 23, 42, 0.65))' }}
/>
</div>
{/* ─── Hero Section ─────────────────────────────────────────────────────── */}
<section className="relative overflow-hidden min-h-screen flex flex-col justify-center pt-24 pb-32 z-10 bg-transparent">
{/* Center Content */}
<motion.div <motion.div
className="container-custom relative z-20 flex flex-col items-center justify-center text-center px-4" className="container-custom relative z-20 flex flex-col items-center text-center px-4"
style={{ opacity: heroOpacity, y: heroY }} style={{ opacity: heroOpacity, y: heroY }}
> >
{/* Badge */}
<motion.div <motion.div
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="mb-3" className="mb-4 inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-[#FFF7ED] border border-[#FED7AA] text-[#F97316] text-xs font-semibold shadow-2xs"
style={{ fontFamily: 'Poppins, sans-serif' }}
> >
<span className="text-[#F97316] text-xs font-bold uppercase tracking-widest" style={{ fontFamily: 'Poppins, sans-serif' }}> <Sparkles className="w-3.5 h-3.5" />
OUR LEGACY & FUTURE <span>OFFICIAL STUDENT PORTAL</span>
</span>
</motion.div> </motion.div>
{/* Heading */}
<motion.h1 <motion.h1
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }} transition={{ duration: 0.6, delay: 0.1 }}
className="text-white text-3xl md:text-4xl font-light mb-2" className="text-white text-3xl md:text-4xl font-light mb-1"
style={{ fontFamily: 'Playfair Display, serif', color: '#FFFFFF', textShadow: '0 2px 10px rgba(0, 0, 0, 0.5)' }} style={{ fontFamily: 'Playfair Display, serif', textShadow: '0 2px 10px rgba(0,0,0,0.5)' }}
> >
Welcome to Welcome to
</motion.h1> </motion.h1>
@@ -76,8 +54,8 @@ export default function Home() {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.2 }} transition={{ duration: 0.7, delay: 0.2 }}
className="text-5xl md:text-7xl font-extrabold leading-tight mb-6 bg-gradient-to-r from-[#F97316] via-[#FB923C] to-[#F97316] bg-clip-text text-transparent" className="text-5xl md:text-7xl font-black leading-tight mb-6 bg-gradient-to-r from-[#F97316] via-[#FB923C] to-[#F97316] bg-clip-text text-transparent"
style={{ fontFamily: 'Playfair Display, serif', color: 'transparent', WebkitTextFillColor: 'transparent', filter: 'drop-shadow(0 4px 12px rgba(249, 115, 22, 0.45))' }} style={{ fontFamily: 'Playfair Display, serif', filter: 'drop-shadow(0 4px 12px rgba(249, 115, 22, 0.45))' }}
> >
RIT Freshers Hub RIT Freshers Hub
</motion.h2> </motion.h2>
@@ -86,24 +64,24 @@ export default function Home() {
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.3 }} transition={{ duration: 0.7, delay: 0.3 }}
className="max-w-2xl text-slate-200 text-sm md:text-base leading-relaxed mb-8" className="max-w-2xl text-slate-200 text-base md:text-lg leading-relaxed mb-8"
style={{ fontFamily: 'Inter, sans-serif' }} style={{ fontFamily: 'Inter, sans-serif' }}
> >
The RIT Freshers Hub is your centralized gateway to campus life, academic resources, AI assistance, clubs, events, bus routes, notes, and student services. Your centralized gateway to campus life at Rajalakshmi Institute of Technology academic notes, AI assistant, bus routes, faculty directory, and campus navigation.
</motion.p> </motion.p>
{/* Buttons */} {/* Call to Actions */}
<motion.div <motion.div
initial={{ opacity: 0, y: 15 }} initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.4 }} transition={{ duration: 0.7, delay: 0.4 }}
className="flex flex-wrap justify-center gap-4 mb-8" className="flex flex-wrap justify-center gap-4 mb-10"
> >
<a href="#built-for-freshers"> <a href="#built-for-freshers">
<motion.button <motion.button
whileHover={{ scale: 1.05, boxShadow: '0 8px 25px rgba(249,115,22,0.4)' }} whileHover={{ scale: 1.04, boxShadow: '0 10px 30px -5px rgba(249,115,22,0.4)' }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.96 }}
className="flex items-center gap-2 px-8 py-3.5 rounded-xl text-white font-semibold text-sm cursor-pointer" className="flex items-center gap-2 px-8 py-3.5 rounded-xl text-white font-semibold text-sm cursor-pointer shadow-md"
style={{ style={{
fontFamily: 'Poppins, sans-serif', fontFamily: 'Poppins, sans-serif',
background: 'linear-gradient(135deg, #F97316, #FB923C)', background: 'linear-gradient(135deg, #F97316, #FB923C)',
@@ -111,29 +89,29 @@ export default function Home() {
> >
<BookOpen className="w-4 h-4" /> <BookOpen className="w-4 h-4" />
Explore Hub Explore Hub
<ArrowRight className="w-3.5 h-3.5" /> <ArrowRight className="w-4 h-4" />
</motion.button> </motion.button>
</a> </a>
<Link to="/ai-assistant"> <Link to="/ai-assistant">
<motion.button <motion.button
whileHover={{ scale: 1.05, borderColor: '#F97316', backgroundColor: 'rgba(255,255,255,0.15)' }} whileHover={{ scale: 1.04, borderColor: '#F97316', backgroundColor: 'rgba(255,255,255,0.2)' }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.96 }}
className="flex items-center gap-2 px-8 py-3.5 rounded-xl font-semibold text-sm border-2 border-white/20 text-white bg-white/10 backdrop-blur-md transition-all cursor-pointer" className="flex items-center gap-2 px-8 py-3.5 rounded-xl font-semibold text-sm border-2 border-white/30 text-white bg-white/10 backdrop-blur-md transition-all cursor-pointer shadow-2xs"
style={{ fontFamily: 'Poppins, sans-serif' }} style={{ fontFamily: 'Poppins, sans-serif' }}
> >
<Bot className="w-4 h-4" /> <Bot className="w-4 h-4 text-[#F97316]" />
Ask Chatbot Ask Assistant
</motion.button> </motion.button>
</Link> </Link>
</motion.div> </motion.div>
{/* Badges */} {/* Trust Badges */}
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.5 }} transition={{ duration: 0.8, delay: 0.5 }}
className="flex flex-wrap justify-center gap-6 text-slate-300 border-t border-white/10 pt-6 max-w-lg w-full" className="flex flex-wrap justify-center gap-6 text-slate-300 border-t border-white/20 pt-6 max-w-lg w-full"
> >
{[ {[
{ icon: CheckCircle, text: 'Official Portal' }, { icon: CheckCircle, text: 'Official Portal' },
@@ -148,54 +126,37 @@ export default function Home() {
</motion.div> </motion.div>
</motion.div> </motion.div>
</section> </section>
</LazyVideoHero>
{/* Floating Glassmorphic Stats Section */} {/* Sleek Stats Card Section */}
<section className="relative z-30 -mt-16 px-4"> <section className="relative z-30 -mt-6 px-4">
<div className="container-custom max-w-5xl"> <div className="container-custom max-w-5xl">
<style>{`
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-8px); }
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
`}</style>
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }} transition={{ duration: 0.8, delay: 0.6 }}
className="animate-float rounded-[24px] border border-white/10 p-6 md:p-8 shadow-[0_25px_60px_-15px_rgba(0,0,0,0.4)]" className="rounded-3xl border border-[#E5E7EB] p-6 md:p-8 bg-white shadow-[0_20px_50px_-15px_rgba(0,0,0,0.06)]"
style={{
background: 'linear-gradient(135deg, rgba(11, 19, 43, 0.9) 0%, rgba(15, 23, 42, 0.8) 100%)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
}}
> >
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-6 text-center"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-6 text-center">
{[ {[
{ value: '20+', label: 'SPORTS', icon: Trophy }, { value: '20+', label: 'SPORTS', icon: Trophy },
{ value: '100+', label: 'FACULTY', icon: GraduationCap }, { value: '100+', label: 'FACULTY', icon: GraduationCap },
{ value: '18', label: 'CLUBS', icon: Award }, { value: '18', label: 'CLUBS', icon: Award },
{ value: '24/7', label: 'CHATBOT', icon: Cpu }, { value: '24/7', label: 'ASSISTANT', icon: Cpu },
{ value: '5000+', label: 'STUDENTS', icon: Users }, { value: '5000+', label: 'STUDENTS', icon: Users },
].map((stat, i) => ( ].map((stat, i) => (
<div key={i} className="flex flex-col items-center justify-center p-4"> <div key={i} className="flex flex-col items-center justify-center p-3">
{/* Icon */} <div className="w-10 h-10 rounded-xl bg-[#FFF7ED] flex items-center justify-center mb-2.5">
<div className="w-10 h-10 rounded-xl bg-[#F97316]/10 flex items-center justify-center mb-3">
<stat.icon className="w-5 h-5 text-[#F97316]" /> <stat.icon className="w-5 h-5 text-[#F97316]" />
</div> </div>
{/* Value */}
<div <div
className="text-2xl md:text-3xl font-extrabold text-white mb-1" className="text-2xl md:text-3xl font-black text-[#1E293B] mb-0.5"
style={{ fontFamily: 'Poppins, sans-serif' }} style={{ fontFamily: 'Poppins, sans-serif' }}
> >
{stat.value} {stat.value}
</div> </div>
{/* Label */}
<div <div
className="text-[10px] md:text-xs font-semibold tracking-wider text-slate-300 uppercase whitespace-nowrap" className="text-[11px] font-bold tracking-wider text-[#94A3B8] uppercase"
style={{ fontFamily: 'Inter, sans-serif' }} style={{ fontFamily: 'Inter, sans-serif' }}
> >
{stat.label} {stat.label}
@@ -208,7 +169,7 @@ export default function Home() {
</section> </section>
{/* ─── Features Section ─────────────────────────────────────────────────── */} {/* ─── Features Section ─────────────────────────────────────────────────── */}
<section id="built-for-freshers" className="section-padding relative z-20" style={{ backgroundColor: '#FAFAFA' }}> <section id="built-for-freshers" className="section-padding relative z-20">
<div className="container-custom"> <div className="container-custom">
<SectionTitle <SectionTitle
tag="Everything You Need" tag="Everything You Need"
@@ -217,7 +178,7 @@ export default function Home() {
subtitle="From notes to AI assistance — we've got everything you need to navigate campus life with confidence." subtitle="From notes to AI assistance — we've got everything you need to navigate campus life with confidence."
/> />
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5"> <StaggerContainer className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{FEATURES.map((feature) => ( {FEATURES.map((feature) => (
<StaggerItem key={feature.id}> <StaggerItem key={feature.id}>
<FeatureCard feature={feature} /> <FeatureCard feature={feature} />
@@ -227,61 +188,47 @@ export default function Home() {
</div> </div>
</section> </section>
{/* ─── Campus Quick Nav ─────────────────────────────────────────────────── */} {/* ─── Campus Quick Nav ─────────────────────────────────────────────────── */}
<section className="section-padding relative z-20" style={{ backgroundColor: '#FAFAFA' }}> <section className="section-padding relative z-20 bg-white border-y border-[#E5E7EB]">
<div className="container-custom"> <div className="container-custom">
<SectionTitle <SectionTitle
tag="Navigate Campus" tag="Navigate Campus"
title="Explore" title="Explore"
highlight="RIT Campus" highlight="RIT Campus"
subtitle="Find your way around campus — departments, labs, library, and all key locations." subtitle="Find your way around campus — departments, labs, library, and key locations."
/> />
<AnimatedContainer> <AnimatedContainer>
<div className="bg-white rounded-3xl border border-[#E5E7EB] overflow-hidden" style={{ boxShadow: '0 8px 40px -8px rgba(0,0,0,0.1)' }}> <div className="bg-white rounded-3xl border border-[#E5E7EB] overflow-hidden shadow-sm">
{/* Map placeholder */} {/* Map Teaser Header */}
<div <div
className="h-56 flex items-center justify-center relative overflow-hidden" className="h-52 flex items-center justify-center relative overflow-hidden"
style={{ background: 'linear-gradient(135deg, #1E293B, #334155)' }} style={{ background: 'linear-gradient(135deg, #1E293B, #0F172A)' }}
> >
<div <div
className="absolute inset-0 opacity-10" className="absolute inset-0 opacity-15"
style={{ style={{
backgroundImage: 'linear-gradient(rgba(249,115,22,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(249,115,22,0.5) 1px, transparent 1px)', backgroundImage: 'linear-gradient(rgba(249,115,22,0.4) 1px, transparent 1px), linear-gradient(90deg, rgba(249,115,22,0.4) 1px, transparent 1px)',
backgroundSize: '30px 30px', backgroundSize: '28px 28px',
}} }}
/> />
{/* Location pins */}
{[ <div className="relative z-10 text-center px-4">
{ label: 'CSE Block', x: '25%', y: '30%' }, <div className="w-12 h-12 rounded-2xl bg-[#F97316]/20 border border-[#F97316]/30 flex items-center justify-center mx-auto mb-3">
{ label: 'Library', x: '55%', y: '45%' }, <MapPin className="w-6 h-6 text-[#F97316]" />
{ label: 'Canteen', x: '70%', y: '65%' },
{ label: 'Hostel', x: '20%', y: '65%' },
].map((pin, i) => (
<motion.div
key={i}
className="absolute flex flex-col items-center gap-1 cursor-pointer group"
style={{ left: pin.x, top: pin.y, transform: 'translate(-50%, -50%)' }}
whileHover={{ scale: 1.1 }}
>
<div className="w-6 h-6 rounded-full flex items-center justify-center shadow-lg group-hover:scale-110 transition-all"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)' }}>
<div className="w-2 h-2 rounded-full bg-white" />
</div> </div>
<span className="text-[10px] text-white font-medium bg-black/50 px-1.5 py-0.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap"> <h3 className="text-white text-lg font-bold mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
{pin.label} Interactive Campus Map
</span> </h3>
</motion.div> <p className="text-slate-400 text-xs mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>
))} Locate all major campus blocks, labs, library & amenities.
<div className="relative z-10 text-center"> </p>
<div className="text-white/40 text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>Interactive Campus Map</div>
<Link to="/campus"> <Link to="/campus">
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
className="mt-2 px-5 py-2 rounded-xl text-white text-xs font-semibold" whileTap={{ scale: 0.95 }}
style={{ background: 'rgba(249,115,22,0.8)', fontFamily: 'Poppins, sans-serif' }} className="px-5 py-2 rounded-xl text-white text-xs font-semibold shadow-sm cursor-pointer"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', fontFamily: 'Poppins, sans-serif' }}
> >
Open Full Map Open Full Map
</motion.button> </motion.button>
@@ -289,20 +236,20 @@ export default function Home() {
</div> </div>
</div> </div>
{/* Quick nav buttons */} {/* Quick Nav Grid */}
<div className="p-5 grid grid-cols-3 sm:grid-cols-7 gap-3"> <div className="p-5 grid grid-cols-3 sm:grid-cols-7 gap-3 bg-[#FAFAFA]">
{CAMPUS_LOCATIONS.map((loc) => { {CAMPUS_LOCATIONS.map((loc) => {
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon]; const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
return ( return (
<Link to="/campus" key={loc.id}> <Link to="/campus" key={loc.id}>
<motion.div <motion.div
whileHover={{ y: -3, backgroundColor: '#FFF7ED' }} whileHover={{ y: -3, backgroundColor: '#FFF7ED' }}
className="flex flex-col items-center gap-2 p-3 rounded-xl cursor-pointer transition-all border border-transparent hover:border-[#FED7AA]" className="flex flex-col items-center gap-2 p-3 rounded-xl cursor-pointer transition-all border border-transparent hover:border-[#FED7AA] bg-white shadow-2xs"
> >
<div className="w-9 h-9 rounded-xl bg-[#F8FAFC] flex items-center justify-center"> <div className="w-9 h-9 rounded-xl bg-[#FFF7ED] flex items-center justify-center">
{Icon && <Icon className="w-4.5 h-4.5 text-[#F97316]" />} {Icon && <Icon className="w-4.5 h-4.5 text-[#F97316]" />}
</div> </div>
<span className="text-[10px] text-[#475569] text-center leading-tight" style={{ fontFamily: 'Inter, sans-serif' }}> <span className="text-[11px] text-[#475569] text-center font-medium leading-tight" style={{ fontFamily: 'Inter, sans-serif' }}>
{loc.name} {loc.name}
</span> </span>
</motion.div> </motion.div>
@@ -314,44 +261,6 @@ export default function Home() {
</AnimatedContainer> </AnimatedContainer>
</div> </div>
</section> </section>
{/* ─── Community Teaser ─────────────────────────────────────────────────── */}
<section className="section-padding bg-white relative z-20">
<div className="container-custom max-w-2xl">
{/* Q&A teaser */}
<AnimatedContainer>
<div className="rounded-3xl p-7 border border-[#E5E7EB]"
style={{ background: 'linear-gradient(135deg, #FAFAFA, #FFF7ED)', boxShadow: '0 4px 20px -4px rgba(0,0,0,0.07)' }}>
<div className="flex items-center gap-3 mb-4">
<div className="w-11 h-11 rounded-xl flex items-center justify-center bg-[#FFF7ED]">
<LucideIcons.MessageCircle className="w-5 h-5 text-[#F97316]" />
</div>
<div>
<h3 className="font-semibold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>Freshers Q&A</h3>
<p className="text-xs text-[#94A3B8]">Get answers from seniors</p>
</div>
</div>
<div className="space-y-3 mb-5">
{['How is hostel life at RIT?', 'Best clubs to join as a fresher?', 'Tips for first semester survival'].map((q, i) => (
<div key={i} className="flex items-start gap-2.5 p-3 bg-white rounded-xl border border-[#E5E7EB]">
<span className="text-[#F97316] text-xs font-bold mt-0.5">Q</span>
<span className="text-xs text-[#475569]" style={{ fontFamily: 'Inter, sans-serif' }}>{q}</span>
</div>
))}
</div>
<Link to="/community">
<motion.button
whileHover={{ scale: 1.02 }}
className="w-full py-2.5 rounded-xl text-sm font-semibold text-[#F97316] border-2 border-[#F97316] hover:bg-[#FFF7ED] transition-all"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
Join the Discussion
</motion.button>
</Link>
</div>
</AnimatedContainer>
</div>
</section>
</div> </div>
); );
} }

View File

@@ -811,31 +811,9 @@ async def on_message(message):
mention_nick_str = f"<@!{discord_client.user.id}>" mention_nick_str = f"<@!{discord_client.user.id}>"
content = content.replace(mention_str, "").replace(mention_nick_str, "").strip() content = content.replace(mention_str, "").replace(mention_nick_str, "").strip()
# Discord /remove Command # Discord /collab & /remove commands disabled as requested
if content.lower().startswith("/remove") or content.lower().startswith("/cancel"): if content.lower().startswith("/collab") or content.lower().startswith("/remove") or content.lower().startswith("/cancel"):
try: await message.reply(" **Dev Hub collaboration requests via Discord are currently disabled.** Please use the RIT Dev Hub website (https://rit-services.in/dev-collab) or the Telegram bot (@Ritchatbot_bot).")
res = requests.get(f"{BACKEND_URL}/api/collab/active/discord/{message.author.id}", timeout=5)
if res.status_code == 200:
requests_list = res.json()
if not requests_list:
await message.reply(" **You have no active collaboration requests to remove.**")
else:
view = DiscordCollabRemoveView(requests_list)
await message.reply("🗑️ **Your Active Collaboration Requests**\nSelect a request from the dropdown below to cancel it:", view=view)
else:
await message.reply("❌ Failed to fetch active requests.")
except Exception as e:
await message.reply(f"❌ Error: {e}")
return
# Discord Interactive /collab command
if content.lower().startswith("/collab"):
view = CollabView()
await message.reply(
"🚀 **Post a Collaboration Request to RIT Dev Hub!**\n"
"Please select a tag from the scroll-down dropdown menu below to open the submission form:",
view=view
)
return return
# Check if this message is a reply to a question DM sent to a helper # Check if this message is a reply to a question DM sent to a helper
@@ -1003,12 +981,8 @@ def send_collab_application(payload: CollabApplicationPayload):
telegram_sent = False telegram_sent = False
discord_sent = False discord_sent = False
# 1. Telegram Broadcast via 24/7 Chatbot & Collab Bot # 1. Telegram Notification strictly to Person A (the project author)
chat_id = payload.telegram_chat_id chat_id = payload.telegram_chat_id
if not chat_id:
helpers = current_config.get("helper_chat_ids", [])
if helpers:
chat_id = helpers[0]
if chat_id: if chat_id:
msg_text = ( msg_text = (
@@ -1031,36 +1005,11 @@ def send_collab_application(payload: CollabApplicationPayload):
res = send_telegram_message(chat_id, msg_text, reply_markup=reply_markup, token=bot_token) res = send_telegram_message(chat_id, msg_text, reply_markup=reply_markup, token=bot_token)
if res.get("ok"): if res.get("ok"):
telegram_sent = True telegram_sent = True
else:
logging.info(f"Skipping Telegram notification for collab application {payload.application_id}: No telegram_chat_id provided for author.")
# 2. Discord Broadcast # 2. Discord Broadcast - Disabled as requested
discord_target_users = [] discord_sent = False
if payload.discord_user_id:
discord_target_users.append(payload.discord_user_id)
config_discord_helpers = current_config.get("discord_helper_user_ids", [])
for dh in config_discord_helpers:
if str(dh) not in [str(x) for x in discord_target_users]:
discord_target_users.append(dh)
if discord_target_users and DISCORD_TOKEN and discord_loop:
try:
asyncio.run_coroutine_threadsafe(
broadcast_discord_collab_application(
payload.application_id,
payload.project_idea,
payload.tag,
payload.applicant_name,
payload.applicant_dept,
payload.applicant_year,
payload.applicant_contact,
payload.message or "",
discord_target_users
),
discord_loop
)
discord_sent = True
except Exception as e:
logging.error(f"Error scheduling Discord collab broadcast: {e}")
return { return {
"status": "success", "status": "success",