diff --git a/client/src/assets/notanglesWithBg.png b/client/src/assets/notanglesWithBg.png
new file mode 100644
index 000000000..85bf6dbb2
Binary files /dev/null and b/client/src/assets/notanglesWithBg.png differ
diff --git a/client/src/components/landingPage/HeroSection/HeroSection.tsx b/client/src/components/landingPage/HeroSection/HeroSection.tsx
index 33b53afed..8c5196a49 100644
--- a/client/src/components/landingPage/HeroSection/HeroSection.tsx
+++ b/client/src/components/landingPage/HeroSection/HeroSection.tsx
@@ -1,14 +1,19 @@
import { NavigateNext } from '@mui/icons-material';
-
+import { useNavigate } from 'react-router-dom';
import notangles from '../../../assets/notangles_1.png';
import { FlipWords } from '../flip-words';
+import { useAuth } from '../../../hooks/useAuth';
-const handleStartClick = () => {
- localStorage.setItem('visited', 'true');
- window.location.href = '/';
-};
+const HeroSection = ({ handleStartClick }: { handleStartClick: () => void }) => {
+ const { loggedIn } = useAuth();
+ const navigate = useNavigate();
+
+ // const handleStartClick = () => {
+ // // TODO: Phase out the visited item
+ // localStorage.setItem('visited', 'true');
+ // navigate('/home', { replace: true });
+ // };
-const HeroSection = () => {
const words = ['plan', 'create', 'organise', 'optimise', 'design'];
return (
@@ -35,7 +40,9 @@ const HeroSection = () => {
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"
onClick={handleStartClick}
>
-
Start
+
+ {loggedIn ? 'Goto Timetable' : 'Get Started'}
+
diff --git a/client/src/components/landingPage/LandingPage.tsx b/client/src/components/landingPage/LandingPage.tsx
index f1f3abaef..34cc189da 100644
--- a/client/src/components/landingPage/LandingPage.tsx
+++ b/client/src/components/landingPage/LandingPage.tsx
@@ -1,36 +1,52 @@
+import { useState } from 'react';
import notangles from '../../assets/notangles_1.png';
+import { useAuth } from '../../hooks/useAuth';
+import AuthModal, { AuthModalProps } from '../login/AuthModal';
import FeedbackSection from './FeedbackSection';
import Footer from './Footer';
import HeroSection from './HeroSection/HeroSection';
import FeaturesSection from './KeyFeaturesSection/FeaturesSection';
import ScrollingFeaturesSection from './ScrollingFeaturesSection';
import SponsorsSection from './SponsorsSection';
+import { useNavigate } from 'react-router-dom';
+import { API_URL } from '../../api/config';
const LandingPage = () => {
+ const [authModalOpen, setAuthModalOpen] = useState(false);
+ const { loading } = useAuth();
+ const onSignIn: AuthModalProps['onSignIn'] = (provider) => {
+ localStorage.setItem('visited', 'true');
+ setAuthModalOpen(false);
+ window.location.href = `${API_URL.server}/auth/login/${provider}`;
+ };
+
return (
-
-
-
-

-
Notangles
+ <>
+
+
+
+

+
Notangles
+
+
+
+ setAuthModalOpen(true)} />
-
-
-
-
-
-
-
-
+
+
+
+
+ {/* Sticky Footer */}
+
-
-
-
- {/* Sticky Footer */}
-
-
-
+
setAuthModalOpen(false)} loading={loading} onSignIn={onSignIn} />
+ >
);
};
diff --git a/client/src/components/login/AuthGuard.tsx b/client/src/components/login/AuthGuard.tsx
new file mode 100644
index 000000000..02f3c3bef
--- /dev/null
+++ b/client/src/components/login/AuthGuard.tsx
@@ -0,0 +1,11 @@
+import { Navigate } from 'react-router-dom';
+import { useAuth } from '../../hooks/useAuth';
+import PageLoading from '../pageLoading/PageLoading';
+
+export function AuthGuard({ children }: { children: JSX.Element }) {
+ const { loading, loggedIn } = useAuth();
+
+ if (loading) return ;
+ if (!loggedIn) return ;
+ return children;
+}
diff --git a/client/src/components/login/AuthModal.tsx b/client/src/components/login/AuthModal.tsx
new file mode 100644
index 000000000..74e564d9f
--- /dev/null
+++ b/client/src/components/login/AuthModal.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { Dialog, DialogContent, Grid, Box, Typography, Button, IconButton, Skeleton, useTheme } from '@mui/material';
+import GitHubIcon from '@mui/icons-material/GitHub';
+import GoogleIcon from '@mui/icons-material/Google';
+import AccountCircleIcon from '@mui/icons-material/AccountCircle';
+import CloseIcon from '@mui/icons-material/Close';
+
+export interface AuthModalProps {
+ open: boolean;
+ onClose: () => void;
+ onSignIn: (provider: 'devsoc' | 'google' | 'github' | 'guest') => void;
+ loading?: boolean;
+}
+
+export default function LoginDialog({ open, onClose, onSignIn, loading = false }: AuthModalProps) {
+ return (
+
+ );
+}
diff --git a/client/src/components/pageLoading/PageLoading.tsx b/client/src/components/pageLoading/PageLoading.tsx
new file mode 100644
index 000000000..fea7ef1ad
--- /dev/null
+++ b/client/src/components/pageLoading/PageLoading.tsx
@@ -0,0 +1,33 @@
+import { keyframes, styled } from '@mui/system';
+import logo from '../../assets/notanglesWithBg.png';
+
+const PageWrapper = styled('div')`
+ height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+`;
+
+const pulse = keyframes`
+ 0% {
+ box-shadow: 0 0 0 0px rgba(84, 72, 91, 0.2);
+ }
+ 100% {
+ box-shadow: 0 0 0 40px rgba(17, 3, 52, 0);
+ }
+`;
+
+const LoadingLogo = styled('img')`
+ width: 200px;
+ border-radius: 50%;
+ animation: ${pulse} 1.5s infinite;
+`;
+
+const PageLoading = () => (
+
+
+
+);
+
+export default PageLoading;
diff --git a/client/src/hooks/useAuth.tsx b/client/src/hooks/useAuth.tsx
new file mode 100644
index 000000000..cd6ab5af0
--- /dev/null
+++ b/client/src/hooks/useAuth.tsx
@@ -0,0 +1,39 @@
+import { createContext, useContext, useState, useEffect } from 'react';
+import { UserInfo } from '../interfaces/User';
+import { API_URL } from '../api/config';
+
+type AuthContextType = {
+ loading: boolean;
+ loggedIn: boolean;
+ user: UserInfo | null;
+};
+
+const AuthContext = createContext({
+ loading: true,
+ loggedIn: false,
+ user: null,
+});
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ const [state, setState] = useState({
+ loading: true,
+ loggedIn: false,
+ user: null,
+ });
+
+ useEffect(() => {
+ fetch(`${API_URL.server}/user/profile`, { credentials: 'include' })
+ .then((res) => {
+ if (!res.ok) throw new Error('not logged in');
+ return res.json();
+ })
+ .then((data) => setState({ loading: false, loggedIn: true, user: data }))
+ .catch(() => setState({ loading: false, loggedIn: false, user: null }));
+ }, []);
+
+ return {children};
+}
+
+export function useAuth() {
+ return useContext(AuthContext);
+}
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 650d38859..f4af9b1a0 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -15,6 +15,8 @@ import LandingPage from './components/landingPage/LandingPage';
import AppContextProvider from './context/AppContext';
import CourseContextProvider from './context/CourseContext';
import * as swRegistration from './serviceWorkerRegistration';
+import { AuthGuard } from './components/login/AuthGuard';
+import { AuthProvider } from './hooks/useAuth';
Sentry.init({
dsn: import.meta.env.VITE_APP_SENTRY_INGEST_CLIENT,
@@ -23,26 +25,30 @@ Sentry.init({
});
const Root: React.FC = () => {
- const hasVisited = localStorage.getItem('visited');
-
return (
-
-
-
-
-
- {hasVisited ? (
- } path="/">
- } />
-
- ) : (
+
+
+
+
+
+
} path="/" />
- )}
-
-
-
-
-
+
+
+
+ }
+ path="/home"
+ >
+ } />
+
+
+
+
+
+
+
);
};
diff --git a/client/src/interfaces/User.ts b/client/src/interfaces/User.ts
new file mode 100644
index 000000000..831b0befc
--- /dev/null
+++ b/client/src/interfaces/User.ts
@@ -0,0 +1,6 @@
+export type UserInfo = {
+ id: string;
+ firstName: string;
+ lastName: string;
+ profilePictureUrl?: string;
+};
diff --git a/server/package.json b/server/package.json
index 654d4b2bd..551b832a8 100644
--- a/server/package.json
+++ b/server/package.json
@@ -91,5 +91,5 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
- "packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
+ "packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
}
diff --git a/server/src/auth/auth.controller.ts b/server/src/auth/auth.controller.ts
index 3209d65d6..e3ddc59ac 100644
--- a/server/src/auth/auth.controller.ts
+++ b/server/src/auth/auth.controller.ts
@@ -4,13 +4,16 @@ import { Response } from 'express';
@Controller('auth')
export class AuthController {
- @Get('login')
+ @Get('login/devsoc')
@UseGuards(AuthGuard('oidc'))
login() {}
@Get('callback/devsoc')
@UseGuards(AuthGuard('oidc'))
callback(@Res() res: Response) {
- res.redirect('http://localhost:3001/api/user/profile');
+ res.redirect(
+ (process.env.NODE_ENV === 'dev' ? `http://` : `https://`) +
+ `${process.env.CLIENT_HOST_NAME}:${process.env.CLIENT_HOST_PORT}`,
+ );
}
}