mirror of
https://github.com/myfatemi04/wheelshare-frontend.git
synced 2025-04-21 11:20:17 -04:00
start home page
This commit is contained in:
commit
f520425e08
51536
package-lock.json
generated
51536
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -12,6 +12,7 @@
|
||||||
"@types/node": "^14.14.37",
|
"@types/node": "^14.14.37",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.3",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
|
|
|
@ -16,10 +16,11 @@ import Main from './components/Main';
|
||||||
//import 'bootstrap/dist/js/bootstrap.bundle.min';
|
//import 'bootstrap/dist/js/bootstrap.bundle.min';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import Authenticator from './components/Authenticator';
|
import Authenticator from './components/Authenticator';
|
||||||
|
import AuthenticationWrapper from './components/AuthenticationWrapper';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<AuthenticationWrapper>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Nav />
|
<Nav />
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -37,7 +38,7 @@ function App() {
|
||||||
<Route component={Home} path="/" />
|
<Route component={Home} path="/" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</div>
|
</AuthenticationWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
// eslint-disable-next-line
|
|
||||||
const dev = process.env.NODE_ENV === 'development';
|
|
||||||
export const API_ENDPOINT = 'http://localhost:5000/api';
|
|
||||||
export const ION_AUTHORIZATION_ENDPOINT =
|
|
||||||
'https://ion.tjhsst.edu/oauth/authorize?response_type=code&client_id=rNa6n9YSg8ftINdyVPpUsaMuxNbHLo9dh1OsOktR&scope=read&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fion%2Fcallback';
|
|
42
src/api/api.ts
Normal file
42
src/api/api.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { APIError } from './error';
|
||||||
|
import { makeAPIGetCall } from './utils';
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const dev = process.env.NODE_ENV === 'development';
|
||||||
|
export const API_ENDPOINT = 'http://localhost:5000/api';
|
||||||
|
export const ION_AUTHORIZATION_ENDPOINT =
|
||||||
|
'https://ion.tjhsst.edu/oauth/authorize?response_type=code&client_id=rNa6n9YSg8ftINdyVPpUsaMuxNbHLo9dh1OsOktR&scope=read&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fion%2Fcallback';
|
||||||
|
|
||||||
|
axios.defaults.baseURL = API_ENDPOINT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param code The code sent by the OAuth provider
|
||||||
|
* @param provider The provider that send the OAuth code
|
||||||
|
*/
|
||||||
|
export async function createSession(
|
||||||
|
code: string,
|
||||||
|
provider: string
|
||||||
|
): Promise<string> {
|
||||||
|
const response = await axios.post('/auth/create_session', {
|
||||||
|
code,
|
||||||
|
provider,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.data.session_id;
|
||||||
|
} else {
|
||||||
|
throw new APIError('/auth/create_session', response.data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMe(): Promise<Carpool.User> {
|
||||||
|
let result = await makeAPIGetCall('/users/@me');
|
||||||
|
return result.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPublicUser(id: string): Promise<Carpool.PublicUser> {
|
||||||
|
let result = await makeAPIGetCall('/users/' + id);
|
||||||
|
return result.data.data;
|
||||||
|
}
|
5
src/api/error.ts
Normal file
5
src/api/error.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export class APIError extends Error {
|
||||||
|
constructor(route: string, message: string) {
|
||||||
|
super('API Error @' + route + ': ' + message);
|
||||||
|
}
|
||||||
|
}
|
102
src/api/utils.ts
Normal file
102
src/api/utils.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import getSessionId from '../lib/getSessionId';
|
||||||
|
import { APIError } from './error';
|
||||||
|
|
||||||
|
export function createUrlWithGetParameters(
|
||||||
|
url: string,
|
||||||
|
params?: Record<string, string | undefined>
|
||||||
|
) {
|
||||||
|
if (params) {
|
||||||
|
// Stringify the parameters
|
||||||
|
let stringifiedParameters = '';
|
||||||
|
for (let [name, value] of Object.entries(params)) {
|
||||||
|
if (value) {
|
||||||
|
stringifiedParameters +=
|
||||||
|
encodeURIComponent(name) + '=' + encodeURIComponent(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stringifiedParameters) {
|
||||||
|
url += '?' + stringifiedParameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function graphql(query: string, variables?: any) {
|
||||||
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
'/graphql',
|
||||||
|
{ query, variables },
|
||||||
|
{ headers: { Authorization: 'Bearer ' + getSessionId() } }
|
||||||
|
)
|
||||||
|
.then((successfulResponse) => {
|
||||||
|
resolve(successfulResponse.data.data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
localStorage.removeItem('session_id');
|
||||||
|
window.location.pathname = '/';
|
||||||
|
} else {
|
||||||
|
reject(new APIError('/api/users/@me', error.response.data.error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeAPIPostCall(url: string, data?: any) {
|
||||||
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.post(url, data, {
|
||||||
|
headers: { Authorization: 'Bearer ' + getSessionId() },
|
||||||
|
})
|
||||||
|
.then((successfulResponse) => resolve(successfulResponse))
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('session_id');
|
||||||
|
window.location.pathname = '/';
|
||||||
|
} else {
|
||||||
|
reject(new APIError(url, error.response?.data?.error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeAPIGetCall(
|
||||||
|
url: string,
|
||||||
|
params?: Record<string, string | undefined>
|
||||||
|
) {
|
||||||
|
url = createUrlWithGetParameters(url, params);
|
||||||
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.get(url, {
|
||||||
|
headers: { Authorization: 'Bearer ' + getSessionId() },
|
||||||
|
})
|
||||||
|
.then((successfulResponse) => resolve(successfulResponse))
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('session_id');
|
||||||
|
window.location.pathname = '/';
|
||||||
|
} else {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeAPIDeleteCall(url: string) {
|
||||||
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.delete(url, {
|
||||||
|
headers: { Authorization: 'Bearer ' + getSessionId() },
|
||||||
|
})
|
||||||
|
.then((successfulResponse) => resolve(successfulResponse))
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('session_id');
|
||||||
|
} else {
|
||||||
|
reject(new APIError(url, error.response.data.error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
19
src/components/AuthenticationContext.ts
Normal file
19
src/components/AuthenticationContext.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export type AuthState = {
|
||||||
|
isLoggedIn: boolean | null;
|
||||||
|
user: Carpool.User | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that can be used to trigger an auth state refresh.
|
||||||
|
*/
|
||||||
|
refreshAuthState: (() => void) | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthState>({
|
||||||
|
isLoggedIn: false,
|
||||||
|
user: null,
|
||||||
|
refreshAuthState: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AuthContext;
|
39
src/components/AuthenticationWrapper.tsx
Normal file
39
src/components/AuthenticationWrapper.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { getMe } from '../api/api';
|
||||||
|
import AuthContext, { AuthState } from './AuthenticationContext';
|
||||||
|
|
||||||
|
export default function AuthenticationWrapper({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const sessionId = localStorage.getItem('session_id');
|
||||||
|
// Prevent race conditions
|
||||||
|
const [authState, setAuthState] = useState<AuthState>({
|
||||||
|
isLoggedIn: null,
|
||||||
|
user: null,
|
||||||
|
refreshAuthState: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshAuthState = useCallback(() => {
|
||||||
|
if (sessionId) {
|
||||||
|
getMe().then((user) => {
|
||||||
|
setAuthState({ isLoggedIn: true, user, refreshAuthState });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setAuthState({ isLoggedIn: false, user: null, refreshAuthState });
|
||||||
|
}
|
||||||
|
}, [sessionId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshAuthState();
|
||||||
|
}, [refreshAuthState]);
|
||||||
|
|
||||||
|
if (authState?.isLoggedIn == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={authState}>{children}</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Redirect, useLocation, useParams } from 'react-router-dom';
|
import { Redirect, useLocation, useParams } from 'react-router-dom';
|
||||||
import { API_ENDPOINT } from '../api';
|
import { API_ENDPOINT } from '../api/api';
|
||||||
|
|
||||||
export default function Authenticator() {
|
export default function Authenticator() {
|
||||||
const { provider } = useParams<{ provider: string }>();
|
const { provider } = useParams<{ provider: string }>();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { API_ENDPOINT } from '../api';
|
import { API_ENDPOINT } from '../api/api';
|
||||||
|
|
||||||
const maybePluralize = (count: number, noun: string, suffix = 's') =>
|
const maybePluralize = (count: number, noun: string, suffix = 's') =>
|
||||||
`${count} ${noun}${count !== 1 ? suffix : ''}`;
|
`${count} ${noun}${count !== 1 ? suffix : ''}`;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ION_AUTHORIZATION_ENDPOINT } from '../api';
|
import { ION_AUTHORIZATION_ENDPOINT } from '../api/api';
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import Button from '@material-ui/core/Button';
|
||||||
import 'bootstrap/dist/js/bootstrap.bundle.min';
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -12,7 +12,11 @@ export default function Home() {
|
||||||
>
|
>
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
<a href={ION_AUTHORIZATION_ENDPOINT}>Log In with Ion</a>
|
<Button
|
||||||
|
onClick={() => (window.location.href = ION_AUTHORIZATION_ENDPOINT)}
|
||||||
|
>
|
||||||
|
Sign In with Ion
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<section
|
<section
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { API_ENDPOINT } from '../api';
|
import { API_ENDPOINT } from '../api/api';
|
||||||
|
|
||||||
const MyPools = () => {
|
const MyPools = () => {
|
||||||
// const id = props.match.params.id;
|
// const id = props.match.params.id;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { API_ENDPOINT } from '../api';
|
import { API_ENDPOINT } from '../api/api';
|
||||||
|
|
||||||
const maybePluralize = (count: number, noun: string, suffix = 's') =>
|
const maybePluralize = (count: number, noun: string, suffix = 's') =>
|
||||||
`${count} ${noun}${count !== 1 ? suffix : ''}`;
|
`${count} ${noun}${count !== 1 ? suffix : ''}`;
|
||||||
|
|
3
src/lib/getSessionId.ts
Normal file
3
src/lib/getSessionId.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default function getSessionId() {
|
||||||
|
return localStorage.getItem('session_id');
|
||||||
|
}
|
8
src/types.d.ts
vendored
8
src/types.d.ts
vendored
|
@ -4,6 +4,14 @@ declare namespace Carpool {
|
||||||
member_ids: string[];
|
member_ids: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Omits the email attribute
|
||||||
|
export interface PublicUser {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -1373,7 +1373,7 @@
|
||||||
"@types/yargs" "^15.0.0"
|
"@types/yargs" "^15.0.0"
|
||||||
"chalk" "^4.0.0"
|
"chalk" "^4.0.0"
|
||||||
|
|
||||||
"@material-ui/core@^4.0.0", "@material-ui/core@^4.11.3":
|
"@material-ui/core@^4.11.3":
|
||||||
"integrity" "sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw=="
|
"integrity" "sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw=="
|
||||||
"resolved" "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz"
|
"resolved" "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz"
|
||||||
"version" "4.11.3"
|
"version" "4.11.3"
|
||||||
|
@ -1911,7 +1911,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@^16.8.6 || ^17.0.0", "@types/react@^17.0.3", "@types/react@>=16.9.11", "@types/react@>=16.9.35":
|
"@types/react@*", "@types/react@^17.0.3", "@types/react@>=16.9.11", "@types/react@>=16.9.35":
|
||||||
"integrity" "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg=="
|
"integrity" "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg=="
|
||||||
"resolved" "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz"
|
"resolved" "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz"
|
||||||
"version" "17.0.3"
|
"version" "17.0.3"
|
||||||
|
@ -2654,6 +2654,13 @@
|
||||||
"resolved" "https://registry.npmjs.org/axe-core/-/axe-core-4.1.2.tgz"
|
"resolved" "https://registry.npmjs.org/axe-core/-/axe-core-4.1.2.tgz"
|
||||||
"version" "4.1.2"
|
"version" "4.1.2"
|
||||||
|
|
||||||
|
"axios@^0.21.1":
|
||||||
|
"integrity" "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA=="
|
||||||
|
"resolved" "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz"
|
||||||
|
"version" "0.21.1"
|
||||||
|
dependencies:
|
||||||
|
"follow-redirects" "^1.10.0"
|
||||||
|
|
||||||
"axobject-query@^2.2.0":
|
"axobject-query@^2.2.0":
|
||||||
"integrity" "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA=="
|
"integrity" "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA=="
|
||||||
"resolved" "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz"
|
"resolved" "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz"
|
||||||
|
@ -5274,7 +5281,7 @@
|
||||||
"inherits" "^2.0.3"
|
"inherits" "^2.0.3"
|
||||||
"readable-stream" "^2.3.6"
|
"readable-stream" "^2.3.6"
|
||||||
|
|
||||||
"follow-redirects@^1.0.0":
|
"follow-redirects@^1.0.0", "follow-redirects@^1.10.0":
|
||||||
"integrity" "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA=="
|
"integrity" "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA=="
|
||||||
"resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz"
|
"resolved" "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz"
|
||||||
"version" "1.13.2"
|
"version" "1.13.2"
|
||||||
|
@ -9453,7 +9460,7 @@
|
||||||
"strip-ansi" "6.0.0"
|
"strip-ansi" "6.0.0"
|
||||||
"text-table" "0.2.0"
|
"text-table" "0.2.0"
|
||||||
|
|
||||||
"react-dom@^16.8.0 || ^17.0.0", "react-dom@^17.0.2":
|
"react-dom@^17.0.2":
|
||||||
"integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="
|
"integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="
|
||||||
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
|
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
|
||||||
"version" "17.0.2"
|
"version" "17.0.2"
|
||||||
|
@ -9621,7 +9628,7 @@
|
||||||
"loose-envify" "^1.4.0"
|
"loose-envify" "^1.4.0"
|
||||||
"prop-types" "^15.6.2"
|
"prop-types" "^15.6.2"
|
||||||
|
|
||||||
"react@^16.8.0 || ^17.0.0", "react@^17.0.2":
|
"react@^17.0.2":
|
||||||
"integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="
|
"integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="
|
||||||
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
|
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
|
||||||
"version" "17.0.2"
|
"version" "17.0.2"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user