Skip to content

Commit 5f38135

Browse files
committed
Add authentication guard and login modal (#1039)
* Add authentication guard and login modal * Fix login redirect * Fix casing on pathname of png
1 parent c6776d9 commit 5f38135

File tree

11 files changed

+273
-48
lines changed

11 files changed

+273
-48
lines changed
433 KB
Loading

client/src/components/landingPage/HeroSection/HeroSection.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { NavigateNext } from '@mui/icons-material';
2-
2+
import { useNavigate } from 'react-router-dom';
33
import notangles from '../../../assets/notangles_1.png';
44
import { FlipWords } from '../flip-words';
5+
import { useAuth } from '../../../hooks/useAuth';
56

6-
const handleStartClick = () => {
7-
localStorage.setItem('visited', 'true');
8-
window.location.href = '/';
9-
};
7+
const HeroSection = ({ handleStartClick }: { handleStartClick: () => void }) => {
8+
const { loggedIn } = useAuth();
9+
const navigate = useNavigate();
10+
11+
// const handleStartClick = () => {
12+
// // TODO: Phase out the visited item
13+
// localStorage.setItem('visited', 'true');
14+
// navigate('/home', { replace: true });
15+
// };
1016

11-
const HeroSection = () => {
1217
const words = ['plan', 'create', 'organise', 'optimise', 'design'];
1318

1419
return (
@@ -35,7 +40,9 @@ const HeroSection = () => {
3540
className="flex justify-center items-center shadow-[0_4px_14px_0_rgb(0,118,255,39%)] hover:shadow-[0_6px_20px_rgba(0,118,255,23%)] hover:bg-[rgba(0,118,255,0.9)] hover:scale-105 px-6 sm:px-8 py-2 sm:py-3 bg-[#0070f3] rounded-3xl text-white font-light transition duration-200 ease-linear mt-5"
3641
onClick={handleStartClick}
3742
>
38-
<p className="pr-1 ml-2 text-xl sm:text-2xl md:text-3xl font-medium">Start</p>
43+
<p className="pr-1 ml-2 text-xl sm:text-2xl md:text-3xl font-medium">
44+
{loggedIn ? 'Goto Timetable' : 'Get Started'}
45+
</p>
3946
<NavigateNext fontSize="large" />
4047
</button>
4148
</div>

client/src/components/landingPage/LandingPage.tsx

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,52 @@
1+
import { useState } from 'react';
12
import notangles from '../../assets/notangles_1.png';
3+
import { useAuth } from '../../hooks/useAuth';
4+
import AuthModal, { AuthModalProps } from '../login/AuthModal';
25
import FeedbackSection from './FeedbackSection';
36
import Footer from './Footer';
47
import HeroSection from './HeroSection/HeroSection';
58
import FeaturesSection from './KeyFeaturesSection/FeaturesSection';
69
import ScrollingFeaturesSection from './ScrollingFeaturesSection';
710
import SponsorsSection from './SponsorsSection';
11+
import { useNavigate } from 'react-router-dom';
12+
import { API_URL } from '../../api/config';
813

914
const LandingPage = () => {
15+
const [authModalOpen, setAuthModalOpen] = useState(false);
16+
const { loading } = useAuth();
17+
const onSignIn: AuthModalProps['onSignIn'] = (provider) => {
18+
localStorage.setItem('visited', 'true');
19+
setAuthModalOpen(false);
20+
window.location.href = `${API_URL.server}/auth/login/${provider}`;
21+
};
22+
1023
return (
11-
<div className="bg-white snap-y snap-mandatory overflow-y-scroll h-screen">
12-
<header className="absolute top-0">
13-
<div className="w-44 h-24 flex justify-center items-center">
14-
<img src={notangles} className="w-12 cursor-pointer" />
15-
<p className="font-semibold text-lg pl-1 cursor-pointer select-none">Notangles</p>
24+
<>
25+
<div className="bg-white snap-y snap-mandatory overflow-y-scroll h-screen">
26+
<header className="absolute top-0">
27+
<div className="w-44 h-24 flex justify-center items-center">
28+
<img src={notangles} className="w-12 cursor-pointer" />
29+
<p className="font-semibold text-lg pl-1 cursor-pointer select-none">Notangles</p>
30+
</div>
31+
</header>
32+
<div className="snap-center h-screen">
33+
<HeroSection handleStartClick={() => setAuthModalOpen(true)} />
1634
</div>
17-
</header>
18-
<div className="snap-center h-screen">
19-
<HeroSection />
20-
</div>
21-
<div className="snap-center h-screen flex flex-col justify-center items-center">
22-
<div className="flex pt-20 flex-col items-around justify-around">
23-
<SponsorsSection />
24-
<FeaturesSection />
35+
<div className="snap-center h-screen flex flex-col justify-center items-center">
36+
<div className="flex pt-20 flex-col items-around justify-around">
37+
<SponsorsSection />
38+
<FeaturesSection />
39+
</div>
40+
</div>
41+
<ScrollingFeaturesSection />
42+
<div className="snap-center h-screen flex flex-col justify-between">
43+
<FeedbackSection />
44+
{/* Sticky Footer */}
45+
<Footer />
2546
</div>
2647
</div>
27-
<ScrollingFeaturesSection />
28-
<div className="snap-center h-screen flex flex-col justify-between">
29-
<FeedbackSection />
30-
{/* Sticky Footer */}
31-
<Footer />
32-
</div>
33-
</div>
48+
<AuthModal open={authModalOpen} onClose={() => setAuthModalOpen(false)} loading={loading} onSignIn={onSignIn} />
49+
</>
3450
);
3551
};
3652

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Navigate } from 'react-router-dom';
2+
import { useAuth } from '../../hooks/useAuth';
3+
import PageLoading from '../pageLoading/PageLoading';
4+
5+
export function AuthGuard({ children }: { children: JSX.Element }) {
6+
const { loading, loggedIn } = useAuth();
7+
8+
if (loading) return <PageLoading />;
9+
if (!loggedIn) return <Navigate to="/" replace />;
10+
return children;
11+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import React from 'react';
2+
import { Dialog, DialogContent, Grid, Box, Typography, Button, IconButton, Skeleton, useTheme } from '@mui/material';
3+
import GitHubIcon from '@mui/icons-material/GitHub';
4+
import GoogleIcon from '@mui/icons-material/Google';
5+
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
6+
import CloseIcon from '@mui/icons-material/Close';
7+
8+
export interface AuthModalProps {
9+
open: boolean;
10+
onClose: () => void;
11+
onSignIn: (provider: 'devsoc' | 'google' | 'github' | 'guest') => void;
12+
loading?: boolean;
13+
}
14+
15+
export default function LoginDialog({ open, onClose, onSignIn, loading = false }: AuthModalProps) {
16+
return (
17+
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
18+
<DialogContent sx={{ p: 0, height: 500, position: 'relative' }}>
19+
<IconButton onClick={onClose} sx={{ position: 'absolute', top: 8, right: 8, zIndex: 20 }}>
20+
<CloseIcon />
21+
</IconButton>
22+
23+
<Grid container sx={{ height: '100%' }}>
24+
{/* ◀ Illustration Panel */}
25+
<Grid
26+
item
27+
xs={12}
28+
md={6}
29+
sx={{
30+
bgcolor: 'primary.main',
31+
color: '#fff',
32+
display: 'flex',
33+
alignItems: 'center',
34+
justifyContent: 'center',
35+
p: 3,
36+
}}
37+
>
38+
<Box textAlign="center">
39+
<Typography variant="h3" gutterBottom>
40+
notangles
41+
</Typography>
42+
<Typography>timetable planner</Typography>
43+
</Box>
44+
</Grid>
45+
46+
{/* ▶ Form Panel */}
47+
<Grid
48+
item
49+
xs={12}
50+
md={6}
51+
sx={{
52+
display: 'flex',
53+
flexDirection: 'column',
54+
justifyContent: 'center',
55+
alignItems: 'center',
56+
p: 4,
57+
}}
58+
>
59+
{loading ? (
60+
<>
61+
{/* Header skeletons */}
62+
<Skeleton width={180} height={32} sx={{ mb: 1 }} />
63+
<Skeleton width={140} height={20} sx={{ mb: 3 }} />
64+
65+
{/* Button skeletons */}
66+
<Skeleton variant="rectangular" width="100%" height={48} sx={{ mb: 2 }} />
67+
<Skeleton variant="rectangular" width="100%" height={48} sx={{ mb: 2 }} />
68+
<Skeleton variant="rectangular" width="100%" height={48} sx={{ mb: 2 }} />
69+
70+
{/* Guest text skeleton */}
71+
<Skeleton width="60%" height={24} />
72+
</>
73+
) : (
74+
<>
75+
<Typography variant="h5" gutterBottom>
76+
Welcome back
77+
</Typography>
78+
<Typography variant="body2" color="text.secondary" mb={3}>
79+
Sign in to continue
80+
</Typography>
81+
82+
<Button fullWidth startIcon={<AccountCircleIcon />} onClick={() => onSignIn('devsoc')} sx={{ mb: 2 }}>
83+
Sign in with zID
84+
</Button>
85+
86+
<Button fullWidth startIcon={<GoogleIcon />} onClick={() => onSignIn('google')} sx={{ mb: 2 }} disabled>
87+
Sign in with Google
88+
</Button>
89+
90+
<Button fullWidth startIcon={<GitHubIcon />} onClick={() => onSignIn('github')} sx={{ mb: 2 }} disabled>
91+
Sign in with GitHub
92+
</Button>
93+
94+
<Button fullWidth variant="outlined" onClick={() => onSignIn('guest')} disabled>
95+
Continue as Guest
96+
</Button>
97+
</>
98+
)}
99+
</Grid>
100+
</Grid>
101+
</DialogContent>
102+
</Dialog>
103+
);
104+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { keyframes, styled } from '@mui/system';
2+
import logo from '../../assets/notanglesWithBg.png';
3+
4+
const PageWrapper = styled('div')`
5+
height: 100vh;
6+
display: flex;
7+
align-items: center;
8+
justify-content: center;
9+
flex-direction: column;
10+
`;
11+
12+
const pulse = keyframes`
13+
0% {
14+
box-shadow: 0 0 0 0px rgba(84, 72, 91, 0.2);
15+
}
16+
100% {
17+
box-shadow: 0 0 0 40px rgba(17, 3, 52, 0);
18+
}
19+
`;
20+
21+
const LoadingLogo = styled('img')`
22+
width: 200px;
23+
border-radius: 50%;
24+
animation: ${pulse} 1.5s infinite;
25+
`;
26+
27+
const PageLoading = () => (
28+
<PageWrapper>
29+
<LoadingLogo src={logo} alt="Notangles Logo" />
30+
</PageWrapper>
31+
);
32+
33+
export default PageLoading;

client/src/hooks/useAuth.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createContext, useContext, useState, useEffect } from 'react';
2+
import { UserInfo } from '../interfaces/User';
3+
import { API_URL } from '../api/config';
4+
5+
type AuthContextType = {
6+
loading: boolean;
7+
loggedIn: boolean;
8+
user: UserInfo | null;
9+
};
10+
11+
const AuthContext = createContext<AuthContextType>({
12+
loading: true,
13+
loggedIn: false,
14+
user: null,
15+
});
16+
17+
export function AuthProvider({ children }: { children: React.ReactNode }) {
18+
const [state, setState] = useState<AuthContextType>({
19+
loading: true,
20+
loggedIn: false,
21+
user: null,
22+
});
23+
24+
useEffect(() => {
25+
fetch(`${API_URL.server}/user/profile`, { credentials: 'include' })
26+
.then((res) => {
27+
if (!res.ok) throw new Error('not logged in');
28+
return res.json();
29+
})
30+
.then((data) => setState({ loading: false, loggedIn: true, user: data }))
31+
.catch(() => setState({ loading: false, loggedIn: false, user: null }));
32+
}, []);
33+
34+
return <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
35+
}
36+
37+
export function useAuth() {
38+
return useContext(AuthContext);
39+
}

client/src/index.tsx

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import LandingPage from './components/landingPage/LandingPage';
1515
import AppContextProvider from './context/AppContext';
1616
import CourseContextProvider from './context/CourseContext';
1717
import * as swRegistration from './serviceWorkerRegistration';
18+
import { AuthGuard } from './components/login/AuthGuard';
19+
import { AuthProvider } from './hooks/useAuth';
1820

1921
Sentry.init({
2022
dsn: import.meta.env.VITE_APP_SENTRY_INGEST_CLIENT,
@@ -23,26 +25,30 @@ Sentry.init({
2325
});
2426

2527
const Root: React.FC = () => {
26-
const hasVisited = localStorage.getItem('visited');
27-
2828
return (
29-
<ApolloProvider client={client}>
30-
<AppContextProvider>
31-
<CourseContextProvider>
32-
<BrowserRouter>
33-
<Routes>
34-
{hasVisited ? (
35-
<Route element={<App />} path="/">
36-
<Route path="/event/:encrypted" element={<EventShareModal />} />
37-
</Route>
38-
) : (
29+
<AuthProvider>
30+
<ApolloProvider client={client}>
31+
<AppContextProvider>
32+
<CourseContextProvider>
33+
<BrowserRouter>
34+
<Routes>
3935
<Route element={<LandingPage />} path="/" />
40-
)}
41-
</Routes>
42-
</BrowserRouter>
43-
</CourseContextProvider>
44-
</AppContextProvider>
45-
</ApolloProvider>
36+
<Route
37+
element={
38+
<AuthGuard>
39+
<App />
40+
</AuthGuard>
41+
}
42+
path="/home"
43+
>
44+
<Route path="/home/event/:encrypted" element={<EventShareModal />} />
45+
</Route>
46+
</Routes>
47+
</BrowserRouter>
48+
</CourseContextProvider>
49+
</AppContextProvider>
50+
</ApolloProvider>
51+
</AuthProvider>
4652
);
4753
};
4854

client/src/interfaces/User.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type UserInfo = {
2+
id: string;
3+
firstName: string;
4+
lastName: string;
5+
profilePictureUrl?: string;
6+
};

server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,5 @@
9191
"coverageDirectory": "../coverage",
9292
"testEnvironment": "node"
9393
},
94-
"packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
94+
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
9595
}

0 commit comments

Comments
 (0)