Compare commits
10 Commits
01a3df2386
...
8433fe33b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8433fe33b2 | ||
|
|
2a86af0113 | ||
|
|
1852239452 | ||
|
|
b461c6cf32 | ||
|
|
68902caaa4 | ||
|
|
6229128dd2 | ||
|
|
7067f7cbad | ||
|
|
674eb437b4 | ||
|
|
5676bb0950 | ||
|
|
add0fbb47c |
223
VPS_MASTER_GUIDE.md
Normal file
223
VPS_MASTER_GUIDE.md
Normal 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.
|
||||
Binary file not shown.
BIN
public/video/whatsapp-video.mp4
Normal file
BIN
public/video/whatsapp-video.mp4
Normal file
Binary file not shown.
@@ -2,7 +2,7 @@ package com.rit.driver;
|
||||
|
||||
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";
|
||||
|
||||
@@ -36,9 +36,10 @@ public class NetworkHelper {
|
||||
conn.setConnectTimeout(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(
|
||||
"{\"latitude\": %f, \"longitude\": %f, \"pin\": \"%s\"}",
|
||||
java.util.Locale.US,
|
||||
"{\"latitude\": %.6f, \"longitude\": %.6f, \"pin\": \"%s\"}",
|
||||
lat, lng, pin
|
||||
);
|
||||
|
||||
|
||||
@@ -97,6 +97,35 @@ public class TrackingService extends Service {
|
||||
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")
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
@@ -137,39 +166,22 @@ public class TrackingService extends Service {
|
||||
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
try {
|
||||
if (locationManager != null) {
|
||||
// Request GPS updates
|
||||
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
5000,
|
||||
0.0f,
|
||||
locationListener
|
||||
);
|
||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 3000, 0.0f, locationListener);
|
||||
}
|
||||
// Request Network/Wi-Fi location updates (essential indoors)
|
||||
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.NETWORK_PROVIDER,
|
||||
5000,
|
||||
0.0f,
|
||||
locationListener
|
||||
);
|
||||
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 3000, 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) {
|
||||
Log.e(TAG, "SecurityException: Location permissions not granted", e);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
// 4. Start periodic 5-second uploader
|
||||
handler.removeCallbacks(periodicUploader);
|
||||
handler.post(periodicUploader);
|
||||
|
||||
return START_REDELIVER_INTENT;
|
||||
}
|
||||
|
||||
@@ -177,6 +189,11 @@ public class TrackingService extends Service {
|
||||
public void onDestroy() {
|
||||
Log.d(TAG, "onDestroy: Stopping service cleanups");
|
||||
|
||||
// Stop periodic uploader timer
|
||||
if (handler != null && periodicUploader != null) {
|
||||
handler.removeCallbacks(periodicUploader);
|
||||
}
|
||||
|
||||
// 1. Remove GPS Listener
|
||||
if (locationManager != null) {
|
||||
locationManager.removeUpdates(locationListener);
|
||||
|
||||
@@ -187,7 +187,8 @@ export default function BusRouteMap({ selectedRoute, allRoutes }: BusRouteMapPro
|
||||
/>
|
||||
|
||||
{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);
|
||||
|
||||
|
||||
84
src/components/LazyVideoHero/LazyVideoHero.tsx
Normal file
84
src/components/LazyVideoHero/LazyVideoHero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
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 type { ChatMessage } from '@/types';
|
||||
import { getChatbotUrl } from '@/lib/utils';
|
||||
@@ -9,7 +9,7 @@ const INITIAL_MESSAGES: ChatMessage[] = [
|
||||
{
|
||||
id: '1',
|
||||
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(),
|
||||
},
|
||||
];
|
||||
@@ -42,7 +42,8 @@ export default function AIAssistant() {
|
||||
};
|
||||
setMessages((prev) => [...prev, userMsg]);
|
||||
setInput('');
|
||||
setIsTyping(true); // Call the Go chatbot microservice
|
||||
setIsTyping(true);
|
||||
|
||||
let responseText = '';
|
||||
try {
|
||||
const res = await fetch(getChatbotUrl('/api/chat'), {
|
||||
@@ -73,7 +74,7 @@ export default function AIAssistant() {
|
||||
} else if (lower.includes('library')) {
|
||||
responseText = responses.library;
|
||||
} 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 (
|
||||
<div className="min-h-screen flex flex-col" style={{ backgroundColor: '#FAFAFA' }}>
|
||||
<div className="min-h-screen flex flex-col bg-[#FAFAFA]">
|
||||
{/* 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="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)' }}>
|
||||
<Bot className="w-5 h-5 text-white" />
|
||||
<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.5 h-5.5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-semibold text-[#1E293B] text-sm" style={{ fontFamily: 'Poppins, sans-serif' }}>RIT Chatbot</h1>
|
||||
<div className="flex items-center gap-1.5 text-xs text-[#94A3B8]">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
||||
Campus Knowledge Enabled
|
||||
<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-[#64748B]">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||
Campus Knowledge Engine Online
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<a
|
||||
href="https://t.me/Ritchatbot_bot"
|
||||
target="_blank"
|
||||
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={{
|
||||
background: 'linear-gradient(135deg, #0088cc, #24A1DE)',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
}}
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
Use on Telegram
|
||||
<span className="hidden sm:inline">Telegram Bot</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.com/api/oauth2/authorize?client_id=1476942599167414506&permissions=8&scope=bot"
|
||||
target="_blank"
|
||||
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={{
|
||||
background: 'linear-gradient(135deg, #5865F2, #7289DA)',
|
||||
fontFamily: 'Inter, sans-serif'
|
||||
}}
|
||||
>
|
||||
<Bot className="w-3.5 h-3.5" />
|
||||
Use on Discord
|
||||
<span className="hidden sm:inline">Discord Bot</span>
|
||||
</a>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
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" />
|
||||
New Chat
|
||||
<span>Reset</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="flex flex-wrap gap-2"
|
||||
>
|
||||
{[
|
||||
{ icon: Sparkles, label: 'Gemini AI' },
|
||||
{ icon: Bot, label: 'RAG Enabled' },
|
||||
{ icon: ChevronRight, label: 'Campus Knowledge' },
|
||||
{ icon: Zap, label: 'Instant Responses' },
|
||||
{ icon: ShieldCheck, label: 'Verified Campus Info' },
|
||||
{ icon: MessageSquare, label: '24/7 Assistance' },
|
||||
].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.label}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Messages */}
|
||||
{/* Chat Container */}
|
||||
<div
|
||||
className="flex-1 bg-white rounded-3xl border border-[#E5E7EB] 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)' }}
|
||||
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: '420px', maxHeight: '65vh', boxShadow: '0 8px 30px -6px rgba(0,0,0,0.04)' }}
|
||||
>
|
||||
{messages.map((msg) => (
|
||||
<motion.div
|
||||
key={msg.id}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
|
||||
>
|
||||
{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" />
|
||||
</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={{
|
||||
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',
|
||||
borderRadius: msg.role === 'user' ? '20px 20px 4px 20px' : '20px 20px 20px 4px',
|
||||
border: msg.role === 'assistant' ? '1px solid #E5E7EB' : 'none',
|
||||
@@ -207,7 +208,7 @@ export default function AIAssistant() {
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{/* Typing */}
|
||||
{/* Typing Indicator */}
|
||||
<AnimatePresence>
|
||||
{isTyping && (
|
||||
<motion.div
|
||||
@@ -216,16 +217,16 @@ export default function AIAssistant() {
|
||||
exit={{ opacity: 0 }}
|
||||
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" />
|
||||
</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) => (
|
||||
<motion.span
|
||||
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] }}
|
||||
transition={{ duration: 0.8, repeat: Infinity, delay: i * 0.15 }}
|
||||
transition={{ duration: 0.6, repeat: Infinity, delay: i * 0.15 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -240,10 +241,10 @@ export default function AIAssistant() {
|
||||
{AI_SUGGESTED_PROMPTS.slice(0, 4).map((prompt, i) => (
|
||||
<motion.button
|
||||
key={i}
|
||||
whileHover={{ scale: 1.02, backgroundColor: '#FFF7ED' }}
|
||||
whileHover={{ scale: 1.02, backgroundColor: '#FFF7ED', borderColor: '#FED7AA' }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
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' }}
|
||||
>
|
||||
{prompt}
|
||||
@@ -251,16 +252,16 @@ export default function AIAssistant() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<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)' }}>
|
||||
{/* Input Field */}
|
||||
<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
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask anything about RIT..."
|
||||
className="flex-1 text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none bg-transparent"
|
||||
placeholder="Ask anything about RIT campus, routes, hostel, or exams..."
|
||||
className="flex-1 text-sm text-[#1E293B] placeholder-[#94A3B8] focus:outline-none bg-transparent pl-2"
|
||||
style={{ fontFamily: 'Inter, sans-serif' }}
|
||||
/>
|
||||
{input && (
|
||||
@@ -274,22 +275,20 @@ export default function AIAssistant() {
|
||||
<X className="w-4 h-4" />
|
||||
</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
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => sendMessage(input)}
|
||||
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)' }}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</motion.button>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,73 +1,51 @@
|
||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ArrowRight, Sparkles, BookOpen, Bot, GraduationCap,
|
||||
CheckCircle, Zap, Shield, Brain, Trophy, Award, Cpu, Users
|
||||
ArrowRight, BookOpen, Bot, GraduationCap,
|
||||
CheckCircle, Zap, Shield, Trophy, Award, Cpu, Users, MapPin, Sparkles
|
||||
} from 'lucide-react';
|
||||
import SectionTitle from '@/components/SectionTitle/SectionTitle';
|
||||
import FeatureCard from '@/components/FeatureCard/FeatureCard';
|
||||
import FloatingCard from '@/components/FloatingCard/FloatingCard';
|
||||
import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer';
|
||||
import AnimatedContainer from '@/components/AnimatedContainer/AnimatedContainer';
|
||||
import { FEATURES, CAMPUS_LOCATIONS } from '@/constants';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
|
||||
import LazyVideoHero from '@/components/LazyVideoHero/LazyVideoHero';
|
||||
|
||||
export default function Home() {
|
||||
const { scrollY } = useScroll();
|
||||
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 (
|
||||
<div className="relative">
|
||||
{/* Sticky Background Video */}
|
||||
<div className="fixed inset-0 w-full h-full z-0 pointer-events-none">
|
||||
<video
|
||||
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 */}
|
||||
<div className="relative bg-[#FAFAFA] min-h-screen overflow-hidden">
|
||||
{/* ─── Hero Section with IntersectionObserver Lazy Video ─────────────────── */}
|
||||
<LazyVideoHero>
|
||||
<section className="relative pt-20 pb-24 z-10">
|
||||
<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 }}
|
||||
>
|
||||
{/* Badge */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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' }}>
|
||||
OUR LEGACY & FUTURE
|
||||
</span>
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
<span>OFFICIAL STUDENT PORTAL</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Heading */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="text-white text-3xl md:text-4xl font-light mb-2"
|
||||
style={{ fontFamily: 'Playfair Display, serif', color: '#FFFFFF', textShadow: '0 2px 10px rgba(0, 0, 0, 0.5)' }}
|
||||
className="text-white text-3xl md:text-4xl font-light mb-1"
|
||||
style={{ fontFamily: 'Playfair Display, serif', textShadow: '0 2px 10px rgba(0,0,0,0.5)' }}
|
||||
>
|
||||
Welcome to
|
||||
</motion.h1>
|
||||
@@ -76,8 +54,8 @@ export default function Home() {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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"
|
||||
style={{ fontFamily: 'Playfair Display, serif', color: 'transparent', WebkitTextFillColor: 'transparent', filter: 'drop-shadow(0 4px 12px rgba(249, 115, 22, 0.45))' }}
|
||||
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', filter: 'drop-shadow(0 4px 12px rgba(249, 115, 22, 0.45))' }}
|
||||
>
|
||||
RIT Freshers Hub
|
||||
</motion.h2>
|
||||
@@ -86,24 +64,24 @@ export default function Home() {
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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' }}
|
||||
>
|
||||
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>
|
||||
|
||||
{/* Buttons */}
|
||||
{/* Call to Actions */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05, boxShadow: '0 8px 25px rgba(249,115,22,0.4)' }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="flex items-center gap-2 px-8 py-3.5 rounded-xl text-white font-semibold text-sm cursor-pointer"
|
||||
whileHover={{ scale: 1.04, boxShadow: '0 10px 30px -5px rgba(249,115,22,0.4)' }}
|
||||
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 shadow-md"
|
||||
style={{
|
||||
fontFamily: 'Poppins, sans-serif',
|
||||
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
||||
@@ -111,29 +89,29 @@ export default function Home() {
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Explore Hub
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</motion.button>
|
||||
</a>
|
||||
|
||||
<Link to="/ai-assistant">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05, borderColor: '#F97316', backgroundColor: 'rgba(255,255,255,0.15)' }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
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"
|
||||
whileHover={{ scale: 1.04, borderColor: '#F97316', backgroundColor: 'rgba(255,255,255,0.2)' }}
|
||||
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/30 text-white bg-white/10 backdrop-blur-md transition-all cursor-pointer shadow-2xs"
|
||||
style={{ fontFamily: 'Poppins, sans-serif' }}
|
||||
>
|
||||
<Bot className="w-4 h-4" />
|
||||
Ask Chatbot
|
||||
<Bot className="w-4 h-4 text-[#F97316]" />
|
||||
Ask Assistant
|
||||
</motion.button>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Badges */}
|
||||
{/* Trust Badges */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
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' },
|
||||
@@ -148,54 +126,37 @@ export default function Home() {
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
</LazyVideoHero>
|
||||
|
||||
{/* Floating Glassmorphic Stats Section */}
|
||||
<section className="relative z-30 -mt-16 px-4">
|
||||
{/* Sleek Stats Card Section */}
|
||||
<section className="relative z-30 -mt-6 px-4">
|
||||
<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
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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)]"
|
||||
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)',
|
||||
}}
|
||||
className="rounded-3xl border border-[#E5E7EB] p-6 md:p-8 bg-white shadow-[0_20px_50px_-15px_rgba(0,0,0,0.06)]"
|
||||
>
|
||||
<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: '100+', label: 'FACULTY', icon: GraduationCap },
|
||||
{ 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 },
|
||||
].map((stat, i) => (
|
||||
<div key={i} className="flex flex-col items-center justify-center p-4">
|
||||
{/* Icon */}
|
||||
<div className="w-10 h-10 rounded-xl bg-[#F97316]/10 flex items-center justify-center mb-3">
|
||||
<div key={i} className="flex flex-col items-center justify-center p-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-[#FFF7ED] flex items-center justify-center mb-2.5">
|
||||
<stat.icon className="w-5 h-5 text-[#F97316]" />
|
||||
</div>
|
||||
{/* Value */}
|
||||
<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' }}
|
||||
>
|
||||
{stat.value}
|
||||
</div>
|
||||
{/* Label */}
|
||||
<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' }}
|
||||
>
|
||||
{stat.label}
|
||||
@@ -208,7 +169,7 @@ export default function Home() {
|
||||
</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">
|
||||
<SectionTitle
|
||||
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."
|
||||
/>
|
||||
|
||||
<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) => (
|
||||
<StaggerItem key={feature.id}>
|
||||
<FeatureCard feature={feature} />
|
||||
@@ -227,61 +188,47 @@ export default function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* ─── 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">
|
||||
<SectionTitle
|
||||
tag="Navigate Campus"
|
||||
title="Explore"
|
||||
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>
|
||||
<div className="bg-white rounded-3xl border border-[#E5E7EB] overflow-hidden" style={{ boxShadow: '0 8px 40px -8px rgba(0,0,0,0.1)' }}>
|
||||
{/* Map placeholder */}
|
||||
<div className="bg-white rounded-3xl border border-[#E5E7EB] overflow-hidden shadow-sm">
|
||||
{/* Map Teaser Header */}
|
||||
<div
|
||||
className="h-56 flex items-center justify-center relative overflow-hidden"
|
||||
style={{ background: 'linear-gradient(135deg, #1E293B, #334155)' }}
|
||||
className="h-52 flex items-center justify-center relative overflow-hidden"
|
||||
style={{ background: 'linear-gradient(135deg, #1E293B, #0F172A)' }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 opacity-10"
|
||||
className="absolute inset-0 opacity-15"
|
||||
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)',
|
||||
backgroundSize: '30px 30px',
|
||||
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: '28px 28px',
|
||||
}}
|
||||
/>
|
||||
{/* Location pins */}
|
||||
{[
|
||||
{ label: 'CSE Block', x: '25%', y: '30%' },
|
||||
{ label: 'Library', x: '55%', y: '45%' },
|
||||
{ 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 className="relative z-10 text-center px-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-[#F97316]/20 border border-[#F97316]/30 flex items-center justify-center mx-auto mb-3">
|
||||
<MapPin className="w-6 h-6 text-[#F97316]" />
|
||||
</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">
|
||||
{pin.label}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
<div className="relative z-10 text-center">
|
||||
<div className="text-white/40 text-sm" style={{ fontFamily: 'Inter, sans-serif' }}>Interactive Campus Map</div>
|
||||
<h3 className="text-white text-lg font-bold mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
|
||||
Interactive Campus Map
|
||||
</h3>
|
||||
<p className="text-slate-400 text-xs mb-3" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
Locate all major campus blocks, labs, library & amenities.
|
||||
</p>
|
||||
<Link to="/campus">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="mt-2 px-5 py-2 rounded-xl text-white text-xs font-semibold"
|
||||
style={{ background: 'rgba(249,115,22,0.8)', fontFamily: 'Poppins, sans-serif' }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
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
|
||||
</motion.button>
|
||||
@@ -289,20 +236,20 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick nav buttons */}
|
||||
<div className="p-5 grid grid-cols-3 sm:grid-cols-7 gap-3">
|
||||
{/* Quick Nav Grid */}
|
||||
<div className="p-5 grid grid-cols-3 sm:grid-cols-7 gap-3 bg-[#FAFAFA]">
|
||||
{CAMPUS_LOCATIONS.map((loc) => {
|
||||
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<{ className?: string }>>)[loc.icon];
|
||||
return (
|
||||
<Link to="/campus" key={loc.id}>
|
||||
<motion.div
|
||||
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]" />}
|
||||
</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}
|
||||
</span>
|
||||
</motion.div>
|
||||
@@ -314,44 +261,6 @@ export default function Home() {
|
||||
</AnimatedContainer>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -811,31 +811,9 @@ async def on_message(message):
|
||||
mention_nick_str = f"<@!{discord_client.user.id}>"
|
||||
content = content.replace(mention_str, "").replace(mention_nick_str, "").strip()
|
||||
|
||||
# Discord /remove Command
|
||||
if content.lower().startswith("/remove") or content.lower().startswith("/cancel"):
|
||||
try:
|
||||
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
|
||||
)
|
||||
# Discord /collab & /remove commands disabled as requested
|
||||
if content.lower().startswith("/collab") or content.lower().startswith("/remove") or content.lower().startswith("/cancel"):
|
||||
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).")
|
||||
return
|
||||
|
||||
# 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
|
||||
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
|
||||
if not chat_id:
|
||||
helpers = current_config.get("helper_chat_ids", [])
|
||||
if helpers:
|
||||
chat_id = helpers[0]
|
||||
|
||||
if chat_id:
|
||||
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)
|
||||
if res.get("ok"):
|
||||
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
|
||||
discord_target_users = []
|
||||
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}")
|
||||
# 2. Discord Broadcast - Disabled as requested
|
||||
discord_sent = False
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
|
||||
Reference in New Issue
Block a user