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
21 changes: 21 additions & 0 deletions src/AnimateIn.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Box } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react'
import { Outlet } from 'react-router-dom';

const MotionBox = motion.create(Box);

function AnimateIn() {
return (
<MotionBox
key="animatedContent"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.1 } }}
>
<Outlet />
</MotionBox>
)
}

export default AnimateIn
14 changes: 14 additions & 0 deletions src/DefaultLayout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'
import { Outlet } from 'react-router-dom'
import Navbar from './components/Navbar'

function DefaultLayout() {
return (
<div className='defaultLayout'>
<Navbar />
<Outlet />
</div>
)
}

export default DefaultLayout
52 changes: 52 additions & 0 deletions src/ProtectedLayout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import ToastWizard from './components/toastWizard';
import { Outlet, useNavigate } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { Box } from '@chakra-ui/react';
import CentredSpinner from './components/CentredSpinner';
import { setDisableSessionCheck } from './slices/AuthState';

const MotionBox = motion.create(Box);

function ProtectedLayout() {
const dispatch = useDispatch();
const navigate = useNavigate();
const { username, loaded, disableSessionCheck, error } = useSelector(state => state.auth);

useEffect(() => {
if (loaded && !username && !disableSessionCheck) {
console.log("Unauthorised access to protected route. Redirecting to login.");
navigate("/auth/login");
ToastWizard.standard("error", "Please sign in first.")
} else if (disableSessionCheck) {
dispatch(setDisableSessionCheck(false));
}
}, [username, loaded])

return (
<AnimatePresence mode="wait">
{!loaded ? (
<MotionBox
key="spinner"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.1 } }}
>
<CentredSpinner />
</MotionBox>
) : (
<MotionBox
key="content"
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.1 } }}
exit={{ opacity: 0 }}
>
<Outlet />
</MotionBox>
)}
</AnimatePresence>
)
}

export default ProtectedLayout
7 changes: 3 additions & 4 deletions src/Layout.jsx → src/RootLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@ import { useDispatch } from 'react-redux'
import { Outlet, useNavigate } from 'react-router';
import Navbar from './components/Navbar';
import { Toaster } from './components/ui/toaster';
import { fetchSession } from './slices/AuthState';

function Layout() {
const dispatch = useDispatch();
const navigate = useNavigate();

useEffect(() => {
dispatch(fetchSession());
console.log("Root layout mounted.");
}, [])

return (
<>
<div className='defaultLayout'>
<Navbar />
<Outlet />
</div>
<Outlet />
<Toaster />
</>
)
Expand Down
18 changes: 18 additions & 0 deletions src/components/CentredSpinner.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Center, Spinner } from '@chakra-ui/react'
import { useEffect, useState } from 'react'

function CentredSpinner() {
const [color, setColor] = useState('primaryColour');

useEffect(() => {
const timer = setInterval(() => {
setColor(prevColor => prevColor === 'primaryColour' ? 'sccciColour' : 'primaryColour');
}, 500);

return () => clearInterval(timer);
}, [])

return <Center h={'80vh'}><Spinner size={"xl"} color={color} /></Center>
}

export default CentredSpinner
18 changes: 15 additions & 3 deletions src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,21 @@ import Sidebar from './Sidebar'
import { Avatar, Box, Button, Flex, HStack, Icon, Spacer, Text } from '@chakra-ui/react'
import { FaHamburger } from 'react-icons/fa'
import { FaBarsStaggered } from 'react-icons/fa6'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'

function Navbar() {
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
const { username } = useSelector(state => state.auth);

const handleLogoClick = () => {
navigate('/');
}

const handleProfileClick = () => {
navigate('/sampleProtected');
}

const toggleSidebar = () => {
setIsOpen(!isOpen);
Expand All @@ -15,10 +27,10 @@ function Navbar() {
<Flex as={'nav'} width={'100%'} p={'10px'} paddingLeft={'20px'} paddingRight={'20px'} borderRadius={'20px'} alignItems={'center'} flexDir={'row'} bgGradient="to-r" gradientFrom="rgb(18, 20, 76)" gradientTo="rgb(0, 124, 122)">
<Icon as={FaBarsStaggered} boxSize={6} color={'white'} onClick={toggleSidebar} cursor={'pointer'} />
<Spacer />
<Text fontSize={'2xl'} fontWeight={'bolder'} color={'white'} fontFamily={'Pixelify Sans'}>ArchAIve</Text>
<Text cursor={'pointer'} onClick={handleLogoClick} fontSize={'2xl'} fontWeight={'bolder'} color={'white'} fontFamily={'Pixelify Sans'}>ArchAIve</Text>
<Spacer />
<Avatar.Root>
<Avatar.Fallback name='John Appleseed' />
<Avatar.Root onClick={handleProfileClick}>
<Avatar.Fallback name={username} />
</Avatar.Root>
</Flex>
<Sidebar isOpen={isOpen} onOpenChange={(e) => setIsOpen(e.open)} />
Expand Down
11 changes: 6 additions & 5 deletions src/components/toastWizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { toaster } from "./ui/toaster";
* "Please wait while we load your data", // loadingDescription
* callback, // action (optional)
* "Cancel" // actionLabel. provide if action is being provided. (optional)
* ['success', 'error', 'loading'] // actionForTypes, i.e which states should render the action button. provide if action is being provided. (optional)
* )
* ```
*
Expand Down Expand Up @@ -72,28 +73,28 @@ class ToastWizard {
return id;
}

static promiseBased(promise, successTitle, successDescription, errorTitle, errorDescription, loadingTitle, loadingDescription, action=null, actionLabel=null) {
static promiseBased(promise, successTitle, successDescription, errorTitle, errorDescription, loadingTitle, loadingDescription, action=null, actionLabel=null, actionForTypes=[]) {
return toaster.promise(promise, {
success: {
title: successTitle,
description: successDescription,
action: action ? {
action: action && actionLabel && actionForTypes.indexOf('success') > -1 ? {
label: actionLabel,
onClick: action
} : null
}: null
},
error: {
title: errorTitle,
description: errorDescription,
action: action ? {
action: action && actionLabel && actionForTypes.indexOf('error') > -1 ? {
label: actionLabel,
onClick: action
} : null
},
loading: {
title: loadingTitle,
description: loadingDescription,
action: action ? {
action: action && actionLabel && actionForTypes.indexOf('loading') > -1 ? {
label: actionLabel,
onClick: action
} : null
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/toaster.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const toastColors = {
error: 'sccciColour',
info: 'blue.500',
warning: 'orange.500',
loading: 'gray.500',
default: 'gray.700',
loading: 'white',
default: 'white',
};


Expand Down
38 changes: 28 additions & 10 deletions src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ import authReducer from './slices/AuthState.js'
import { BrowserRouter, Route, Routes } from 'react-router'
import Catalogue from './pages/Catalogue.jsx'
// import Test from './pages/test.jsx'
import Layout from './Layout.jsx'
import RootLayout from './RootLayout.jsx'
import GalleryLayout from './GalleryLayout.jsx';
import MainTheme from './themes/MainTheme.js'
import Health from './pages/Health.jsx'
import Homepage from './pages/Homepage.jsx'
import Login from './pages/Login.jsx';
import DefaultLayout from './DefaultLayout.jsx';
import SampleProtected from './pages/SampleProtected.jsx';
import ProtectedLayout from './ProtectedLayout.jsx';
import AnimateIn from './AnimateIn.jsx';

const store = configureStore({
reducer: {
Expand All @@ -28,19 +32,33 @@ createRoot(document.getElementById('root')).render(
<ChakraProvider value={MainTheme}>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Homepage />} />
<Route path='health' element={<Health />} />
<Route path='/' element={<RootLayout />}>
{/* Default Layout Pages */}
<Route element={<DefaultLayout />}>
<Route element={<AnimateIn />}>
<Route index element={<Homepage />} />

<Route path="auth">
<Route path="login" element={<Login />} />
<Route path='health' element={<Health />} />

<Route path='auth'>
<Route path='login' element={<Login />} />
</Route>
</Route>

{/* Protected Pages */}
<Route element={<ProtectedLayout />}>
<Route path='sampleProtected' element={<SampleProtected />} />
</Route>
</Route>

{/* <Route path='/test' element={<Test />} /> */}
</Route>
{/* Pages needing a custom layout */}

<Route path="/catalogue" element={<GalleryLayout />}>
<Route index element={<Catalogue />} />
{/* Gallery Layout Pages */}
<Route element={<GalleryLayout />}>
<Route element={<ProtectedLayout />}>
<Route path='catalogue' element={<Catalogue />} />
</Route>
</Route>
</Route>
</Routes>
</BrowserRouter>
Expand Down
6 changes: 5 additions & 1 deletion src/networking.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,12 @@ const instance = axios.create({
})

instance.interceptors.request.use((config) => {
config.headers["Content-Type"] = "application/json";
if (config.method == 'post') {
config.headers["Content-Type"] = "application/json";
}

config.headers["APIKey"] = import.meta.env.VITE_BACKEND_API_KEY;
config.withCredentials = true;

return config;
}, (err) => {
Expand Down
10 changes: 9 additions & 1 deletion src/pages/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ import { useState } from 'react'
import sccciBuildingImage from '../assets/sccciBuildingOpening1964.png'
import server, { JSONResponse } from '../networking'
import ToastWizard from '../components/toastWizard'
import { useDispatch } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { fetchSession } from '../slices/AuthState'

function Login() {
const dispatch = useDispatch();
const navigate = useNavigate();

const [usernameOrEmail, setUsernameOrEmail] = useState('');
const [password, setPassword] = useState('');
const [loginLoading, setLoginLoading] = useState(false);
Expand Down Expand Up @@ -38,6 +44,8 @@ function Login() {
}

// Success case
dispatch(fetchSession());
navigate("/sampleProtected");
ToastWizard.standard("success", "Login successful.", "Welcome back to ArchAIve!")
} else {
console.log("Unexpected response in login request:", res.data);
Expand All @@ -47,7 +55,7 @@ function Login() {
setLoginLoading(false);
})
.catch(err => {
if (err.response.data instanceof JSONResponse) {
if (err.response && err.response.data instanceof JSONResponse) {
console.log("Error response in login request:", err.response.data.fullMessage());
if (err.response.data.userErrorType()) {
ToastWizard.standard("error", "Login failed.", err.response.data.message);
Expand Down
42 changes: 40 additions & 2 deletions src/pages/SampleProtected.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,46 @@
import React from 'react'
import { Button, Text } from "@chakra-ui/react"
import { useDispatch } from "react-redux"
import { logout } from "../slices/AuthState";
import ToastWizard from "../components/toastWizard";
import { useNavigate } from "react-router-dom";

function SampleProtected() {
const dispatch = useDispatch();
const navigate = useNavigate();

const handleLogout = () => {
// ToastWizard.standard("success", "Logout successful.", "See you soon!");

const logoutPromise = new Promise((resolve, reject) => {
dispatch(logout(true, (msg) => {
if (typeof msg === 'string') {
reject(msg);
} else {
navigate('/auth/login');
resolve(msg);
}
}));
})

ToastWizard.promiseBased(
logoutPromise,
"Logged out successfully.",
"You have been logged out.",
"Logout failed.",
"Something went wrong while logging out. Please try again.",
"Logging out...",
"Connecting to server...",
handleLogout,
"Retry",
['error']
)
}

return (
<div>SampleProtected</div>
<div>
<Text>SampleProtected</Text>
<Button variant={'ArchPrimary'} onClick={handleLogout}>Logout</Button>
</div>
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/slices/AuthState.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const logout = (disableSessionCheck=false, handler=null) => async (dispat
} else if (typeof err === 'string') {
errorMessage = err;
}
console.log("Error in logout:", errorMessage);
console.log("ASLOGOUT ERROR:", errorMessage);
if (handler) {
handler(errorMessage);
}
Expand Down