Skip to content

Commit

Permalink
Merge branch 'NetManagerMapFunc' of github.com:AKATWIJUKA-ELIA/AirQo-…
Browse files Browse the repository at this point in the history
…frontend into NetManagerMapFunc
  • Loading branch information
AKATWIJUKA-ELIA committed Mar 4, 2025
2 parents 3aebe38 + e0c8cdd commit 479cc56
Show file tree
Hide file tree
Showing 74 changed files with 1,536 additions and 1,297 deletions.
2 changes: 1 addition & 1 deletion k8s/platform/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ replicaCount: 1
image:
repository: eu.gcr.io/airqo-250220/airqo-next-platform
pullPolicy: Always
tag: prod-16e65200-1739133930
tag: prod-6a098ecd-1740727843
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
Expand Down
2 changes: 1 addition & 1 deletion k8s/platform/values-stage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ replicaCount: 1
image:
repository: eu.gcr.io/airqo-250220/airqo-stage-next-platform
pullPolicy: Always
tag: stage-963fc29b-1740488187
tag: stage-93a63a9e-1740727732
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
Expand Down
79 changes: 2 additions & 77 deletions src/platform/src/core/utils/protectedRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,87 +2,17 @@ import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useSelector, useDispatch } from 'react-redux';
import LogoutUser from '@/core/utils/LogoutUser';
import Cookies from 'js-cookie';
import jwt_decode from 'jwt-decode';
import {
setUserInfo,
setSuccess,
} from '@/lib/store/services/account/LoginSlice';
import { getIndividualUserPreferences } from '@/lib/store/services/account/UserDefaultsSlice';
import { getUserDetails } from '@/core/apis/Account';
import Spinner from '../../common/components/Spinner';

const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;

export default function withAuth(Component) {
return function WithAuthComponent(props) {
const dispatch = useDispatch();
const router = useRouter();
const userCredentials = useSelector((state) => state.login);
const [isRedirecting, setIsRedirecting] = React.useState(
router.query.success === 'google',
);

const retryWithDelay = async (fn, retries = MAX_RETRIES) => {
try {
return await fn();
} catch (error) {
if (retries > 0 && error.response?.status === 429) {
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
return retryWithDelay(fn, retries - 1);
}
throw error;
}
};

const setupUserSession = async (user) => {
if (!user.groups[0]?.grp_title) {
throw new Error(
'Server error. Contact support to add you to the AirQo Organisation',
);
}

localStorage.setItem('loggedUser', JSON.stringify(user));

const preferencesResponse = await retryWithDelay(() =>
dispatch(getIndividualUserPreferences({ identifier: user._id })),
);
if (preferencesResponse.payload.success) {
const preferences = preferencesResponse.payload.preferences;
const activeGroup = preferences[0]?.group_id
? user.groups.find((group) => group._id === preferences[0].group_id)
: user.groups.find((group) => group.grp_title === 'airqo');
localStorage.setItem('activeGroup', JSON.stringify(activeGroup));
}

dispatch(setUserInfo(user));
dispatch(setSuccess(true));
};

useEffect(() => {
if (typeof window !== 'undefined') {
// Handle Google redirect first
if (router.query.success === 'google') {
const token = Cookies.get('access_token');
if (token) {
localStorage.setItem('token', token);
const decoded = jwt_decode(token);
retryWithDelay(() => getUserDetails(decoded._id, token))
.then((response) => setupUserSession(response.users[0]))
.catch((error) => {
console.error('Google auth error:', error);
setIsRedirecting(false);
router.push('/account/login');
});
} else {
setIsRedirecting(false);
router.push('/account/login');
}
return; // Exit early to prevent further checks until redirect is resolved
}

const storedUserGroup = localStorage.getItem('activeGroup');

if (!userCredentials.success) {
router.push('/account/login');
}
Expand All @@ -91,12 +21,7 @@ export default function withAuth(Component) {
LogoutUser(dispatch, router);
}
}
}, [userCredentials, dispatch, router, retryWithDelay, isRedirecting]);

// Block rendering until redirect is handled
if (isRedirecting) {
return <Spinner width={20} height={20} />;
}
}, [userCredentials, dispatch, router]);

// Render the component if the user is authenticated
return userCredentials.success ? <Component {...props} /> : null;
Expand Down
93 changes: 47 additions & 46 deletions src/platform/src/pages/account/creation/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,73 @@ import GoogleLogo from '@/icons/Common/google_logo.svg';
import { getGoogleAuthDetails } from '@/core/apis/Account';
import CheckComponent from '@/components/Account/CheckComponent';

const FORM_URL = 'https://forms.gle/VX5p2s65n8U51iBc8';

const userRoles = [
{
title: 'Individual',
subText:
'Empower yourself with real-time Air Pollution Location Data for research and personal use. Stay informed, stay healthy. Join the clean air revolution today.',
disabled: false,
// Internal route for individuals.
route: (router) => router.push(`/account/creation/individual/register`),
},
{
title: 'Organisation',
subText:
'Beyond data, gain access to network management tools. Drive meaningful change, one location at a time. Shape a cleaner future for all.',
disabled: true,
disabled: false,
// External route for organisations.
route: () => {
window.location.href = FORM_URL;
},
},
];

const UserDesignation = () => {
const [clickedRole, setClickedRole] = useState('');
const router = useRouter();

const routeToCreation = () => {
if (clickedRole) {
router.push(`/account/creation/${clickedRole.toLowerCase()}/register`);
}
const handleRoleClick = (roleTitle, disabled) => {
if (disabled) return;
setClickedRole((prevRole) => (prevRole === roleTitle ? '' : roleTitle));
};

const handleRoleClick = (roleTitle) => {
setClickedRole((prevRole) => (prevRole === roleTitle ? '' : roleTitle));
const routeToCreation = () => {
if (!clickedRole) return;
const selectedRole = userRoles.find((role) => role.title === clickedRole);
if (selectedRole && selectedRole.route) {
selectedRole.route(router);
}
};

return (
<div className="relative w-screen h-screen bg-white overflow-x-hidden">
<div className="absolute left-0 right-0 top-0 bottom-0 mb-0 mt-14 sm:mt-20 lg:mt-44 mx-auto w-11/12 lg:w-7/12 h-auto flex flex-col items-center">
<div className="absolute left-0 right-0 top-0 bottom-0 mt-14 sm:mt-20 lg:mt-44 mx-auto w-11/12 lg:w-7/12 flex flex-col items-center">
<h2 className="text-3xl text-black-700 font-semibold text-center">
How are you planning to use AirQo Analytics?
</h2>
<p className="text-xl text-black-700 font-normal mt-3 text-center">
We'll streamline your setup experience accordingly
We&apos;ll streamline your setup experience accordingly
</p>
<div className="mt-10 flex justify-center items-center">
<div className="flex flex-col lg:flex-row lg:items-stretch lg:ml-20">
{userRoles.map((role, index) => (
<div
key={index}
className="w-full cursor-pointer mb-8 lg:w-10/12 lg:mb-0 flex flex-col"
onClick={() => {
if (role.disabled) {
return;
}
handleRoleClick(role.title);
}}
role="button"
tabIndex={0}
onClick={() => handleRoleClick(role.title, role.disabled)}
onKeyUp={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
if (role.disabled) {
return;
}
handleRoleClick(role.title);
handleRoleClick(role.title, role.disabled);
}
}}
>
<CheckComponent
text={role.title}
width={'w-full lg:w-10/12'}
width="w-full lg:w-10/12"
subText={role.subText}
checked={clickedRole === role.title}
disabled={role.disabled}
Expand All @@ -79,39 +84,35 @@ const UserDesignation = () => {
onClick={routeToCreation}
className="mt-6 w-[262px] flex justify-center items-center px-4 py-2 bg-blue-600 text-white rounded-[12px]"
>
Continue
{clickedRole === 'Organisation' ? 'Get started' : 'Continue'}
</button>
)}
</div>
</div>
);
};

const GoogleAccountCreation = () => {
return (
<div className="w-full">
<div className="mt-6 grid grid-cols-3 items-center justify-center">
<span className="w-full border border-grey-200"></span>
<span className="justify-self-center w-fit">Or</span>
<span className="w-full border border-grey-200"></span>
</div>
<div className="mt-6">
<button
className="btn bg-form-input rounded-none w-full outline-none border-none flex flex-row items-center justify-center hover:bg-grey-200"
onClick={() => {
getGoogleAuthDetails();
}}
>
<span style={{ color: '#000000', fontWeight: '400', opacity: '0.5' }}>
Sign up with
</span>
<span className="pl-2">
<GoogleLogo />
</span>
</button>
</div>
const GoogleAccountCreation = () => (
<div className="w-full mt-6">
<div className="grid grid-cols-3 items-center justify-center">
<span className="w-full border border-grey-200"></span>
<span className="justify-self-center w-fit">Or</span>
<span className="w-full border border-grey-200"></span>
</div>
);
};
<div className="mt-6">
<button
className="btn bg-form-input rounded-none w-full outline-none border-none flex flex-row items-center justify-center hover:bg-grey-200"
onClick={getGoogleAuthDetails}
>
<span style={{ color: '#000000', fontWeight: '400', opacity: '0.5' }}>
Sign up with
</span>
<span className="pl-2">
<GoogleLogo />
</span>
</button>
</div>
</div>
);

export default UserDesignation;
24 changes: 0 additions & 24 deletions src/platform/src/pages/account/login/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
} from '@/lib/store/services/account/LoginSlice';
import { getIndividualUserPreferences } from '@/lib/store/services/account/UserDefaultsSlice';
import { postUserLoginDetails, getUserDetails } from '@/core/apis/Account';
import { GOOGLE_AUTH_URL } from '@/core/urls/authentication';

const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;
Expand Down Expand Up @@ -111,15 +110,6 @@ const UserLogin = () => {
dispatch(setUserData({ key, value }));
};

const handleGoogleLogin = async () => {
try {
// Redirect to Google auth URL
window.location.href = GOOGLE_AUTH_URL;
} catch (error) {
console.error('Login error:', error);
}
};

return (
<AccountPageLayout
pageTitle="AirQo Analytics | Login"
Expand Down Expand Up @@ -178,7 +168,6 @@ const UserLogin = () => {
</div>
</div>
</div>

<div className="mt-10">
<button
data-testid="login-btn"
Expand All @@ -192,19 +181,6 @@ const UserLogin = () => {
'Login'
)}
</button>

<button
data-testid="google-login-btn"
className="w-full btn border-blue-900 rounded-[12px] text-white text-sm outline-none border"
disabled={loading}
onClick={handleGoogleLogin}
>
{loading ? (
<Spinner data-testid="spinner" width={25} height={25} />
) : (
'Login with Google'
)}
</button>
</div>
</form>
<div className="mt-8 w-full flex justify-center">
Expand Down
15 changes: 15 additions & 0 deletions src/website2/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ const nextConfig = {
destination: '/clean-air-forum/about',
permanent: true,
},
{
source: '/clean-air-network',
destination: '/clean-air-network/about',
permanent: true,
},
{
source: '/clean-air/about',
destination: '/clean-air-network/about',
permanent: true,
},
{
source: '/clean-air',
destination: '/clean-air-network/about',
permanent: true,
},
];
},
};
Expand Down
Binary file added src/website2/public/Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/website2/public/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/website2/public/assets/icons/Icon1.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// components/icons/Icon1.tsx

import React from 'react';

interface IconProps {
Expand Down
2 changes: 1 addition & 1 deletion src/website2/public/assets/icons/Icon2.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// components/icons/Icon2.tsx

import React from 'react';

interface IconProps {
Expand Down
2 changes: 1 addition & 1 deletion src/website2/public/assets/icons/Icon3.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// components/icons/Icon3.tsx

import React from 'react';

interface IconProps {
Expand Down
2 changes: 1 addition & 1 deletion src/website2/public/assets/icons/Icon4.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// components/icons/Icon4.tsx

import React from 'react';

interface IconProps {
Expand Down
2 changes: 1 addition & 1 deletion src/website2/public/assets/icons/Icon5.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// components/icons/Icon5.tsx

import React from 'react';

interface IconProps {
Expand Down
2 changes: 1 addition & 1 deletion src/website2/public/assets/icons/Icon6.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// components/icons/Icon6.tsx

import React from 'react';

interface IconProps {
Expand Down
2 changes: 1 addition & 1 deletion src/website2/public/assets/icons/Icon7.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// components/icons/Icon6.tsx

import React from 'react';

interface IconProps {
Expand Down
2 changes: 1 addition & 1 deletion src/website2/public/assets/icons/Icon8.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// components/icons/Icon6.tsx

import React from 'react';

interface IconProps {
Expand Down
Binary file added src/website2/public/favicon.ico
Binary file not shown.
Binary file added src/website2/public/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 479cc56

Please sign in to comment.