diff --git a/app/src/components/CommentBox.tsx b/app/src/components/CommentBox.tsx
index 7cfaeee..3504b96 100644
--- a/app/src/components/CommentBox.tsx
+++ b/app/src/components/CommentBox.tsx
@@ -1,4 +1,5 @@
import { Clock, Pencil, Trash2 } from 'lucide-react';
+import { Loader } from './Loader';
interface CommentBoxProps {
comment: {
@@ -171,7 +172,7 @@ export function CommentBox({
disabled={isBusy || !editingText.trim()}
className="bg-zinc-900 text-white hover:bg-zinc-800 font-semibold text-xs px-4.5 py-1.5 rounded-lg transition disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer flex items-center gap-1.5"
>
- {isBusy && }
+ {isBusy && }
Save
diff --git a/app/src/components/Loader.tsx b/app/src/components/Loader.tsx
new file mode 100644
index 0000000..e21ff05
--- /dev/null
+++ b/app/src/components/Loader.tsx
@@ -0,0 +1,30 @@
+interface LoaderProps {
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+ color?: 'white' | 'dark' | 'orange' | 'rose';
+ className?: string;
+}
+
+export function Loader({ size = 'md', color = 'dark', className = '' }: LoaderProps) {
+ const sizeClasses = {
+ xs: 'w-3 h-3 border-2',
+ sm: 'w-4 h-4 border-2',
+ md: 'w-8 h-8 border-[3px]',
+ lg: 'w-12 h-12 border-4',
+ xl: 'w-16 h-16 border-4',
+ };
+
+ const colorClasses = {
+ white: 'border-white/20 border-t-white',
+ dark: 'border-zinc-200 border-t-zinc-800',
+ orange: 'border-amber-100 border-t-amber-500',
+ rose: 'border-rose-100 border-t-rose-500',
+ };
+
+ return (
+
+ );
+}
diff --git a/app/src/pages/admin/AEPostView.tsx b/app/src/pages/admin/AEPostView.tsx
index 774b4e2..008d9a4 100644
--- a/app/src/pages/admin/AEPostView.tsx
+++ b/app/src/pages/admin/AEPostView.tsx
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { AlertCircle, ServerCrash, ClipboardList, GraduationCap, BedDouble, Building2, Zap, Hammer } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
// ── Types ─────────────────────────────────────────────────────────────────────
@@ -200,7 +201,7 @@ export function AEPostView() {
diff --git a/app/src/pages/admin/AdminPostView.tsx b/app/src/pages/admin/AdminPostView.tsx
index fd990fd..c33f522 100644
--- a/app/src/pages/admin/AdminPostView.tsx
+++ b/app/src/pages/admin/AdminPostView.tsx
@@ -23,6 +23,7 @@ import {
} from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
import { CommentBox } from '../../components/CommentBox';
+import { Loader } from '../../components/Loader';
// ── Types ──────────────────────────────────────────────────────────────────────
@@ -603,7 +604,7 @@ export function AdminPostView() {
@@ -921,7 +922,7 @@ export function AdminPostView() {
className="inline-flex items-center gap-2 text-xs font-semibold text-white bg-zinc-900 hover:bg-zinc-800 px-4 py-2 rounded-lg transition-all duration-200 disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none cursor-pointer"
>
{acting ? (
-
+
) : }
Post Comment
@@ -950,7 +951,7 @@ export function AdminPostView() {
className="inline-flex items-center gap-2 text-xs font-semibold text-white bg-amber-500 hover:bg-amber-600 px-4 py-2 rounded-lg transition-all duration-200 disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none cursor-pointer"
>
{acting ? (
-
+
) : btn.icon}
{btn.label}
@@ -1000,7 +1001,7 @@ export function AdminPostView() {
className={`inline-flex items-center gap-2 text-xs font-semibold text-white ${buttonColorClass} px-4 py-2 rounded-lg transition-all duration-200 disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none cursor-pointer`}
>
{acting ? (
-
+
) : btn.icon}
{btn.label}
diff --git a/app/src/pages/admin/JEPostView.tsx b/app/src/pages/admin/JEPostView.tsx
index 06b84e8..1e1c155 100644
--- a/app/src/pages/admin/JEPostView.tsx
+++ b/app/src/pages/admin/JEPostView.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { AlertCircle, ServerCrash, ClipboardList, GraduationCap, BedDouble, Building2, Zap, Hammer } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
// ── Types ─────────────────────────────────────────────────────────────────────
@@ -185,7 +186,7 @@ export function JEPostView() {
diff --git a/app/src/pages/admin/XENPostView.tsx b/app/src/pages/admin/XENPostView.tsx
index 0c93059..e786a83 100644
--- a/app/src/pages/admin/XENPostView.tsx
+++ b/app/src/pages/admin/XENPostView.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { AlertCircle, ServerCrash, ClipboardList, GraduationCap, BedDouble, Building2, Zap, Hammer } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
// ── Types ─────────────────────────────────────────────────────────────────────
@@ -187,7 +188,7 @@ export function XENPostView() {
diff --git a/app/src/pages/auth/AccountResetPass.tsx b/app/src/pages/auth/AccountResetPass.tsx
index dbbf88e..4ff1d07 100644
--- a/app/src/pages/auth/AccountResetPass.tsx
+++ b/app/src/pages/auth/AccountResetPass.tsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
import { Eye, EyeOff } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
const roleToApi: Record = {
faculty: '/api/auth/faculty/reset-password',
@@ -203,12 +204,7 @@ export function AccountResetPass() {
disabled={loading}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Resetting…' : 'Reset Password'}
{loginPath && (
diff --git a/app/src/pages/auth/CentreHeadForgotPassword.tsx b/app/src/pages/auth/CentreHeadForgotPassword.tsx
index 0bdd1f8..fd08ee4 100644
--- a/app/src/pages/auth/CentreHeadForgotPassword.tsx
+++ b/app/src/pages/auth/CentreHeadForgotPassword.tsx
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
export function CentreHeadForgotPassword() {
const [email, setEmail] = useState('');
@@ -94,12 +95,7 @@ export function CentreHeadForgotPassword() {
disabled={loading || submitted}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${(loading || submitted) ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Sending…' : submitted ? 'Link Sent' : 'Send Reset Link'}
diff --git a/app/src/pages/auth/CentreHeadLogin.tsx b/app/src/pages/auth/CentreHeadLogin.tsx
index cc2c6fe..9dc7936 100644
--- a/app/src/pages/auth/CentreHeadLogin.tsx
+++ b/app/src/pages/auth/CentreHeadLogin.tsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Eye, EyeOff } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
export function CentreHeadLogin() {
const [email, setEmail] = useState('');
@@ -134,12 +135,7 @@ export function CentreHeadLogin() {
disabled={loading}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Logging in…' : 'Login'}
diff --git a/app/src/pages/auth/CentreHeadSignup.tsx b/app/src/pages/auth/CentreHeadSignup.tsx
index e8b8737..74a6f79 100644
--- a/app/src/pages/auth/CentreHeadSignup.tsx
+++ b/app/src/pages/auth/CentreHeadSignup.tsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Eye, EyeOff } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
import { BUILDINGS } from '../../constants/models';
export function CentreHeadSignup() {
@@ -191,12 +192,7 @@ export function CentreHeadSignup() {
disabled={loading}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Registering…' : 'Register as Centre Head'}
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Sending…' : submitted ? 'Link Sent' : 'Send Reset Link'}
diff --git a/app/src/pages/auth/FacultyLogin.tsx b/app/src/pages/auth/FacultyLogin.tsx
index 122a9a4..c68e0d1 100644
--- a/app/src/pages/auth/FacultyLogin.tsx
+++ b/app/src/pages/auth/FacultyLogin.tsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Eye, EyeOff } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
export function FacultyLogin() {
const [email, setEmail] = useState('');
@@ -134,12 +135,7 @@ export function FacultyLogin() {
disabled={loading}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Logging in…' : 'Login'}
diff --git a/app/src/pages/auth/FacultySignup.tsx b/app/src/pages/auth/FacultySignup.tsx
index 318a930..45f54f8 100644
--- a/app/src/pages/auth/FacultySignup.tsx
+++ b/app/src/pages/auth/FacultySignup.tsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Eye, EyeOff } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
import { DEPARTMENTS, BLOCK_LABELS, BLOCK_TYPES } from '../../constants/models';
export function FacultySignup() {
@@ -251,12 +252,7 @@ export function FacultySignup() {
disabled={loading}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Registering…' : 'Register as Faculty'}
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Logging in…' : 'Login to Portal'}
diff --git a/app/src/pages/auth/VerifyAccount.tsx b/app/src/pages/auth/VerifyAccount.tsx
index f0d5460..5a525b4 100644
--- a/app/src/pages/auth/VerifyAccount.tsx
+++ b/app/src/pages/auth/VerifyAccount.tsx
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
type VerifyStatus = 'loading' | 'success' | 'error' | 'no-token';
@@ -91,7 +92,7 @@ export function VerifyAccount() {
{/* Loading */}
{status === 'loading' && (
-
+
Verifying your account, please wait…
)}
diff --git a/app/src/pages/auth/WardenForgotPassword.tsx b/app/src/pages/auth/WardenForgotPassword.tsx
index 825ac5f..226b8c2 100644
--- a/app/src/pages/auth/WardenForgotPassword.tsx
+++ b/app/src/pages/auth/WardenForgotPassword.tsx
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
export function WardenForgotPassword() {
const [email, setEmail] = useState('');
@@ -94,12 +95,7 @@ export function WardenForgotPassword() {
disabled={loading || submitted}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${(loading || submitted) ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Sending…' : submitted ? 'Link Sent' : 'Send Reset Link'}
diff --git a/app/src/pages/auth/WardenLogin.tsx b/app/src/pages/auth/WardenLogin.tsx
index dd7626d..b084622 100644
--- a/app/src/pages/auth/WardenLogin.tsx
+++ b/app/src/pages/auth/WardenLogin.tsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Eye, EyeOff } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
export function WardenLogin() {
const [email, setEmail] = useState('');
@@ -134,12 +135,7 @@ export function WardenLogin() {
disabled={loading}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Logging in…' : 'Login'}
diff --git a/app/src/pages/auth/WardenSignup.tsx b/app/src/pages/auth/WardenSignup.tsx
index 36dc736..f969314 100644
--- a/app/src/pages/auth/WardenSignup.tsx
+++ b/app/src/pages/auth/WardenSignup.tsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Eye, EyeOff } from 'lucide-react';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
import { HOSTELS } from '../../constants/models';
export function WardenSignup() {
@@ -191,12 +192,7 @@ export function WardenSignup() {
disabled={loading}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Registering…' : 'Register as Warden'}
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Submitting…' : 'Submit Complaint'}
diff --git a/app/src/pages/post/FacultyPost.tsx b/app/src/pages/post/FacultyPost.tsx
index 3349183..cee551c 100644
--- a/app/src/pages/post/FacultyPost.tsx
+++ b/app/src/pages/post/FacultyPost.tsx
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
import { POST_PLACES, POST_TYPES } from '../../constants/models';
export function FacultyPost() {
@@ -161,12 +162,7 @@ export function FacultyPost() {
disabled={loading}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Submitting…' : 'Submit Complaint'}
diff --git a/app/src/pages/post/PostView.tsx b/app/src/pages/post/PostView.tsx
index 32b5006..7b8f4ec 100644
--- a/app/src/pages/post/PostView.tsx
+++ b/app/src/pages/post/PostView.tsx
@@ -8,6 +8,7 @@ import {
import { MainLayout } from '../../components/layout/MainLayout';
import { POST_PLACES } from '../../constants/models';
import { CommentBox } from '../../components/CommentBox';
+import { Loader } from '../../components/Loader';
type Role = 'faculty' | 'warden' | 'centrehead';
@@ -159,7 +160,7 @@ export function PostView() {
-
+
Loading complaint details…
@@ -313,7 +314,7 @@ export function PostView() {
disabled={isBusy}
className="inline-flex items-center gap-1 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-xs font-semibold text-white rounded-lg transition disabled:opacity-40 cursor-pointer"
>
- {isBusy ? : }
+ {isBusy ? : }
Save
@@ -611,7 +612,7 @@ export function PostView() {
className="inline-flex items-center gap-1.5 text-xs font-semibold text-white bg-zinc-900 hover:bg-zinc-800 disabled:bg-zinc-200 disabled:text-zinc-400 px-4 py-2 rounded-lg transition-all duration-200 disabled:cursor-not-allowed cursor-pointer"
>
{commentSubmitting && (
-
+
)}
Comment
diff --git a/app/src/pages/post/WardenPost.tsx b/app/src/pages/post/WardenPost.tsx
index 1385cc6..b81220c 100644
--- a/app/src/pages/post/WardenPost.tsx
+++ b/app/src/pages/post/WardenPost.tsx
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { MainLayout } from '../../components/layout/MainLayout';
+import { Loader } from '../../components/Loader';
import { POST_TYPES } from '../../constants/models';
export function WardenPost() {
@@ -163,12 +164,7 @@ export function WardenPost() {
disabled={loading}
className={`inline-flex items-center gap-2 bg-[#16a34a] hover:bg-[#15803d] text-white font-semibold py-2.5 px-8 rounded-lg transition-colors duration-200 text-sm active:scale-[0.98] ${loading ? 'opacity-70 cursor-not-allowed' : 'cursor-pointer'}`}
>
- {loading && (
-
- )}
+ {loading && }
{loading ? 'Submitting…' : 'Submit Complaint'}
diff --git a/app/src/pages/profile/Profile.tsx b/app/src/pages/profile/Profile.tsx
index 5f62f84..5ee478d 100644
--- a/app/src/pages/profile/Profile.tsx
+++ b/app/src/pages/profile/Profile.tsx
@@ -7,6 +7,7 @@ import {
import { MainLayout } from '../../components/layout/MainLayout';
import { ComplaintCard } from '../../components/ComplaintCard';
import type { ComplaintPost, EditForm, Role } from '../../components/ComplaintCard';
+import { Loader } from '../../components/Loader';
interface ProfileData {
name?: string;
@@ -83,7 +84,7 @@ export function Profile() {
-
+
Loading profile data…
@@ -293,7 +294,7 @@ export function Profile() {
{postsLoading && (
-
+
Fetching your complaints…
)}