diff --git a/package-lock.json b/package-lock.json
index 2741a99..41370f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@heroicons/react": "^2.2.0",
+ "lucide-react": "^0.564.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0"
@@ -62,7 +63,6 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -350,7 +350,6 @@
"integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
@@ -392,7 +391,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -433,7 +431,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -1698,7 +1695,6 @@
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1709,7 +1705,6 @@
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1769,7 +1764,6 @@
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
@@ -2021,7 +2015,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2163,7 +2156,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2520,7 +2512,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3369,6 +3360,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.564.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.564.0.tgz",
+ "integrity": "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/mathml-tag-names": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-4.0.0.tgz",
@@ -3615,7 +3615,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -3643,7 +3642,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -3686,7 +3684,6 @@
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -3777,7 +3774,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3787,7 +3783,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4445,7 +4440,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4552,7 +4546,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -4688,7 +4681,6 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index eb24117..1a7a2d0 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@heroicons/react": "^2.2.0",
+ "lucide-react": "^0.564.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0"
diff --git a/src/App.tsx b/src/App.tsx
index 51e9225..62582dc 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,7 +2,6 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { ThemeProvider } from './contexts/ThemeContext';
import AppLayout from './components/AppLayout.tsx';
import Home from './pages/Home.tsx';
-import Register from './pages/Register.tsx';
import Dashboard from './pages/Dashboard.tsx';
import CreateEvent from './pages/events/CreateEvent.tsx';
import EventHistory from './pages/events/EventHistory.tsx';
@@ -11,30 +10,32 @@ import Roster from './pages/Roster.tsx';
import Notifications from './pages/Notifications.tsx';
import Profile from './pages/Profile.tsx';
import Settings from './pages/Settings.tsx';
+import { AuthProvider } from './context/AuthContext.tsx';
import HelpCenter from './pages/HelpCenter.tsx';
function App() {
return (
<>
-
-
-
- } />
- } />
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
-
+
+
+
+
+ } />
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
>
);
}
diff --git a/src/components/LoginPopup.tsx b/src/components/LoginPopup.tsx
new file mode 100644
index 0000000..0500910
--- /dev/null
+++ b/src/components/LoginPopup.tsx
@@ -0,0 +1,48 @@
+import React, { useEffect } from 'react';
+import { useAuth } from '../context/AuthContext';
+import '../css/login.css';
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+};
+
+export default function LoginPopup({ isOpen, onClose }: Props) {
+ const { login, user } = useAuth();
+
+ useEffect(() => {
+ if (user && isOpen) {
+ onClose();
+ }
+ }, [user, isOpen, onClose]);
+
+ if (!isOpen) return null;
+
+ const handleOverlayClick = () => {
+ onClose();
+ };
+
+ const handlePopupClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ };
+
+ return (
+
+
+
+
+
Register or Login
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx
index d6cd94a..757f806 100644
--- a/src/components/sidebar/Sidebar.tsx
+++ b/src/components/sidebar/Sidebar.tsx
@@ -1,6 +1,10 @@
import '../../css/sidebar.css';
+import '../../css/login.css';
import SidebarGroup from './SidebarGroup.tsx';
import SidebarLink from './SidebarLink.tsx';
+//import { type useNavigate } from 'react-router-dom';
+import { useState } from 'react';
+import LoginPopup from '../LoginPopup.tsx';
import SidebarProfile from './SidebarProfile.tsx';
import SidebarWorkspace from './SidebarWorkspace.tsx';
@@ -24,37 +28,69 @@ type SidebarProps = {
};
export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
+ const [showLogin, setShowLogin] = useState(false);
+
return (
-
-
-
- } nav="/dashboard" />
- } collapsed={collapsed}>
- } nav="/events/create" />
+ <>
+
+
+
+ } nav="/dashboard" />
+ } nav="/events" />
+ } nav="/calendar" />
+ } nav="/roster" />
+ }
+ nav="/notifications"
+ />
+
+
+ } nav="/profile" />
+ } nav="/settings" />
+
+
setShowLogin(true)}>
+ {' '}
+ Register{' '}
+
+
setShowLogin(false)} />
+
+ } nav="/dashboard" />
+ } collapsed={collapsed}>
+ } nav="/events/create" />
+ }
+ nav="/events/history"
+ />
+
+ } nav="/calendar" />
+ } nav="/roster" />
}
- nav="/events/history"
+ label="Notifications"
+ icon={}
+ nav="/notifications"
/>
-
- } nav="/calendar" />
- } nav="/roster" />
- } nav="/notifications" />
-
-
- } nav="/help" />
- } nav="/settings" />
- }
- name="Kevin Smith"
- email="smithk@rpi.edu"
- />
-
-
+
+
+ }
+ nav="/help"
+ />
+ } nav="/settings" />
+ }
+ name="Kevin Smith"
+ email="smithk@rpi.edu"
+ />
+
+
+ >
);
}
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
new file mode 100644
index 0000000..e2eb7fb
--- /dev/null
+++ b/src/context/AuthContext.tsx
@@ -0,0 +1,128 @@
+/* eslint-disable react-refresh/only-export-components*/
+import React, { createContext, useContext, useState, type ReactNode } from 'react';
+
+type User = {
+ id?: string;
+ name?: string;
+ email?: string;
+};
+
+type AuthContextType = {
+ user: User | null;
+ loading: boolean;
+ error: string | null;
+ login: (provider: 'google' | 'microsoft') => void;
+ logout: () => Promise;
+ refreshUser: () => Promise;
+};
+
+type AuthProviderProps = {
+ children: ReactNode;
+};
+
+const AuthContext = createContext(undefined);
+
+export const AuthProvider: React.FC = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchMe = async (): Promise => {
+ try {
+ setLoading(true);
+
+ const response = await fetch('https://api.capyrpi.org/v1/auth/me', {
+ headers: {
+ Accept: 'application/json',
+ },
+ });
+
+ if (response.ok) {
+ const data: User = await response.json();
+ setUser(data);
+ } else {
+ setUser(null);
+ }
+ } catch (err: unknown) {
+ if (err instanceof Error) {
+ console.error('Failed to fetch user:', err);
+ setError(err.message);
+ } else {
+ console.error('Unknown error:', err);
+ setError('Unknown error');
+ }
+
+ setUser(null);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const login = (provider: 'google' | 'microsoft'): void => {
+ const url =
+ provider === 'google'
+ ? 'https://api.capyrpi.org/v1/auth/google'
+ : 'https://api.capyrpi.org/v1/auth/microsoft';
+
+ window.open(url, '_blank');
+
+ const pollInterval = setInterval(async () => {
+ try {
+ const response = await fetch('https://api.capyrpi.org/v1/auth/me', {
+ headers: {
+ Accept: 'application/json',
+ },
+ });
+
+ if (response.ok) {
+ const data: User = await response.json();
+ setUser(data);
+ clearInterval(pollInterval);
+ }
+ } catch (err: unknown) {
+ if (err instanceof Error) {
+ console.error('Polling failed:', err.message);
+ }
+ }
+ }, 3000);
+
+ setTimeout(() => {
+ clearInterval(pollInterval);
+ }, 120000);
+ };
+ const logout = async (): Promise => {
+ try {
+ await fetch('https://api.capyrpi.org/v1/auth/logout', {
+ method: 'POST',
+ });
+
+ setUser(null);
+
+ window.location.href = '/app/';
+ } catch (err: unknown) {
+ if (err instanceof Error) {
+ console.error('Logout failed:', err.message);
+ }
+ }
+ };
+ const value: AuthContextType = {
+ user,
+ loading,
+ error,
+ login,
+ logout,
+ refreshUser: fetchMe,
+ };
+
+ return {children};
+};
+
+export const useAuth = (): AuthContextType => {
+ const context = useContext(AuthContext);
+
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+
+ return context;
+};
diff --git a/src/css/login.css b/src/css/login.css
new file mode 100644
index 0000000..218bcfa
--- /dev/null
+++ b/src/css/login.css
@@ -0,0 +1,59 @@
+.popup-overlay {
+ position: fixed;
+ inset: 0;
+ background-color: var(--standard-text);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 9999;
+}
+
+.popup-container {
+ position: relative;
+ background-color: var(--standard-white);
+ border: 1px var(--standard-text);
+ border-radius: 8px;
+ padding: 24px;
+ width: 320px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ box-shadow: 0 4px 12px var(--standard-text);
+}
+
+.popup-title {
+ margin: 0;
+ margin-bottom: 10px;
+ text-align: center;
+ color: var(--standard-black);
+}
+
+.popup-button {
+ background-color: var(--standard-text);
+ border: var(--standard-text);
+ color: var(--standard-white);
+ padding: 10px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.popup-button:hover {
+ background-color: #f0f0f0;
+}
+
+.popup-close-x {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background: none;
+ border: none;
+ font-size: 20px;
+ font-weight: bold;
+ cursor: pointer;
+ color: var(--standard-text);
+}
+
+.popup-close-x:hover {
+ color: var(--standard-black);
+}
diff --git a/src/css/sidebar.css b/src/css/sidebar.css
index d6d9656..061b9e5 100644
--- a/src/css/sidebar.css
+++ b/src/css/sidebar.css
@@ -203,3 +203,7 @@
max-width: 0;
opacity: 0;
}
+
+.register-btn {
+ color: black;
+}
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx
new file mode 100644
index 0000000..3decd95
--- /dev/null
+++ b/src/pages/Login.tsx
@@ -0,0 +1,135 @@
+import { Navigate, useLocation } from 'react-router-dom';
+import { useAuth } from '../context/AuthContext';
+import { Shield, Sparkles } from 'lucide-react';
+
+const Login = () => {
+ const { user, login } = useAuth();
+ const location = useLocation();
+ const from = location.state?.from?.pathname || '/becapy';
+
+ if (user) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Interactive dashboard for RPI students
+
+
+
+ );
+};
+
+export default Login;
diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx
deleted file mode 100644
index 20ed1ab..0000000
--- a/src/pages/Register.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { useNavigate } from 'react-router-dom';
-
-export default function Register() {
- const navigate = useNavigate();
-
- return (
- <>
- Register
-
-
-
- >
- );
-}