diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..4ced3eb 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, {useEffect, useRef, useState} from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' @@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) { } return
{NavBarItemComponentContents}
-} +} \ No newline at end of file diff --git a/frontend/src/config.ts b/frontend/src/config.ts index a9783c8..3adcea7 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -8,8 +8,8 @@ export const localStorageStyleKey = 'style' export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20' -export const appTitle = 'created by Flatlogic generator!' +export const appTitle = 'The Power Suite Executive Directory' export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}` -export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || '' +export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || '' \ No newline at end of file diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..bbc840e 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' @@ -63,6 +62,19 @@ export default function LayoutAuthenticated({ if (!hasPermission(currentUser, permission)) router.push('/error'); }, [currentUser, permission]); + + // Mandatory Opt-In Gating + useEffect(() => { + if (!currentUser) return; + + const memberProfile = currentUser?.member_profiles_user?.[0] || currentUser?.member_profiles_user; + const isOptInPage = router.pathname === '/profile/opt-in'; + + // If user is a member but hasn't consented, redirect to opt-in + if (memberProfile && !memberProfile.directory_consent && !isOptInPage) { + router.push('/profile/opt-in'); + } + }, [currentUser, router.pathname]); const darkMode = useAppSelector((state) => state.style.darkMode) @@ -122,7 +134,7 @@ export default function LayoutAuthenticated({ onAsideLgClose={() => setIsAsideLgActive(false)} /> {children} - Hand-crafted & Made with ❤️ + Power Suite Executive Directory © 2026 ) diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index d4d027c..4325420 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -3,89 +3,70 @@ import { MenuAsideItem } from './interfaces' const menuAside: MenuAsideItem[] = [ { - href: '/dashboard', - icon: icon.mdiViewDashboardOutline, - label: 'Dashboard', - }, - - { - href: '/users/users-list', - label: 'Users', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiAccountGroup ?? icon.mdiTable, - permissions: 'READ_USERS' - }, - { - href: '/roles/roles-list', - label: 'Roles', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable, - permissions: 'READ_ROLES' - }, - { - href: '/permissions/permissions-list', - label: 'Permissions', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, - permissions: 'READ_PERMISSIONS' + href: '/', + icon: icon.mdiHomeOutline, + label: 'Home', }, { href: '/member_profiles/member_profiles-list', - label: 'Member profiles', + label: 'Executive Directory', // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - icon: 'mdiBadgeAccountOutline' in icon ? icon['mdiBadgeAccountOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, + icon: icon.mdiCardAccountDetailsOutline ?? icon.mdiTable, permissions: 'READ_MEMBER_PROFILES' }, - { - href: '/expertise_areas/expertise_areas-list', - label: 'Expertise areas', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiLightbulbOnOutline' in icon ? icon['mdiLightbulbOnOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_EXPERTISE_AREAS' - }, - { - href: '/member_import_batches/member_import_batches-list', - label: 'Member import batches', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiFileUploadOutline' in icon ? icon['mdiFileUploadOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_MEMBER_IMPORT_BATCHES' - }, - { - href: '/email_reminders/email_reminders-list', - label: 'Email reminders', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiEmailOutline' in icon ? icon['mdiEmailOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_EMAIL_REMINDERS' - }, - { - href: '/audit_events/audit_events-list', - label: 'Audit events', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: 'mdiClipboardTextOutline' in icon ? icon['mdiClipboardTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_AUDIT_EVENTS' - }, { href: '/profile', - label: 'Profile', + label: 'My Profile', icon: icon.mdiAccountCircle, }, - - { - href: '/api-docs', - target: '_blank', - label: 'Swagger API', - icon: icon.mdiFileCode, - permissions: 'READ_API_DOCS' + label: 'Administration', + icon: icon.mdiShieldEditOutline, + permissions: 'READ_USERS', // Only show to users who can read users (Admins) + menu: [ + { + href: '/dashboard', + icon: icon.mdiChartTimelineVariant, + label: 'Analytics', + }, + { + href: '/users/users-list', + label: 'Member Accounts', + icon: icon.mdiAccountGroup, + }, + { + href: '/member_import_batches/member_import_batches-list', + label: 'CSV Imports', + icon: icon.mdiFileUploadOutline, + }, + { + href: '/expertise_areas/expertise_areas-list', + label: 'Expertise Areas', + icon: icon.mdiLightbulbOnOutline, + }, + { + href: '/email_reminders/email_reminders-list', + label: 'System Emails', + icon: icon.mdiEmailOutline, + }, + { + href: '/audit_events/audit_events-list', + label: 'Audit Log', + icon: icon.mdiClipboardTextOutline, + }, + { + href: '/roles/roles-list', + label: 'Roles', + icon: icon.mdiShieldAccountVariantOutline, + }, + { + href: '/permissions/permissions-list', + label: 'Permissions', + icon: icon.mdiShieldAccountOutline, + }, + ] }, ] -export default menuAside +export default menuAside \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index f9196da..dd261ac 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,94 @@ - -import React, { useEffect, useState } from 'react'; -import type { ReactElement } from 'react'; +import React, { ReactElement } from 'react'; import Head from 'next/head'; import Link from 'next/link'; -import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; +import { useRouter } from 'next/router'; +import { useAppSelector, useAppDispatch } from '../stores/hooks'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; - +import BaseButton from '../components/BaseButton'; +import { logoutUser } from '../stores/authSlice'; export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('background'); - const textColor = useAppSelector((state) => state.style.linkColor); + const router = useRouter(); + const dispatch = useAppDispatch(); + const { currentUser, token } = useAppSelector((state) => state.auth); + + const handleLogout = () => { + dispatch(logoutUser()); + router.push('/login'); + }; - const title = 'Power Suite Executive Directory' - - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - - const imageBlock = (image) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
+ // If not authenticated, show a minimal landing with Login CTA + if (!token) { + return ( +
+ + {getPageTitle('The Power Suite')} + +
+

+ Zetas in Corporate America: The Power Suite +

+

+ A private executive directory connecting Power Suite members across industries, regions, and leadership levels. +

+
+ +
+
); + } - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; - + // Option C Landing Page for Authenticated Users return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('Home')} +
+
+

+ Zetas in Corporate America: The Power Suite +

+

+ A private executive directory connecting Power Suite members across industries, regions, and leadership levels. +

+
- -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
+
+ + +
+ + Update My Profile + + + Reconfirm My Profile + + +
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
-
); } Starter.getLayout = function getLayout(page: ReactElement) { return {page}; -}; - +}; \ No newline at end of file diff --git a/frontend/src/pages/profile/opt-in.tsx b/frontend/src/pages/profile/opt-in.tsx new file mode 100644 index 0000000..10752cf --- /dev/null +++ b/frontend/src/pages/profile/opt-in.tsx @@ -0,0 +1,116 @@ +import React, { ReactElement, useState, useEffect } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { useAppDispatch, useAppSelector } from '../../stores/hooks'; +import LayoutAuthenticated from '../../layouts/Authenticated'; +import { getPageTitle } from '../../config'; +import CardBox from '../../components/CardBox'; +import SectionMain from '../../components/SectionMain'; +import BaseButton from '../../components/BaseButton'; +import { findMe } from '../../stores/authSlice'; +import axios from 'axios'; +import { toast } from 'react-toastify'; + +export default function ProfileOptIn() { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { currentUser } = useAppSelector((state) => state.auth); + const [consent, setConsent] = useState(false); + const [loading, setLoading] = useState(false); + + // Get the member profile ID. It might be in member_profiles_user array or object depending on associations + const memberProfile = currentUser?.member_profiles_user?.[0] || currentUser?.member_profiles_user; + + useEffect(() => { + if (memberProfile?.directory_consent) { + router.push('/'); + } + }, [memberProfile, router]); + + const handleConsent = async () => { + if (!consent) { + toast.error('You must provide consent to proceed.'); + return; + } + + setLoading(true); + try { + // Update member profile with consent + await axios.put(`/member_profiles/${memberProfile.id}`, { + data: { + directory_consent: true, + consented_at: new Date(), + profile_status: 'draft' // Initial status after consent + } + }); + + await dispatch(findMe()); + toast.success('Consent recorded. Please complete your profile.'); + router.push('/member_profiles/member_profiles-form'); + } catch (error) { + console.error(error); + toast.error('Failed to save consent. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( + <> + + {getPageTitle('Directory Consent')} + + +
+ +
+
+

Private Directory Consent

+

+ Welcome to Zetas in Corporate America: The Power Suite. To access and be visible in our private executive directory, we require your explicit consent. +

+
+ +
+

Mandatory Disclosure

+

+ I understand this directory is visible only to approved Power Suite members and I consent to having my professional information displayed. +

+

+ Your profile will remain in a "Draft" status and will not be searchable by other members until you complete all required fields, including your headshot and LinkedIn URL. +

+
+ +
+ setConsent(e.target.checked)} + /> + +
+ +
+ +
+
+
+
+
+ + ); +} + +ProfileOptIn.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; \ No newline at end of file