Skip to content

Commit 8a30d0b

Browse files
author
k.golikov
committed
Add user info
1 parent a7678c3 commit 8a30d0b

File tree

8 files changed

+221
-21
lines changed

8 files changed

+221
-21
lines changed

src/actions/api/appAxios.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import axios, { AxiosInstance } from 'axios';
2+
import call from '../../utils/call';
3+
import camelizeKeys from '../../utils/camelizeKeys';
4+
5+
const appAxios = call<AxiosInstance>(() => {
6+
const instance = axios.create();
7+
8+
instance.interceptors.response.use((response) => {
9+
response.data = camelizeKeys(response.data);
10+
return response;
11+
});
12+
13+
return instance;
14+
});
15+
16+
export default appAxios;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { UserIpInfo } from '../../../types/ipapi.co';
2+
import appAxios from '../../api/appAxios';
3+
4+
const getUserInfo = async (): Promise<UserIpInfo> => {
5+
const response = await appAxios.get<UserIpInfo>('https://ipapi.co/json/');
6+
return response.data;
7+
};
8+
9+
export default getUserInfo;

src/constants/router/menuItems.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,10 @@ const menuItems: MenuItem[] = [
5656
title: 'BG Generator'
5757
},
5858
{
59-
route: routes.templateTextGenerator,
60-
isGray: true
59+
route: routes.userInfo
6160
},
6261
{
63-
route: routes.userInfo,
62+
route: routes.templateTextGenerator,
6463
isGray: true
6564
},
6665
{

src/hooks/useAsync.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ export interface AsyncHookParams<T> {
77
onComplete?: () => void;
88
onLoadingChange?: (isLoading: boolean) => void;
99
onErrorChange?: (error: unknown) => void;
10-
isLoadingInitial?: boolean;
11-
errorInitial?: boolean;
10+
initialResult?: T;
11+
initialIsLoading?: boolean;
12+
initialError?: boolean;
1213
doInvokeOnMount?: boolean;
1314
}
1415

@@ -19,13 +20,23 @@ const useAsync = <T>(promiseFn: (() => Promise<T>) | Promise<T>, params?: AsyncH
1920
onComplete,
2021
onLoadingChange,
2122
onErrorChange,
22-
isLoadingInitial,
23-
errorInitial,
23+
initialResult,
24+
initialIsLoading,
25+
initialError,
2426
doInvokeOnMount
2527
} = params ?? {};
2628

27-
const [isLoading, setIsLoading] = useState<boolean>(isLoadingInitial ?? false);
28-
const [error, setError] = useState<unknown>(errorInitial);
29+
const [result, setResult] = useState<T | undefined>(initialResult);
30+
const [isLoading, setIsLoading] = useState<boolean>(initialIsLoading ?? false);
31+
const [error, setError] = useState<unknown>(initialError);
32+
33+
const changeResult = useCallback(
34+
(value: T) => {
35+
setResult(value);
36+
onSuccess?.(value);
37+
},
38+
[onSuccess]
39+
);
2940

3041
const changeLoading = useCallback(
3142
(value: boolean) => {
@@ -50,7 +61,7 @@ const useAsync = <T>(promiseFn: (() => Promise<T>) | Promise<T>, params?: AsyncH
5061

5162
const result: T = isFunction(promiseFn) ? await promiseFn() : await promiseFn;
5263

53-
onSuccess?.(result);
64+
changeResult(result);
5465
} catch (e) {
5566
changeError(e);
5667
onError?.(e);
@@ -67,6 +78,7 @@ const useAsync = <T>(promiseFn: (() => Promise<T>) | Promise<T>, params?: AsyncH
6778
}, []);
6879

6980
return {
81+
result,
7082
invoke,
7183
isLoading,
7284
error
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.container {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 8px;
5+
6+
.ipAddress {
7+
font-size: 1.1rem;
8+
display: flex;
9+
flex-direction: row;
10+
align-items: center;
11+
}
12+
13+
.ipAddressSkeleton {
14+
height: 22px !important;
15+
width: 165px;
16+
margin-top: 3px;
17+
}
18+
19+
.locationSkeleton {
20+
height: 22px !important;
21+
width: 165px;
22+
margin-top: 1px;
23+
}
24+
25+
.ipDataProviderSkeleton {
26+
height: 20px !important;
27+
width: 200px;
28+
margin-bottom: 3.5px;
29+
}
30+
}

src/pages/userInfoPage/UserInfoPage.tsx

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,103 @@
1-
import React from 'react';
2-
import PageContainer, { PageTag } from '../../components/pageContainer/PageContainer';
1+
import React, { FunctionComponent } from 'react';
2+
import PageContainer from '../../components/pageContainer/PageContainer';
33
import Text from 'antd/lib/typography/Text';
4-
import { Space } from 'antd';
4+
import { Col, notification, Skeleton, Space, Tag, Tooltip } from 'antd';
5+
import styles from './UserInfoPage.module.scss';
6+
import useAsync from '../../hooks/useAsync';
7+
import getUserInfo from '../../actions/ipapi.co/api/getUserInfo';
8+
import getErrorMessage from '../../utils/getErrorMessage';
9+
import ExternalLink from '../../components/ExternalLink';
10+
import getScreenSize from '../../utils/getScreenSize';
11+
import getScaledScreenSize from '../../utils/getScaledScreenSize';
512

6-
const tags = [PageTag.WIP];
13+
const UserInfoPage: FunctionComponent = () => {
14+
const {
15+
result: userIpInfo,
16+
isLoading: isUserIpInfoLoading,
17+
error: userIpInfoError
18+
} = useAsync(getUserInfo, {
19+
doInvokeOnMount: true,
20+
onError: (error) => {
21+
console.error(error);
22+
notification.error({
23+
message: 'An error occurred while getting the IP address',
24+
description: getErrorMessage(error)
25+
});
26+
},
27+
onSuccess: console.log
28+
});
29+
30+
const browserLanguages = window.navigator.languages;
31+
const realScreenSize = getScaledScreenSize();
32+
const screenSize = getScreenSize();
33+
const scale = window.devicePixelRatio;
34+
const screenOrientation = window.screen.orientation.type;
735

8-
const UserInfoPage = () => {
936
return (
10-
<PageContainer title="User Info" tags={tags}>
11-
<Space>
12-
<Text strong>
13-
<Text>IP: </Text>
14-
<Text copyable>192.168.0.123 (fake)</Text>
37+
<PageContainer title="User Info">
38+
<Col className={styles.container}>
39+
{!userIpInfoError && (
40+
<Space direction="vertical" className="mb-2">
41+
{isUserIpInfoLoading ? (
42+
<>
43+
<Skeleton.Input active className={styles.ipAddressSkeleton} />
44+
<Skeleton.Input active className={styles.locationSkeleton} />
45+
<Skeleton.Input active className={styles.ipDataProviderSkeleton} />
46+
</>
47+
) : (
48+
<>
49+
<Text strong className={styles.ipAddress}>
50+
<Text strong>IP: </Text>
51+
<Text copyable>{userIpInfo?.ip}</Text>
52+
</Text>
53+
<Text>
54+
<Text strong>Location:</Text>
55+
<Text className="ms-1">
56+
{userIpInfo?.countryName}, {userIpInfo?.city}
57+
</Text>
58+
</Text>
59+
<Text type="secondary">
60+
The data is provided by{' '}
61+
<ExternalLink href="https://ipapi.co/">ipapi.co</ExternalLink>
62+
</Text>
63+
</>
64+
)}
65+
</Space>
66+
)}
67+
<Text>
68+
<Text strong>Browser languages:</Text>
69+
<Text className="ms-2">
70+
{browserLanguages.map((language, index) =>
71+
index === 0 ? (
72+
<Tooltip title="Your primary language" placement="bottom">
73+
<Tag color="gold">{language}</Tag>
74+
</Tooltip>
75+
) : (
76+
<Tag color="default">{language}</Tag>
77+
)
78+
)}
79+
</Text>
80+
</Text>
81+
<Text>
82+
<Text strong>Screen size:</Text>
83+
<Text className="ms-2">
84+
{realScreenSize.width}x{realScreenSize.height}
85+
</Text>
86+
{scale !== 1 && (
87+
<Text className="ms-1" type="secondary">
88+
({screenSize.width}x{screenSize.height} * {scale})
89+
</Text>
90+
)}
91+
</Text>
92+
<Text>
93+
<Text strong>Pixel ratio:</Text>
94+
<Text className="ms-2">{scale}</Text>
95+
</Text>
96+
<Text>
97+
<Text strong>Orientation:</Text>
98+
<Text className="ms-2">{screenOrientation}</Text>
1599
</Text>
16-
</Space>
100+
</Col>
17101
</PageContainer>
18102
);
19103
};

src/types/ipapi.co.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//Generated by https://mrgrd56.github.io/#/tools/json-to-typescript ;)
2+
3+
export interface UserIpInfo {
4+
ip: string;
5+
version: string;
6+
city: string;
7+
region: string;
8+
regionCode: string;
9+
country: string;
10+
countryName: string;
11+
countryCode: string;
12+
countryCodeIso3: string;
13+
countryCapital: string;
14+
countryTld: string;
15+
continentCode: string;
16+
inEu: boolean;
17+
postal: string;
18+
latitude: number;
19+
longitude: number;
20+
timezone: string;
21+
utcOffset: string;
22+
countryCallingCode: string;
23+
currency: string;
24+
currencyName: string;
25+
languages: string;
26+
countryArea: number;
27+
countryPopulation: number;
28+
asn: string;
29+
org: string;
30+
}

src/utils/camelizeKeys.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { camelCase, isNil } from 'lodash';
2+
3+
//https://stackoverflow.com/a/50620653/14899408
4+
5+
const camelizeKeys = (obj: any): any => {
6+
if (Array.isArray(obj)) {
7+
return obj.map((v) => camelizeKeys(v));
8+
} else if (!isNil(obj) && obj.constructor === Object) {
9+
return Object.keys(obj).reduce(
10+
(result, key) => ({
11+
...result,
12+
[camelCase(key)]: camelizeKeys(obj[key])
13+
}),
14+
{}
15+
);
16+
}
17+
return obj;
18+
};
19+
20+
export default camelizeKeys;

0 commit comments

Comments
 (0)