Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added client/src/assets/notanglesWithBg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 14 additions & 7 deletions client/src/components/landingPage/HeroSection/HeroSection.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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}
>
<p className="pr-1 ml-2 text-xl sm:text-2xl md:text-3xl font-medium">Start</p>
<p className="pr-1 ml-2 text-xl sm:text-2xl md:text-3xl font-medium">
{loggedIn ? 'Goto Timetable' : 'Get Started'}
</p>
<NavigateNext fontSize="large" />
</button>
</div>
Expand Down
56 changes: 36 additions & 20 deletions client/src/components/landingPage/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="bg-white snap-y snap-mandatory overflow-y-scroll h-screen">
<header className="absolute top-0">
<div className="w-44 h-24 flex justify-center items-center">
<img src={notangles} className="w-12 cursor-pointer" />
<p className="font-semibold text-lg pl-1 cursor-pointer select-none">Notangles</p>
<>
<div className="bg-white snap-y snap-mandatory overflow-y-scroll h-screen">
<header className="absolute top-0">
<div className="w-44 h-24 flex justify-center items-center">
<img src={notangles} className="w-12 cursor-pointer" />
<p className="font-semibold text-lg pl-1 cursor-pointer select-none">Notangles</p>
</div>
</header>
<div className="snap-center h-screen">
<HeroSection handleStartClick={() => setAuthModalOpen(true)} />
</div>
</header>
<div className="snap-center h-screen">
<HeroSection />
</div>
<div className="snap-center h-screen flex flex-col justify-center items-center">
<div className="flex pt-20 flex-col items-around justify-around">
<SponsorsSection />
<FeaturesSection />
<div className="snap-center h-screen flex flex-col justify-center items-center">
<div className="flex pt-20 flex-col items-around justify-around">
<SponsorsSection />
<FeaturesSection />
</div>
</div>
<ScrollingFeaturesSection />
<div className="snap-center h-screen flex flex-col justify-between">
<FeedbackSection />
{/* Sticky Footer */}
<Footer />
</div>
</div>
<ScrollingFeaturesSection />
<div className="snap-center h-screen flex flex-col justify-between">
<FeedbackSection />
{/* Sticky Footer */}
<Footer />
</div>
</div>
<AuthModal open={authModalOpen} onClose={() => setAuthModalOpen(false)} loading={loading} onSignIn={onSignIn} />
</>
);
};

Expand Down
11 changes: 11 additions & 0 deletions client/src/components/login/AuthGuard.tsx
Original file line number Diff line number Diff line change
@@ -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 <PageLoading />;
if (!loggedIn) return <Navigate to="/" replace />;
return children;
}
104 changes: 104 additions & 0 deletions client/src/components/login/AuthModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogContent sx={{ p: 0, height: 500, position: 'relative' }}>
<IconButton onClick={onClose} sx={{ position: 'absolute', top: 8, right: 8, zIndex: 20 }}>
<CloseIcon />
</IconButton>

<Grid container sx={{ height: '100%' }}>
{/* ◀ Illustration Panel */}
<Grid
item
xs={12}
md={6}
sx={{
bgcolor: 'primary.main',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: 3,
}}
>
<Box textAlign="center">
<Typography variant="h3" gutterBottom>
notangles
</Typography>
<Typography>timetable planner</Typography>
</Box>
</Grid>

{/* ▶ Form Panel */}
<Grid
item
xs={12}
md={6}
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
p: 4,
}}
>
{loading ? (
<>
{/* Header skeletons */}
<Skeleton width={180} height={32} sx={{ mb: 1 }} />
<Skeleton width={140} height={20} sx={{ mb: 3 }} />

{/* Button skeletons */}
<Skeleton variant="rectangular" width="100%" height={48} sx={{ mb: 2 }} />
<Skeleton variant="rectangular" width="100%" height={48} sx={{ mb: 2 }} />
<Skeleton variant="rectangular" width="100%" height={48} sx={{ mb: 2 }} />

{/* Guest text skeleton */}
<Skeleton width="60%" height={24} />
</>
) : (
<>
<Typography variant="h5" gutterBottom>
Welcome back
</Typography>
<Typography variant="body2" color="text.secondary" mb={3}>
Sign in to continue
</Typography>

<Button fullWidth startIcon={<AccountCircleIcon />} onClick={() => onSignIn('devsoc')} sx={{ mb: 2 }}>
Sign in with zID
</Button>

<Button fullWidth startIcon={<GoogleIcon />} onClick={() => onSignIn('google')} sx={{ mb: 2 }} disabled>
Sign in with Google
</Button>

<Button fullWidth startIcon={<GitHubIcon />} onClick={() => onSignIn('github')} sx={{ mb: 2 }} disabled>
Sign in with GitHub
</Button>

<Button fullWidth variant="outlined" onClick={() => onSignIn('guest')} disabled>
Continue as Guest
</Button>
</>
)}
</Grid>
</Grid>
</DialogContent>
</Dialog>
);
}
33 changes: 33 additions & 0 deletions client/src/components/pageLoading/PageLoading.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<PageWrapper>
<LoadingLogo src={logo} alt="Notangles Logo" />
</PageWrapper>
);

export default PageLoading;
39 changes: 39 additions & 0 deletions client/src/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
@@ -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<AuthContextType>({
loading: true,
loggedIn: false,
user: null,
});

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<AuthContextType>({
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 <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
}

export function useAuth() {
return useContext(AuthContext);
}
42 changes: 24 additions & 18 deletions client/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,26 +25,30 @@ Sentry.init({
});

const Root: React.FC = () => {
const hasVisited = localStorage.getItem('visited');

return (
<ApolloProvider client={client}>
<AppContextProvider>
<CourseContextProvider>
<BrowserRouter>
<Routes>
{hasVisited ? (
<Route element={<App />} path="/">
<Route path="/event/:encrypted" element={<EventShareModal />} />
</Route>
) : (
<AuthProvider>
<ApolloProvider client={client}>
<AppContextProvider>
<CourseContextProvider>
<BrowserRouter>
<Routes>
<Route element={<LandingPage />} path="/" />
)}
</Routes>
</BrowserRouter>
</CourseContextProvider>
</AppContextProvider>
</ApolloProvider>
<Route
element={
<AuthGuard>
<App />
</AuthGuard>
}
path="/home"
>
<Route path="/home/event/:encrypted" element={<EventShareModal />} />
</Route>
</Routes>
</BrowserRouter>
</CourseContextProvider>
</AppContextProvider>
</ApolloProvider>
</AuthProvider>
);
};

Expand Down
6 changes: 6 additions & 0 deletions client/src/interfaces/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type UserInfo = {
id: string;
firstName: string;
lastName: string;
profilePictureUrl?: string;
};
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,5 @@
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
}
7 changes: 5 additions & 2 deletions server/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
);
}
}