From 5cfc757932d53d0d130cf260336f2760f739c679 Mon Sep 17 00:00:00 2001 From: Luca8991 Date: Tue, 31 May 2022 23:10:43 +0200 Subject: [PATCH] #17 admin routes and login with Slack --- apps/sumilan-app/src/app/App.tsx | 37 +----- .../src/app/components/Admin/AdminMenu.css | 113 ++++++++++++++++++ .../src/app/components/Admin/AdminMenu.tsx | 105 ++++++++++++++++ apps/sumilan-app/src/app/constants/index.ts | 4 +- apps/sumilan-app/src/app/contexts/Auth.tsx | 47 ++++---- apps/sumilan-app/src/app/models/auth.model.ts | 8 ++ .../sumilan-app/src/app/pages/Admin/Admin.tsx | 13 ++ .../src/app/pages/Admin/AdminLogin.css | 8 ++ .../src/app/pages/Admin/AdminLogin.tsx | 32 +++++ .../src/app/pages/Admin/PrivateRoute.tsx | 26 ++++ apps/sumilan-app/src/app/pages/Routes.tsx | 59 +++++++++ .../sumilan-app/src/app/pages/Start/Start.css | 8 -- .../sumilan-app/src/app/pages/Start/Start.tsx | 112 ----------------- 13 files changed, 399 insertions(+), 173 deletions(-) create mode 100644 apps/sumilan-app/src/app/components/Admin/AdminMenu.css create mode 100644 apps/sumilan-app/src/app/components/Admin/AdminMenu.tsx create mode 100644 apps/sumilan-app/src/app/models/auth.model.ts create mode 100644 apps/sumilan-app/src/app/pages/Admin/Admin.tsx create mode 100644 apps/sumilan-app/src/app/pages/Admin/AdminLogin.css create mode 100644 apps/sumilan-app/src/app/pages/Admin/AdminLogin.tsx create mode 100644 apps/sumilan-app/src/app/pages/Admin/PrivateRoute.tsx create mode 100644 apps/sumilan-app/src/app/pages/Routes.tsx delete mode 100644 apps/sumilan-app/src/app/pages/Start/Start.css delete mode 100644 apps/sumilan-app/src/app/pages/Start/Start.tsx diff --git a/apps/sumilan-app/src/app/App.tsx b/apps/sumilan-app/src/app/App.tsx index 51e49d3..0ef5142 100644 --- a/apps/sumilan-app/src/app/App.tsx +++ b/apps/sumilan-app/src/app/App.tsx @@ -1,6 +1,5 @@ -import { IonApp, IonRouterOutlet, IonSplitPane } from '@ionic/react'; +import { IonApp } from '@ionic/react'; import { IonReactRouter } from '@ionic/react-router'; -import { Redirect, Route } from 'react-router-dom'; import i18n from "i18next"; import LanguageDetector from 'i18next-browser-languagedetector'; import { initReactI18next } from "react-i18next"; @@ -24,18 +23,12 @@ import '@ionic/react/css/display.css'; /* Theme variables */ import './theme/variables.css'; -import Menu from './components/Menu'; -import Home from './pages/Home/Home'; -import Event from './pages/Event/Event'; -import Young from './pages/Young/Young'; -import Chapter from './pages/Chapter/Chapter'; -import Activist from './pages/Activist/Activits'; -import Certificates from './pages/Certificates/Certificates'; - import en from '../assets/i18n/en.json'; import it from '../assets/i18n/it.json'; import { getAppVersion } from './utils/version'; import { loadGA } from './libs/ga'; +import Routes from './pages/Routes'; +import { AuthProvider } from './contexts/Auth'; i18n .use(LanguageDetector) @@ -64,30 +57,12 @@ loadGA(process.env['NX_GOOGLE_ANALYTICS_ID'] as string); const App: React.FC = () => { - const homeRouter = ( - - - - - - - - - - - {/* */} - - - - } /> - - - ); - return ( - {homeRouter} + + + ); diff --git a/apps/sumilan-app/src/app/components/Admin/AdminMenu.css b/apps/sumilan-app/src/app/components/Admin/AdminMenu.css new file mode 100644 index 0000000..0ca47a2 --- /dev/null +++ b/apps/sumilan-app/src/app/components/Admin/AdminMenu.css @@ -0,0 +1,113 @@ +ion-menu ion-content { + --background: var(--ion-item-background, var(--ion-background-color, #fff)); +} + +ion-menu.md ion-content { + --padding-start: 8px; + --padding-end: 8px; + --padding-top: 20px; + --padding-bottom: 20px; +} + +ion-menu.md ion-list { + padding: 20px 0; +} + +ion-menu.md ion-note { + margin-bottom: 30px; +} + +ion-menu.md ion-list-header, ion-menu.md ion-note { + padding-left: 10px; +} + +ion-menu.md ion-list#inbox-list { + border-bottom: 1px solid var(--ion-color-step-150, #d7d8da); +} + +ion-menu.md ion-list#inbox-list ion-list-header { + font-size: 22px; + font-weight: 600; + min-height: 20px; +} + +ion-menu.md ion-list#labels-list ion-list-header { + font-size: 16px; + margin-bottom: 18px; + color: #757575; + min-height: 26px; +} + +ion-menu.md ion-item { + --padding-start: 10px; + --padding-end: 10px; + border-radius: 4px; +} + +ion-menu.md ion-item.selected { + --background: rgba(var(--ion-color-primary-rgb), 0.14); +} + +ion-menu.md ion-item.selected ion-icon { + color: var(--ion-color-primary); +} + +ion-menu.md ion-item ion-icon { + color: #616e7e; +} + +ion-menu.md ion-item ion-label { + font-weight: 500; +} + +ion-menu.ios ion-content { + --padding-bottom: 20px; +} + +ion-menu.ios ion-list { + padding: 20px 0 0 0; +} + +ion-menu.ios ion-note { + line-height: 24px; + margin-bottom: 20px; +} + +ion-menu.ios ion-item { + --padding-start: 16px; + --padding-end: 16px; + --min-height: 50px; +} + +ion-menu.ios ion-item ion-icon { + font-size: 24px; + color: #73849a; +} + +ion-menu.ios ion-item .selected ion-icon { + color: var(--ion-color-primary); +} + +ion-menu.ios ion-list#labels-list ion-list-header { + margin-bottom: 8px; +} + +ion-menu.ios ion-list-header, +ion-menu.ios ion-note { + padding-left: 16px; + padding-right: 16px; +} + +ion-menu.ios ion-note { + margin-bottom: 8px; +} + +ion-note { + display: inline-block; + font-size: 16px; + color: var(--ion-color-medium-shade); +} + +ion-item.selected { + --color: var(--ion-color-primary); +} \ No newline at end of file diff --git a/apps/sumilan-app/src/app/components/Admin/AdminMenu.tsx b/apps/sumilan-app/src/app/components/Admin/AdminMenu.tsx new file mode 100644 index 0000000..8594189 --- /dev/null +++ b/apps/sumilan-app/src/app/components/Admin/AdminMenu.tsx @@ -0,0 +1,105 @@ +import { useRef } from 'react'; +import { + IonAvatar, + IonButton, + IonContent, + IonFooter, + IonIcon, + IonItem, + IonLabel, + IonList, + IonMenu, + IonMenuToggle, +} from '@ionic/react'; +import { useLocation } from 'react-router-dom'; +import { help, logOutOutline } from 'ionicons/icons'; +import { useTranslation } from 'react-i18next'; + +import { openPreferenceCenter } from '../../libs/avacy'; +import { getAppVersion } from '../../utils/version'; +import { useAuth } from '../../contexts/Auth'; +import { AuthContextModel } from '../../models/auth.model'; + +import './AdminMenu.css'; + +interface AppPage { + url: string; + iosIcon: string; + mdIcon: string; +} + +const appPages: AppPage[] = [ + { + url: 'questions', + iosIcon: help, + mdIcon: help + }, +]; + +const AdminMenu: React.FC = () => { + const menuRef = useRef(null); + const location = useLocation(); + const { user, signOut } = useAuth() as AuthContextModel; + const { t } = useTranslation(); + + const handleCookieClick = () => { + openPreferenceCenter(); + menuRef.current?.close(); + }; + + const handleLogoutClick = () => { + const wantsToLogout = window.confirm("Do you want to logout?"); + if (wantsToLogout) { + signOut(); + } + }; + + return ( + + + + + {user && ( + + + Profile avatar + + {user.user_metadata['name']} + + + + + )} + + {appPages.map((appPage, index) => { + return ( + + + + {t(appPage.url.toUpperCase() + '.title')} + + + ); + })} + + + + + + + +

{t("COOKIES.preferences")}

+
+
+ + +

Version: {getAppVersion()}

+
+
+
+
+
+ ); +}; + +export default AdminMenu; diff --git a/apps/sumilan-app/src/app/constants/index.ts b/apps/sumilan-app/src/app/constants/index.ts index a8a4aa2..7af8dcf 100644 --- a/apps/sumilan-app/src/app/constants/index.ts +++ b/apps/sumilan-app/src/app/constants/index.ts @@ -23,4 +23,6 @@ export const PROGRESS_STEPS = [ id: 5, progress: 1, }, -]; \ No newline at end of file +]; + +export const ADMIN_DASHBOARD_BASE_PATH = '/admin'; \ No newline at end of file diff --git a/apps/sumilan-app/src/app/contexts/Auth.tsx b/apps/sumilan-app/src/app/contexts/Auth.tsx index 50a69e3..98a954d 100644 --- a/apps/sumilan-app/src/app/contexts/Auth.tsx +++ b/apps/sumilan-app/src/app/contexts/Auth.tsx @@ -1,50 +1,55 @@ import { useContext, useState, useEffect, createContext } from 'react'; -import { User, UserCredentials } from '@supabase/supabase-js'; +import { User } from '@supabase/supabase-js'; import { supabase } from '../libs/supabase'; +import { AuthContextModel } from '../models/auth.model'; +import { ADMIN_DASHBOARD_BASE_PATH } from '../constants'; -const AuthContext = createContext(null); +const AuthContext = createContext(null); export const AuthProvider: React.FC = ({ children }) => { - const [user, setUser] = useState() - const [loading, setLoading] = useState(true) + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); useEffect(() => { // Check active sessions and sets the user - const session = supabase.auth.session() + const session = supabase.auth.session(); - setUser(session?.user ?? null) - setLoading(false) + setUser(session?.user ?? null); + setLoading(false); // Listen for changes on auth state (logged in, signed out, etc.) const { data: listener } = supabase.auth.onAuthStateChange( - async (event, session) => { - setUser(session?.user ?? null) - setLoading(false) + async (_event, session) => { + setUser(session?.user ?? null); + setLoading(false); } - ) + ); return () => { - listener?.unsubscribe() - } - }, []) + listener?.unsubscribe(); + }; + }, []); - // Will be passed down to Signup, Login and Dashboard components + // Will be passed down to components const value = { - signUp: (data: UserCredentials) => supabase.auth.signUp(data), - signIn: (data: UserCredentials) => supabase.auth.signIn(data), + signIn: () => supabase.auth.signIn({ + provider: 'slack', + }, { + redirectTo: `${window.location.origin}${ADMIN_DASHBOARD_BASE_PATH}` + }), signOut: () => supabase.auth.signOut(), user, loading, - } + }; return ( - {!loading && children} + {children} - ) + ); }; export const useAuth = () => { return useContext(AuthContext); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/apps/sumilan-app/src/app/models/auth.model.ts b/apps/sumilan-app/src/app/models/auth.model.ts new file mode 100644 index 0000000..01ba6e5 --- /dev/null +++ b/apps/sumilan-app/src/app/models/auth.model.ts @@ -0,0 +1,8 @@ +import { User, GoTrueClient } from "@supabase/supabase-js"; + +export interface AuthContextModel { + signIn: () => ReturnType; + signOut: () => ReturnType; + user: User | null; + loading: boolean; +}; \ No newline at end of file diff --git a/apps/sumilan-app/src/app/pages/Admin/Admin.tsx b/apps/sumilan-app/src/app/pages/Admin/Admin.tsx new file mode 100644 index 0000000..5bf7bfe --- /dev/null +++ b/apps/sumilan-app/src/app/pages/Admin/Admin.tsx @@ -0,0 +1,13 @@ +import { IonContent, IonPage } from "@ionic/react"; + +const Admin = () => { + return ( + + + admin content + + + ); +}; + +export default Admin; \ No newline at end of file diff --git a/apps/sumilan-app/src/app/pages/Admin/AdminLogin.css b/apps/sumilan-app/src/app/pages/Admin/AdminLogin.css new file mode 100644 index 0000000..0e6b10a --- /dev/null +++ b/apps/sumilan-app/src/app/pages/Admin/AdminLogin.css @@ -0,0 +1,8 @@ +.admin-login-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/apps/sumilan-app/src/app/pages/Admin/AdminLogin.tsx b/apps/sumilan-app/src/app/pages/Admin/AdminLogin.tsx new file mode 100644 index 0000000..bad2fe5 --- /dev/null +++ b/apps/sumilan-app/src/app/pages/Admin/AdminLogin.tsx @@ -0,0 +1,32 @@ +import { IonPage, IonContent, IonButton, IonIcon } from "@ionic/react"; +import { logoSlack } from "ionicons/icons"; +import { Redirect } from "react-router-dom"; + +import { useAuth } from "../../contexts/Auth"; +import { AuthContextModel } from "../../models/auth.model"; +import { ADMIN_DASHBOARD_BASE_PATH } from "../../constants"; + +import "./AdminLogin.css"; + +const AdminLogin = () => { + const { user, signIn } = useAuth() as AuthContextModel; + + if (user) { + return ; + } + + return ( + + +
+ + + Login with Slack + +
+
+
+ ); +}; + +export default AdminLogin; \ No newline at end of file diff --git a/apps/sumilan-app/src/app/pages/Admin/PrivateRoute.tsx b/apps/sumilan-app/src/app/pages/Admin/PrivateRoute.tsx new file mode 100644 index 0000000..cb6e96c --- /dev/null +++ b/apps/sumilan-app/src/app/pages/Admin/PrivateRoute.tsx @@ -0,0 +1,26 @@ +import { Redirect, Route, RouteProps, useRouteMatch } from "react-router-dom"; + +import { useAuth } from "../../contexts/Auth"; +import { AuthContextModel } from "../../models/auth.model"; + +const PrivateRoute: React.FC = ({ children, ...rest }) => { + const { user } = useAuth() as AuthContextModel; + const match = useRouteMatch(); + + return ( + + user + ? children + : ( + + ) + } + /> + ); +}; + +export default PrivateRoute; \ No newline at end of file diff --git a/apps/sumilan-app/src/app/pages/Routes.tsx b/apps/sumilan-app/src/app/pages/Routes.tsx new file mode 100644 index 0000000..a0ec113 --- /dev/null +++ b/apps/sumilan-app/src/app/pages/Routes.tsx @@ -0,0 +1,59 @@ +import { IonSplitPane, IonRouterOutlet } from "@ionic/react"; +import { Redirect, Route, useLocation, useRouteMatch } from 'react-router-dom'; + +import Menu from "../components/Menu"; +import Activist from "./Activist/Activits"; +import Certificates from "./Certificates/Certificates"; +import Chapter from "./Chapter/Chapter"; +import Home from "./Home/Home"; +import Event from "./Event/Event"; +import Admin from "./Admin/Admin"; +import AdminMenu from "../components/Admin/AdminMenu"; +import { ADMIN_DASHBOARD_BASE_PATH } from "../constants"; +import { useAuth } from "../contexts/Auth"; +import { AuthContextModel } from "../models/auth.model"; +import AdminLogin from "./Admin/AdminLogin"; +import PrivateRoute from "./Admin/PrivateRoute"; + +const AdminRouter = () => { + const match = useRouteMatch(); + + return ( + + + + + + } /> + + ); +}; + +const Routes = () => { + const location = useLocation(); + const { user } = useAuth() as AuthContextModel; + + return ( + + {location.pathname.startsWith(ADMIN_DASHBOARD_BASE_PATH) + ? (user ? : null) + : + } + + + + + + {/* redirect to home if going only to /event path */} + } /> + {/* handle event tabs */} + } /> + {/* fallback route */} + + } /> + + + ); +}; + +export default Routes; \ No newline at end of file diff --git a/apps/sumilan-app/src/app/pages/Start/Start.css b/apps/sumilan-app/src/app/pages/Start/Start.css deleted file mode 100644 index 05376e0..0000000 --- a/apps/sumilan-app/src/app/pages/Start/Start.css +++ /dev/null @@ -1,8 +0,0 @@ -.center-container { - text-align: center; - position: absolute; - left: 0; - right: 0; - top: 50%; - transform: translateY(-50%); -} diff --git a/apps/sumilan-app/src/app/pages/Start/Start.tsx b/apps/sumilan-app/src/app/pages/Start/Start.tsx deleted file mode 100644 index 2a12ad7..0000000 --- a/apps/sumilan-app/src/app/pages/Start/Start.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { - IonButton, - IonCol, - IonContent, - IonGrid, - IonPage, - IonRow, - IonSpinner, - IonText, - useIonAlert, -} from "@ionic/react"; -import './Start.css'; -import { useState } from "react"; -import { RouteComponentProps } from "react-router"; -import LogoImage from "../../components/LogoImage/LogoImage"; -import { useAuth } from "../../contexts/Auth"; -import { object, string } from 'yup'; -import { useForm } from "react-hook-form"; -import { yupResolver } from '@hookform/resolvers/yup'; -import InputComponent from "../../components/Input/InputComponent"; - -const Start: React.FC = (props: RouteComponentProps) => { - - const validationSchema = object().shape({ - email: string().required('Inserisci un\'email').email('Inserisci un\'email valida'), - }); - const { control, handleSubmit, formState: { errors } } = useForm({ resolver: yupResolver(validationSchema) }); - const { signIn } = useAuth(); - const [present] = useIonAlert(); - const [magicLinkSent, setMagicLinkSent] = useState(false); - const [displayEmail, setDisplayEmail] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const loginInUser = async (data: any) => { - const { email } = data; - setIsLoading(true); - const { error } = await signIn({ email }); - setIsLoading(false); - - if (error) { - present({ - header: 'Errore', - message: `${error.message}`, - buttons: [ - 'Ok' - ], - }); - } else { - setDisplayEmail(email); - setMagicLinkSent(true); - } - }; - - const resendEmailHandler = () => { - setMagicLinkSent(false); - setDisplayEmail(''); - }; - - const content = isLoading ? - ( - - ) : - magicLinkSent ? - ( - <> - -

Effettua il login cliccando sul link che ti abbiamo inviato all'email:
{displayEmail}

-
- - Email non ricevuta? - - - ) - : - ( -
- - < br /> - - Inizia - - - ); - - return ( - - -
- - - - - - {content} - - - - -
-
-
- ); -}; - -export default Start; \ No newline at end of file