diff --git a/package-lock.json b/package-lock.json index d04b7c094..69331046c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ant-design/icons": "^4.7.0", "@ant-design/pro-form": "^1.52.13", "@ant-design/pro-table": "^2.62.7", + "@auth0/auth0-react": "^2.2.4", "@craco/craco": "^6.4.3", "@material-ui/core": "^4.12.4", "@reduxjs/toolkit": "^1.8.2", @@ -245,6 +246,23 @@ "react": ">=16.9.0" } }, + "node_modules/@auth0/auth0-react": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-2.2.4.tgz", + "integrity": "sha512-l29PQC0WdgkCoOc6WeMAY26gsy/yXJICW0jHfj0nz8rZZphYKrLNqTRWFFCMJY+sagza9tSgB1kG/UvQYgGh9A==", + "dependencies": { + "@auth0/auth0-spa-js": "^2.1.3" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17 || ^18", + "react-dom": "^16.11.0 || ^17 || ^18" + } + }, + "node_modules/@auth0/auth0-spa-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-2.1.3.tgz", + "integrity": "sha512-NMTBNuuG4g3rame1aCnNS5qFYIzsTUV5qTFPRfTyYFS1feS6jsCBR+eTq9YkxCp1yuoM2UIcjunPaoPl77U9xQ==" + }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -30233,6 +30251,19 @@ "resize-observer-polyfill": "^1.5.1" } }, + "@auth0/auth0-react": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-2.2.4.tgz", + "integrity": "sha512-l29PQC0WdgkCoOc6WeMAY26gsy/yXJICW0jHfj0nz8rZZphYKrLNqTRWFFCMJY+sagza9tSgB1kG/UvQYgGh9A==", + "requires": { + "@auth0/auth0-spa-js": "^2.1.3" + } + }, + "@auth0/auth0-spa-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-2.1.3.tgz", + "integrity": "sha512-NMTBNuuG4g3rame1aCnNS5qFYIzsTUV5qTFPRfTyYFS1feS6jsCBR+eTq9YkxCp1yuoM2UIcjunPaoPl77U9xQ==" + }, "@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", diff --git a/package.json b/package.json index 284588c52..325790713 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@ant-design/icons": "^4.7.0", "@ant-design/pro-form": "^1.52.13", "@ant-design/pro-table": "^2.62.7", + "@auth0/auth0-react": "^2.2.4", "@craco/craco": "^6.4.3", "@material-ui/core": "^4.12.4", "@reduxjs/toolkit": "^1.8.2", diff --git a/src/auth0-provider-with-history.js b/src/auth0-provider-with-history.js new file mode 100644 index 000000000..b4575683e --- /dev/null +++ b/src/auth0-provider-with-history.js @@ -0,0 +1,32 @@ +import { Auth0Provider } from '@auth0/auth0-react'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; + +export const Auth0ProviderWithHistory = ({ children }) => { + const history = useHistory(); + + const domain = process.env.REACT_APP_AUTH0_DOMAIN; + const clientId = process.env.REACT_APP_AUTH0_CLIENT_ID; + const redirectUri = process.env.REACT_APP_AUTH0_CALLBACK_URL; + + const onRedirectCallback = appState => { + history.push(appState?.returnTo || window.location.pathname); + }; + + if (!(domain && clientId)) { + return null; + } + + return ( + + {children} + + ); +}; diff --git a/src/callback-page.js b/src/callback-page.js new file mode 100644 index 000000000..12163da8d --- /dev/null +++ b/src/callback-page.js @@ -0,0 +1,12 @@ +import React from 'react'; +// import { NavBar } from "../components/navigation/desktop/nav-bar"; +// import { MobileNavBar } from "../components/navigation/mobile/mobile-nav-bar"; + +export const CallbackPage = () => { + return ( +
+

CALLBACK PAGE

+
+
+ ); +}; diff --git a/src/components/Layout/Header.jsx b/src/components/Layout/Header.jsx index 3346cbfe4..0cee057a5 100644 --- a/src/components/Layout/Header.jsx +++ b/src/components/Layout/Header.jsx @@ -1,12 +1,18 @@ +import { useAuth0 } from '@auth0/auth0-react'; import React from 'react'; import { Image } from 'antd'; import { Link } from 'react-router-dom'; import Logo from '../../styles/Images/WhiteLogo.png'; import { colors } from '../../styles/data_vis_colors'; +import { LoginButton } from '../common/login-button'; +import { LogoutButton } from '../common/logout-button'; +import { SignupButton } from '../common/signup-button'; const { primary_accent_color } = colors; function HeaderContent() { + const { isAuthenticated } = useAuth0(); + return (
Home - + Graphs + {!isAuthenticated && ( + <> + + + + )} + {isAuthenticated && ( + <> + + Profile + + + + )}
); diff --git a/src/components/common/login-button.js b/src/components/common/login-button.js new file mode 100644 index 000000000..088c6a4fc --- /dev/null +++ b/src/components/common/login-button.js @@ -0,0 +1,32 @@ +import { useAuth0 } from '@auth0/auth0-react'; +import React from 'react'; + +export const LoginButton = () => { + const { loginWithRedirect } = useAuth0(); + + const handleLogin = async () => { + await loginWithRedirect({ + appState: { + returnTo: '/profile', + }, + authorizationParams: { + screen_hint: 'signup', + }, + }); + }; + + return ( + + ); +}; diff --git a/src/components/common/logout-button.js b/src/components/common/logout-button.js new file mode 100644 index 000000000..f6520823b --- /dev/null +++ b/src/components/common/logout-button.js @@ -0,0 +1,29 @@ +import { useAuth0 } from '@auth0/auth0-react'; +import React from 'react'; + +export const LogoutButton = () => { + const { logout } = useAuth0(); + + const handleLogout = () => { + logout({ + logoutParams: { + returnTo: window.location.origin, + }, + }); + }; + + return ( + + ); +}; diff --git a/src/components/common/signup-button.js b/src/components/common/signup-button.js new file mode 100644 index 000000000..66484376c --- /dev/null +++ b/src/components/common/signup-button.js @@ -0,0 +1,33 @@ +import { useAuth0 } from '@auth0/auth0-react'; +import React from 'react'; + +export const SignupButton = () => { + const { loginWithRedirect } = useAuth0(); + + const handleSignUp = async () => { + await loginWithRedirect({ + appState: { + returnTo: '/profile', + }, + authorizationParams: { + screen_hint: 'signup', + }, + }); + }; + + return ( + + ); +}; diff --git a/src/components/pages/DataVisualizations/GraphWrapper.jsx b/src/components/pages/DataVisualizations/GraphWrapper.jsx index 2d25a26e8..c082f4d92 100644 --- a/src/components/pages/DataVisualizations/GraphWrapper.jsx +++ b/src/components/pages/DataVisualizations/GraphWrapper.jsx @@ -10,7 +10,7 @@ import YearLimitsSelect from './YearLimitsSelect'; import ViewSelect from './ViewSelect'; import axios from 'axios'; import { resetVisualizationQuery } from '../../../state/actionCreators'; -import test_data from '../../../data/test_data.json'; +//import test_data from '../../../data/test_data.json'; import { colors } from '../../../styles/data_vis_colors'; import ScrollToTopOnMount from '../../../utils/scrollToTopOnMount'; @@ -19,10 +19,12 @@ const { background_color } = colors; function GraphWrapper(props) { const { set_view, dispatch } = props; let { office, view } = useParams(); + if (!view) { set_view('time-series'); view = 'time-series'; } + let map_to_render; if (!office) { switch (view) { @@ -50,6 +52,7 @@ function GraphWrapper(props) { break; } } + function updateStateWithNewData(years, view, office, stateSettingCallback) { /* _ _ @@ -73,9 +76,15 @@ function GraphWrapper(props) { */ + // updated test data to API data (SR 3/16/24) + const baseURL = `https://hrf-asylum-be-b.herokuapp.com/cases`; + let endpoint = + view === 'citizenship' ? '/citizenshipSummary' : '/fiscalSummary'; + if (office === 'all' || !office) { axios - .get(process.env.REACT_APP_API_URI, { + //.get(process.env.REACT_APP_API_URI, { + .get(`${baseURL}${endpoint}`, { // mock URL, can be simply replaced by `${Real_Production_URL}/summary` in prod! params: { from: years[0], @@ -83,15 +92,21 @@ function GraphWrapper(props) { }, }) .then(result => { - stateSettingCallback(view, office, test_data); // <-- `test_data` here can be simply replaced by `result.data` in prod! + //stateSettingCallback(view, office, view === 'citizenship' ? result.data : [result.data]); // <-- `test_data` here can be simply replaced by `result.data` in prod! + stateSettingCallback( + view, + office, + view === 'citizenship' ? result.data : [result.data] + ); }) .catch(err => { console.error(err); }); } else { axios - .get(process.env.REACT_APP_API_URI, { - // mock URL, can be simply replaced by `${Real_Production_URL}/summary` in prod! + //.get(process.env.REACT_APP_API_URI, { + // mock URL, can be simply replaced by `${Real_Production_URL}/summary` in prod! + .get(`${baseURL}${endpoint}`, { params: { from: years[0], to: years[1], @@ -99,7 +114,11 @@ function GraphWrapper(props) { }, }) .then(result => { - stateSettingCallback(view, office, test_data); // <-- `test_data` here can be simply replaced by `result.data` in prod! + stateSettingCallback( + view, + office, + view === 'citizenship' ? result.data : [result.data] + ); // <-- `test_data` here can be simply replaced by `result.data` in prod! }) .catch(err => { console.error(err); diff --git a/src/components/pages/DataVisualizations/Graphs/CitizenshipMapAll.jsx b/src/components/pages/DataVisualizations/Graphs/CitizenshipMapAll.jsx index a94c202a9..67483827c 100644 --- a/src/components/pages/DataVisualizations/Graphs/CitizenshipMapAll.jsx +++ b/src/components/pages/DataVisualizations/Graphs/CitizenshipMapAll.jsx @@ -58,7 +58,7 @@ function CitizenshipMapAll(props) { 'Citizenship', 'Total Cases', '% Granted', - '% Admin Close / Dismissal', + '% Admin Closed / Dismissal', '% Denied', ]; @@ -115,8 +115,12 @@ function CitizenshipMapAll(props) { />

Table view

diff --git a/src/components/pages/DataVisualizations/Graphs/CitizenshipMapSingleOffice.jsx b/src/components/pages/DataVisualizations/Graphs/CitizenshipMapSingleOffice.jsx index 18305f99e..bed68a48c 100644 --- a/src/components/pages/DataVisualizations/Graphs/CitizenshipMapSingleOffice.jsx +++ b/src/components/pages/DataVisualizations/Graphs/CitizenshipMapSingleOffice.jsx @@ -57,7 +57,7 @@ function CitizenshipMapSingleOffice(props) { 'Citizenship', 'Total Cases', '% Granted', - '% Admin Close / Dismissal', + '% Admin Closed / Dismissal', '% Denied', ]; return ( @@ -107,10 +107,14 @@ function CitizenshipMapSingleOffice(props) { }} style={{ width: '100%', fontWeight: '900' }} /> - +

Table view

diff --git a/src/components/pages/DataVisualizations/Graphs/TableComponents/TableRow.jsx b/src/components/pages/DataVisualizations/Graphs/TableComponents/TableRow.jsx index 2ede5a306..066d70cc8 100644 --- a/src/components/pages/DataVisualizations/Graphs/TableComponents/TableRow.jsx +++ b/src/components/pages/DataVisualizations/Graphs/TableComponents/TableRow.jsx @@ -17,25 +17,26 @@ function TableRow(props) { }} > {columns.map((property, idx) => { - if (row) { - if (typeof row[property] === 'object') { - return ( - + ); + } else { + return ( +
+ - ); - } else { - return ( -
- -
- ); - } +
+ ); } })} diff --git a/src/components/pages/DataVisualizations/GraphsContainer.jsx b/src/components/pages/DataVisualizations/GraphsContainer.jsx index 9dc39a163..4715a8979 100644 --- a/src/components/pages/DataVisualizations/GraphsContainer.jsx +++ b/src/components/pages/DataVisualizations/GraphsContainer.jsx @@ -26,17 +26,17 @@ function GraphsContainer() { 'New Orleans, LA', ]; function handle_office_select(value) { - // if (view === 'office-heat-map') { - // set_view('time-series'); - // } - // if (value === 'All') { - // history.push( - // `/graphs/all/${view === 'office-heat-map' ? 'time-series' : view}` - // ); - // } - // history.push( - // `/graphs/${value}/${view === 'office-heat-map' ? 'time-series' : view}` - // ); + if (view === 'office-heat-map') { + set_view('time-series'); + } + if (value === 'All') { + history.push( + `/graphs/all/${view === 'office-heat-map' ? 'time-series' : view}` + ); + } + history.push( + `/graphs/${value}/${view === 'office-heat-map' ? 'time-series' : view}` + ); switch (value) { case 'All Offices': @@ -152,7 +152,7 @@ function GraphsContainer() { > {offices.map((office, idx) => office === 'All' ? ( - ) : ( diff --git a/src/components/pages/Landing/RenderLandingPage.jsx b/src/components/pages/Landing/RenderLandingPage.jsx index 6be6b69a4..11929150c 100644 --- a/src/components/pages/Landing/RenderLandingPage.jsx +++ b/src/components/pages/Landing/RenderLandingPage.jsx @@ -1,25 +1,31 @@ import React from 'react'; // ADD IMPORTS BACK FOR GRAPHS SECTION -// import GrantRatesByOfficeImg from '../../../styles/Images/bar-graph-no-text.png'; -// import GrantRatesByNationalityImg from '../../../styles/Images/pie-chart-no-text.png'; -// import GrantRatesOverTimeImg from '../../../styles/Images/line-graph-no-text.png'; + +// SR - imported styling images +import GrantRatesByOfficeImg from '../../../styles/Images/bar-graph-no-text.png'; +import GrantRatesByNationalityImg from '../../../styles/Images/pie-chart-no-text.png'; +import GrantRatesOverTimeImg from '../../../styles/Images/line-graph-no-text.png'; import HrfPhoto from '../../../styles/Images/paper-stack.jpg'; import '../../../styles/RenderLandingPage.less'; import { Button } from 'antd'; import { useHistory } from 'react-router-dom'; + // for the purposes of testing PageNav -// import PageNav from '../../common/PageNav'; +//import PageNav from '../../common/PageNav'; function RenderLandingPage(props) { + // declare function for scroll back to top const scrollToTop = () => { document.body.scrollTop = 0; document.documentElement.scrollTop = 0; }; + // declare history function for navigation const history = useHistory(); return (
+ {/* Header Section */}

Asylum Office Grant Rate Tracker

@@ -31,18 +37,64 @@ function RenderLandingPage(props) {
- {/* Graphs Section: Add code here for the graphs section for your first ticket */} - {/*
*/} + {/* Graphs Section (SR 3/3)*/} +
+
+ Grant rates by office +

Search Grant Rates By Office

+
+
+ Grant rates by nationality +

Search Grant Rates By Nationality

+
+
+ Grant rates over time +

Search Grant Rates Over Time

+
+
+ + {/* Button container and buttons */}
+ + {/* Download Data Button (SR 3/3) */} +
+ {/* Middle Section (SR 3/3)*/}
Human Rights First @@ -55,13 +107,53 @@ function RenderLandingPage(props) { through a Freedom of Information Act request. You can search for information on asylum grant rates by year, nationality, and asylum office, visualize the data with charts and heat maps, and download - the data set + the data set.
- {/* Bottom Section: Add code here for the graphs section for your first ticket */} - {/*
*/} + {/* Bottom Section (SR 3/3) */} +
+

Systemic Disparity Insights

+
+
+

36%

+

+ By the end of the Trump administration, the average asylum + office grant rate had fallen 36 percent from an average of 44 + percent in fiscal year 2016 to 28 percent in fiscal year 2020. +

+
+
+

5%

+

+ The New York asylum office grant rate dropped to 5 percent in + fiscal year 2020. +

+
+
+

6x Lower

+

+ Between fiscal year 2017 and 2020, the New York asylum office’s + average grant rate was six times lower than the San Francisco + asylum office. +

+
+
+ + {/* Read More button takes user to article on humanrightsfirst.org (SR 3/3) */} + +

scrollToTop()} className="back-to-top"> Back To Top ^

diff --git a/src/components/pages/profile-page.js b/src/components/pages/profile-page.js new file mode 100644 index 000000000..a6370ed71 --- /dev/null +++ b/src/components/pages/profile-page.js @@ -0,0 +1,33 @@ +import { useAuth0 } from '@auth0/auth0-react'; +import React from 'react'; +// import { NavBar } from "../components/navigation/desktop/nav-bar"; +// import { MobileNavBar } from "../components/navigation/mobile/mobile-nav-bar"; + +export const ProfilePage = () => { + const { user } = useAuth0(); + + if (!user) { + return null; + } + + return ( +
+

+ Profile Page +

+
+
+ Profile +
+
+

+ Username: {user.name} +

+

+ Email: {user.email} +

+
+
+
+ ); +}; diff --git a/src/components/protected-route.js b/src/components/protected-route.js new file mode 100644 index 000000000..4a14de930 --- /dev/null +++ b/src/components/protected-route.js @@ -0,0 +1,17 @@ +import { withAuthenticationRequired } from '@auth0/auth0-react'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import LoadingComponent from './common/LoadingComponent'; + +export const ProtectedRoute = ({ component, ...args }) => ( + ( +
+ +
+ ), + })} + {...args} + /> +); diff --git a/src/index.jsx b/src/index.jsx index 73962baa5..80e594339 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -10,7 +10,7 @@ import { import 'antd/dist/antd.less'; import { NotFoundPage } from './components/pages/NotFound'; import { LandingPage } from './components/pages/Landing'; - +import { ProtectedRoute } from './components/protected-route'; import { FooterContent, SubFooter } from './components/Layout/Footer'; import { HeaderContent } from './components/Layout/Header'; @@ -22,6 +22,9 @@ import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import reducer from './state/reducers'; import { colors } from './styles/data_vis_colors'; +import { CallbackPage } from './callback-page'; +import { ProfilePage } from './components/pages/profile-page'; +import { Auth0ProviderWithHistory } from './auth0-provider-with-history'; const { primary_accent_color } = colors; @@ -29,9 +32,11 @@ const store = configureStore({ reducer: reducer }); ReactDOM.render( - - - + + + + + , document.getElementById('root') @@ -54,6 +59,8 @@ export function App() { + +