├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.css
├── App.test.tsx
├── Router.tsx
├── api
│ ├── User.api.tsx
│ └── User.api.type.tsx
├── component
│ ├── button
│ │ ├── Primary
│ │ │ ├── PrimaryButton.style.scss
│ │ │ ├── PrimaryButton.tsx
│ │ │ └── PrimaryButton.type.tsx
│ │ └── RefreshButton
│ │ │ ├── RefreshButton.style.scss
│ │ │ ├── RefreshButton.tsx
│ │ │ └── RefreshButton.type.tsx
│ └── user
│ │ ├── UserCard.style.scss
│ │ ├── UserCard.tsx
│ │ ├── UserCard.type.tsx
│ │ ├── UserCountry
│ │ ├── UserCountry.style.scss
│ │ ├── UserCountry.tsx
│ │ └── UserCountry.type.tsx
│ │ ├── UserImage
│ │ ├── UserImage.style.scss
│ │ ├── UserImage.tsx
│ │ └── UserImage.type.tsx
│ │ └── UserName
│ │ ├── UserName.style.scss
│ │ ├── UserName.tsx
│ │ └── UserName.type.tsx
├── context
│ ├── MainContext.tsx
│ └── MainContext.type.tsx
├── index.css
├── index.tsx
├── layout
│ └── main
│ │ ├── ErrorBoundary.tsx
│ │ ├── LayoutFooter.tsx
│ │ ├── LayoutHeader.tsx
│ │ ├── Main.style.scss
│ │ └── Main.tsx
├── logo.svg
├── react-app-env.d.ts
├── reportWebVitals.ts
├── screen
│ ├── About
│ │ ├── About.style.scss
│ │ └── About.tsx
│ └── Home
│ │ ├── Home.style.scss
│ │ └── Home.tsx
├── setupTests.ts
└── util
│ ├── LocalStorage.tsx
│ ├── fetch.tsx
│ └── i18n.tsx
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Random user
2 | 
3 | - React Hook + typescript + scss + router + context
4 | - Refresh the button to get another random user
5 | - It stores previous items by LocalStorage
6 | - We could switch between items by Arrow(right →, left,←,) and enter or space to get new one
7 | - It has simple Responsive components
8 | - It uses randomuser.me API
9 |
10 | ## Getting Started with Create React App
11 |
12 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
13 |
14 | ## Available Scripts
15 |
16 | In the project directory, you can run:
17 |
18 | ### `npm start`
19 |
20 | Runs the app in the development mode.\
21 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
22 |
23 | The page will reload if you make edits.\
24 | You will also see any lint errors in the console.
25 |
26 | ### `npm test`
27 |
28 | Launches the test runner in the interactive watch mode.\
29 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
30 |
31 | ### `npm run build`
32 |
33 | Builds the app for production to the `build` folder.\
34 | It correctly bundles React in production mode and optimizes the build for the best performance.
35 |
36 | The build is minified and the filenames include the hashes.\
37 | Your app is ready to be deployed!
38 |
39 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
40 |
41 | ### `npm run eject`
42 |
43 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
44 |
45 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
46 |
47 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
48 |
49 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
50 |
51 | ## Learn More
52 |
53 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
54 |
55 | To learn React, check out the [React documentation](https://reactjs.org/).
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "randomuser",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.4",
7 | "@testing-library/react": "^13.3.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "@types/jest": "^27.5.2",
10 | "@types/node": "^16.11.39",
11 | "@types/react": "^18.0.12",
12 | "@types/react-dom": "^18.0.5",
13 | "i18next": "^21.8.9",
14 | "react": "^18.1.0",
15 | "react-content-loader": "^6.2.0",
16 | "react-dom": "^18.1.0",
17 | "react-i18next": "^11.17.1",
18 | "react-router-dom": "^6.3.0",
19 | "react-scripts": "5.0.1",
20 | "typescript": "^4.7.3",
21 | "web-vitals": "^2.1.4"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": [
31 | "react-app",
32 | "react-app/jest"
33 | ]
34 | },
35 | "browserslist": {
36 | "production": [
37 | ">0.2%",
38 | "not dead",
39 | "not op_mini all"
40 | ],
41 | "development": [
42 | "last 1 chrome version",
43 | "last 1 firefox version",
44 | "last 1 safari version"
45 | ]
46 | },
47 | "devDependencies": {
48 | "sass": "^1.52.3"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hamid/randomuser/cde00a632238a0b8256121e2a8b293491371fc1d/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hamid/randomuser/cde00a632238a0b8256121e2a8b293491371fc1d/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hamid/randomuser/cde00a632238a0b8256121e2a8b293491371fc1d/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import Router from './Router';
4 |
5 | test('renders learn react link', () => {
6 | render();
7 | const linkElement = screen.getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/Router.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, lazy } from 'react';
2 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
3 | import { useTranslation } from 'react-i18next';
4 | import './App.css';
5 |
6 | import MainLayout from './layout/main/Main';
7 |
8 | //- Screens
9 | const Home = lazy(() => import('./screen/Home/Home'));
10 | const About = lazy(() => import('./screen/About/About'));
11 |
12 | const App = () =>{
13 |
14 | const { t } = useTranslation();
15 |
16 | return
17 | {t('loading')}}>
18 |
19 | } >
20 | } />
21 | } />
22 |
23 |
24 |
25 |
26 | };
27 |
28 | export default App;
29 |
--------------------------------------------------------------------------------
/src/api/User.api.tsx:
--------------------------------------------------------------------------------
1 | import { RootObject } from "./User.api.type";
2 | import LS from "../util/LocalStorage";
3 | export * from "./User.api.type";
4 |
5 | export default class UserApi {
6 |
7 | key = 'users';
8 |
9 | get = async (): Promise =>{
10 | try {
11 | let result = await fetch('https://randomuser.me/api/')
12 | if (result){
13 | let resultObj = await result.json();
14 | LS.push(this.key,resultObj);
15 | return resultObj;
16 | }
17 | return await false;
18 | } catch (e) {
19 | console.log("Error in fetch data", e);
20 | return await false;
21 | }
22 | }
23 |
24 |
25 | getAll = async (): Promise =>{
26 | return LS.get(this.key);
27 | }
28 |
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/api/User.api.type.tsx:
--------------------------------------------------------------------------------
1 |
2 | export interface Name {
3 | title: string;
4 | first: string;
5 | last: string;
6 | }
7 |
8 | export interface Street {
9 | number: number;
10 | name: string;
11 | }
12 |
13 | export interface Coordinates {
14 | latitude: string;
15 | longitude: string;
16 | }
17 |
18 | export interface Timezone {
19 | offset: string;
20 | description: string;
21 | }
22 |
23 | export interface Location {
24 | street: Street;
25 | city: string;
26 | state: string;
27 | country: string;
28 | postcode: number;
29 | coordinates: Coordinates;
30 | timezone: Timezone;
31 | }
32 |
33 | export interface Login {
34 | uuid: string;
35 | username: string;
36 | password: string;
37 | salt: string;
38 | md5: string;
39 | sha1: string;
40 | sha256: string;
41 | }
42 |
43 | export interface Dob {
44 | date: Date;
45 | age: number;
46 | }
47 |
48 | export interface Registered {
49 | date: Date;
50 | age: number;
51 | }
52 |
53 | export interface Id {
54 | name: string;
55 | value: string;
56 | }
57 |
58 | export interface Picture {
59 | large: string;
60 | medium: string;
61 | thumbnail: string;
62 | }
63 |
64 | export interface UserApiType {
65 | gender?: string;
66 | name?: Name;
67 | location?: Location;
68 | email?: string;
69 | login?: Login;
70 | dob?: Dob;
71 | registered?: Registered;
72 | phone?: string;
73 | cell?: string;
74 | id?: Id;
75 | picture?: Picture;
76 | nat?: string;
77 | }
78 |
79 | export interface Info {
80 | seed: string;
81 | results: number;
82 | page: number;
83 | version: string;
84 | }
85 |
86 | export interface RootObject {
87 | results: UserApiType[];
88 | info: Info;
89 | }
--------------------------------------------------------------------------------
/src/component/button/Primary/PrimaryButton.style.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --primary-button-bg: #f1f1f1;
3 | --primary-button-bg-dark: #e9e9e9;
4 | --primary-button-bg-light: #f4f4f4;
5 | --primary-button-color: var(--c-text-light);
6 | }
7 |
8 | .primary-button{
9 | color: var(--primary-button-color);
10 | font-weight: bold;
11 | background: var(--primary-button-bg);
12 | border-radius: 10px;
13 | padding: 7px 20px;
14 | border: 0px;
15 | border-bottom: 4px solid #ccc;
16 | cursor: pointer;
17 | transition: all ease 150ms;
18 | font-family: "Raleway", sans-serif;
19 | &:hover{
20 | border-bottom: 3px solid #ccc;
21 | background-color: var(--primary-button-bg-dark);
22 | }
23 | &:active{
24 | border-bottom: 1px solid #ccc;
25 | background-color: var(--primary-button-bg-light);
26 | }
27 | }
--------------------------------------------------------------------------------
/src/component/button/Primary/PrimaryButton.tsx:
--------------------------------------------------------------------------------
1 | import type PrimaryButtonProps from "./PrimaryButton.type";
2 | import './PrimaryButton.style.scss'
3 |
4 | const PrimaryButton = (props: PrimaryButtonProps)=>{
5 |
6 | let label = props.label;
7 | let onClick = props.onClick;
8 | let disable = props.disable ?? false;
9 | let loading = props.loading;
10 |
11 | return(
12 |
19 | )
20 | }
21 |
22 | export default PrimaryButton;
--------------------------------------------------------------------------------
/src/component/button/Primary/PrimaryButton.type.tsx:
--------------------------------------------------------------------------------
1 | export default interface PrimaryButtonProps{
2 | label:string,
3 | loading?:boolean,
4 | disable?:boolean,
5 | onClick?: (e:any)=>void,
6 | }
--------------------------------------------------------------------------------
/src/component/button/RefreshButton/RefreshButton.style.scss:
--------------------------------------------------------------------------------
1 | .refresh-button{
2 | width: var(--user-card-holder-width);
3 | min-height: 38px;
4 | border-radius: 0px 0px var(--user-card-holder-radius) var(--user-card-holder-radius);
5 | padding: var(--user-card-holder-padding);
6 | display: flex;
7 | flex: 1;
8 | flex-direction: row;
9 | justify-content: center;
10 | align-items: flex-start;
11 | background-color: #fff;
12 | margin: 0 auto;
13 | margin-top: -20px;
14 | box-shadow: var(--user-card-holder-shadow);
15 | z-index: 1;
16 | position: relative;
17 | padding-top: 35px;
18 | border-bottom: var(--user-card-border-bottom);
19 | animation-name: fadeBottom;
20 | animation-duration: 500ms;
21 | }
22 |
23 |
24 | @keyframes fadeBottom {
25 | from{
26 | opacity: 0;
27 | margin-top: -90px;
28 | }
29 | to{
30 | opacity: 1;
31 | margin-top: -20px;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/component/button/RefreshButton/RefreshButton.tsx:
--------------------------------------------------------------------------------
1 | import PrimaryButton from "../Primary/PrimaryButton";
2 | import './RefreshButton.style.scss'
3 | import type RefreshButtonProps from "./RefreshButton.type";
4 |
5 | const RefreshButton = (props: RefreshButtonProps)=>{
6 |
7 | let label = props.label;
8 | let onClick = props.onClick;
9 | let disable = props.disable ?? false;
10 | let loading = props.loading;
11 |
12 | return(
13 |
21 | )
22 | }
23 |
24 | export default RefreshButton;
--------------------------------------------------------------------------------
/src/component/button/RefreshButton/RefreshButton.type.tsx:
--------------------------------------------------------------------------------
1 | export default interface RefreshButtonProps {
2 | label:string,
3 | loading?:boolean,
4 | disable?:boolean,
5 | onClick?: (e:any)=>void,
6 | }
--------------------------------------------------------------------------------
/src/component/user/UserCard.style.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | :root {
4 | --user-card-holder-width: 16vw;
5 | --user-card-holder-min-height: 40vh;
6 | --user-card-holder-padding: 10px;
7 | --user-card-holder-radius: 20px;
8 | --user-card-holder-shadow: 0 0 15px rgba(0,0,0,0.15);
9 | --user-card-holder-shadow-hover: 0 0 55px rgba(0,0,0,0.15);
10 | --user-card-border-bottom: 4px solid #e4e4e4;
11 | }
12 |
13 | .user-card-holder{
14 | width: var(--user-card-holder-width);
15 | min-height: var(--user-card-holder-min-height);
16 | border-radius: var(--user-card-holder-radius);
17 | padding: var(--user-card-holder-padding);
18 | display: flex;
19 | flex: 1;
20 | flex-direction: column;
21 | justify-content: center;
22 | align-items: flex-start;
23 | background-color: #fff;
24 | margin: 0 auto;
25 | margin-top: 0%;
26 | box-shadow: var(--user-card-holder-shadow);
27 | z-index: 10;
28 | position: relative;
29 | border-bottom: var(--user-card-border-bottom);
30 | transition: all ease 150ms;
31 | cursor: pointer;
32 |
33 | &:hover{
34 | transform: scale(1.05,1.05);
35 | box-shadow: var(--user-card-holder-shadow-hover);
36 | }
37 | &:active{
38 | transform: scale(1.03, 1.03);
39 | box-shadow: var(--user-card-holder-shadow);
40 | }
41 |
42 |
43 | .col-w1{
44 | display: flex;
45 | flex-direction: column;
46 | flex:3;
47 | align-self: stretch;
48 | }
49 | .col-w2{
50 | display: flex;
51 | flex-direction: column;
52 | flex:1;
53 | align-self: stretch;
54 | align-items: center;
55 | }
56 | }
57 |
58 | @media only screen and (max-width: 1000px) {
59 |
60 | :root {
61 | --user-card-holder-width: 55vw;
62 | --user-card-holder-min-height: 40vh;
63 | --user-card-holder-padding: 10px;
64 | --user-card-holder-radius: 20px;
65 | --user-card-holder-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
66 | --user-card-border-bottom: 4px solid #e4e4e4;
67 | }
68 | }
69 |
70 |
71 | @media only screen and (max-width: 600px) {
72 |
73 | :root {
74 | --user-card-holder-width: 90vw;
75 | --user-card-holder-min-height: 40vh;
76 | --user-card-holder-padding: 10px;
77 | --user-card-holder-radius: 20px;
78 | --user-card-holder-shadow: 0 0 15px rgba(0, 0, 0, 0.15);
79 | --user-card-border-bottom: 4px solid #e4e4e4;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/component/user/UserCard.tsx:
--------------------------------------------------------------------------------
1 | import ContentLoader from "react-content-loader"
2 | import UserImage from './UserImage/UserImage';
3 | import UserName from './UserName/UserName';
4 | import UserCountry from './UserCountry/UserCountry';
5 | import type UserCardType from './UserCard.type'
6 | import './UserCard.style.scss'
7 |
8 |
9 | const UserCard = (props: UserCardType)=>{
10 |
11 |
12 | const renderContentLoader =function(){
13 | return (
22 |
23 |
24 |
25 | )
26 | }
27 |
28 |
29 |
30 | //- loading
31 | let loadingContent = null;
32 | if (props.loading || props.user?.name === undefined)
33 | loadingContent= renderContentLoader();
34 |
35 | let user = props.user;
36 | //- prepear user info
37 | let firstName = `${user?.name?.title}. ${user?.name?.first}`;
38 | let lastName = `${user?.name?.last}`;
39 | let image = user?.picture;
40 | let country = user?.location?.country ?? "";
41 | let countryDesc = `${user?.location?.city} ${user?.location?.state} ${user?.location?.state} ${user?.location?.street.name}`;
42 |
43 | return(
44 |
45 | {props.loading ? loadingContent
46 | :<>
47 |
55 |
65 | >}
66 |
67 | )
68 | }
69 | export default UserCard;
--------------------------------------------------------------------------------
/src/component/user/UserCard.type.tsx:
--------------------------------------------------------------------------------
1 | import { UserApiType } from "../../api/User.api.type"
2 | export default interface UserCardType {
3 | loading : boolean
4 | user ?: UserApiType
5 | }
--------------------------------------------------------------------------------
/src/component/user/UserCountry/UserCountry.style.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | :root {
4 | --user-country-color : var(--c-text-light);
5 | --user-country-bg : #f1f1f1
6 | }
7 |
8 | .user-country-holder{
9 |
10 | display: flex;
11 | flex-direction: row;
12 | flex-grow: 1;
13 | align-items: flex-end;
14 |
15 |
16 | .user-country-name{
17 | font-size: 1.3em;
18 | color: var(--user-country-color);
19 | margin: 10px;
20 | text-transform: capitalize;
21 | background: var(--user-country-bg);
22 | padding: 5px 20px;
23 | border-radius: 20px;
24 |
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/component/user/UserCountry/UserCountry.tsx:
--------------------------------------------------------------------------------
1 | import './UserCountry.style.scss';
2 | import type UserCountryType from "./UserCountry.type";
3 |
4 | const UserCountry = (props: UserCountryType)=>{
5 | return(
6 |
7 |
{props.name}
8 |
9 | )
10 | }
11 | export default UserCountry;
--------------------------------------------------------------------------------
/src/component/user/UserCountry/UserCountry.type.tsx:
--------------------------------------------------------------------------------
1 | export default interface UserCountryType {
2 | name : string
3 | desc ?: string
4 | }
--------------------------------------------------------------------------------
/src/component/user/UserImage/UserImage.style.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | .user-card-image{
6 | display: block;
7 | width: 100%;
8 | img{
9 | display: block;
10 | width: 100%;
11 | aspect-ratio: 3/3;
12 | object-fit: cover;
13 | object-position: top center;
14 | border-radius: calc(var(--user-card-holder-radius) - var(--user-card-holder-padding));
15 | animation-name: fadein;
16 | animation-duration: 700ms;
17 | }
18 |
19 | }
20 | @keyframes fadein {
21 | from {
22 | opacity: 0;
23 | }
24 |
25 | to {
26 | opacity: 1;
27 | }
28 | }
--------------------------------------------------------------------------------
/src/component/user/UserImage/UserImage.tsx:
--------------------------------------------------------------------------------
1 | import './UserImage.style.scss'
2 | import type UserImageType from "./UserImage.type";
3 |
4 | const UserImage = (props: UserImageType)=>{
5 | return(
6 |
7 |
8 |
9 | )
10 | }
11 | export default UserImage;
--------------------------------------------------------------------------------
/src/component/user/UserImage/UserImage.type.tsx:
--------------------------------------------------------------------------------
1 | export default interface UserImageType{
2 | large: string
3 | medium: string
4 | thumbnail?: string
5 | alt ?: string
6 | }
--------------------------------------------------------------------------------
/src/component/user/UserName/UserName.style.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | :root {
4 | --user-first-name-color : var(--c-text);
5 | --user-last-name-color : var(--c-text-light);
6 | }
7 |
8 | h1.user-first-name{
9 | font-size: 2em;
10 | color: var(--user-first-name-color);
11 | margin: 5px;
12 | text-transform: capitalize;
13 | }
14 | h2.user-last-name{
15 | font-size: 1.5em;
16 | color: var(--user-last-name-color);
17 | margin: 5px;
18 | text-transform: capitalize;
19 | }
--------------------------------------------------------------------------------
/src/component/user/UserName/UserName.tsx:
--------------------------------------------------------------------------------
1 | import './UserName.style.scss'
2 | import type UserNameType from "./UserName.type";
3 |
4 | const UserName = (props: UserNameType)=>{
5 | return(
6 | <>
7 | {props.firstName}
8 | {props.lastName}
9 | >
10 | )
11 | }
12 | export default UserName;
--------------------------------------------------------------------------------
/src/component/user/UserName/UserName.type.tsx:
--------------------------------------------------------------------------------
1 | export default interface UserNameType{
2 | firstName: string
3 | lastName: string
4 | }
--------------------------------------------------------------------------------
/src/context/MainContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useEffect } from 'react';
2 | import { AppContextInterface, AppContextPropsInterface} from "./MainContext.type";
3 | import UserApi, { UserApiType, RootObject } from "../api/User.api";
4 |
5 | const context = createContext({ loading: false, user:{} });
6 | const AppContext = (props: AppContextPropsInterface)=>{
7 |
8 |
9 | //-- Define State
10 | var [appLoading, setAppLoading] = useState(false);
11 | var [activeUser, setActiveUser] = useState({} as UserApiType);
12 | var [activeUserIndex, setActiveUserIndex] = useState(0);
13 |
14 | //-- Define methods
15 | const UserApiObj = new UserApi();
16 | const getCurrentUser = async (): Promise => {
17 | setAppLoading(true);
18 | let newUser = {};
19 | let data: RootObject | Boolean = await UserApiObj.get();
20 |
21 | if (data) {
22 | newUser = (data as RootObject).results[0];
23 | setActiveUser(newUser);
24 | let allList = await UserApiObj.getAll();
25 | if (allList && allList.length){
26 | console.log(allList.length - 1, activeUserIndex);
27 | setActiveUserIndex(allList.length-1); // set index to last recently item
28 | }
29 | }
30 | setAppLoading(false);
31 | return newUser
32 | };
33 | const getAllUsers = async (): Promise =>{
34 | return await UserApiObj.getAll();
35 | }
36 | const getPrevUser = async (): Promise =>{
37 | console.log(activeUserIndex - 1);
38 | if (activeUserIndex-1 >=0)
39 | setActiveUserIndex(activeUserIndex-1);
40 | let lastItem = {} as RootObject;
41 | let list = await UserApiObj.getAll();
42 |
43 | if(list && list.length){
44 | lastItem = list[activeUserIndex-1];
45 | }
46 | return lastItem;
47 | }
48 | const getNextUser = async (): Promise =>{
49 | console.log(activeUserIndex +1);
50 | let list = await UserApiObj.getAll();
51 | let lastItem = {} as RootObject;
52 | if (activeUserIndex+1 && list && list.length-1 > activeUserIndex)
53 | setActiveUserIndex(activeUserIndex+1);
54 | if(list && list.length){
55 | lastItem = list[activeUserIndex+1];
56 | }
57 | return lastItem;
58 | }
59 |
60 | useEffect(()=>{
61 |
62 | },[])
63 |
64 | return (
65 |
76 | {props.children}
77 |
78 | )
79 | }
80 |
81 | export { AppContext };
82 | export default context;
--------------------------------------------------------------------------------
/src/context/MainContext.type.tsx:
--------------------------------------------------------------------------------
1 | import { UserApiType, RootObject } from "../api/User.api.type"
2 |
3 | export interface AppContextInterface{
4 | loading : boolean
5 | user : {
6 | activeUser ?: UserApiType
7 | setActiveUser?: (user: UserApiType) => void
8 | getCurrentUser?: () => Promise
9 | getAllUsers?: () => Promise
10 | getPrevUser?: () => Promise
11 | getNextUser?: () => Promise
12 | }
13 |
14 | }
15 | export interface AppContextPropsInterface {
16 | children : JSX.Element
17 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import Router from './Router';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | import './util/i18n';
8 | import { AppContext } from './context/MainContext';
9 |
10 | const root = ReactDOM.createRoot(
11 | document.getElementById('root') as HTMLElement
12 | );
13 | root.render(
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | // If you want to start measuring performance in your app, pass a function
22 | // to log results (for example: reportWebVitals(console.log))
23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
24 | reportWebVitals();
25 |
--------------------------------------------------------------------------------
/src/layout/main/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | export default class ErrorBoundary extends React.Component {
3 | constructor(props:any) {
4 | super(props);
5 | this.state = { hasError: false };
6 | }
7 |
8 | static getDerivedStateFromError(error:any) {
9 | // Update state so the next render will show the fallback UI.
10 | return { hasError: true };
11 | }
12 |
13 | componentDidCatch(error:any, errorInfo:any) {
14 | // You can also log the error to an error reporting service
15 | console.log(error, errorInfo);
16 | }
17 |
18 | render() {
19 | if (this.state.hasError) {
20 | return Something went wrong.
;
21 | }
22 |
23 | return this.props.children;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/layout/main/LayoutFooter.tsx:
--------------------------------------------------------------------------------
1 | const LayoutFooter = ()=>{
2 | return ()
5 | }
6 | export default LayoutFooter
--------------------------------------------------------------------------------
/src/layout/main/LayoutHeader.tsx:
--------------------------------------------------------------------------------
1 | import { NavLink,useLocation } from 'react-router-dom';
2 |
3 | const LayoutHeader = () => {
4 | let { pathname } = useLocation();
5 | console.log(pathname,"pathname");
6 | return (
7 |
8 |
9 |
19 |
20 |
21 | )
22 | }
23 | export default LayoutHeader
--------------------------------------------------------------------------------
/src/layout/main/Main.style.scss:
--------------------------------------------------------------------------------
1 | @import url('https: //fonts.googleapis.com/css2?family=Raleway&display=swap');
2 |
3 | :root {
4 | --c-primary: #ccc;
5 | --c-text: #1D1F22;
6 | --c-text-light: #626468;
7 | --c-active: #2c3e50;
8 | }
9 |
10 | html,
11 | body {
12 | margin: 0;
13 | background-color: #eee;
14 | box-sizing: border-box;
15 | }
16 |
17 | body {
18 | font-family: 'Raleway', sans-serif;
19 | font-size: 10px;
20 | font-weight: 400;
21 | }
22 |
23 | a {
24 | text-decoration: none;
25 | color: var(--c-text);
26 | }
27 |
28 | a:hover {
29 | transition: all ease 150ms;
30 | color: var(--c-primary);
31 | }
32 |
33 | ul {
34 | margin: 0;
35 | padding: 0;
36 | list-style: none;
37 | }
38 |
39 |
40 | .main-layout{
41 | position: relative;
42 | min-height: 100vh;
43 | display: flex;
44 | flex-direction: column;
45 | justify-content: space-between;
46 | flex: 1;
47 | }
48 |
49 | .outlet-holder {
50 | position: relative;
51 | min-height: 100vh;
52 | flex: 1;
53 | }
54 |
55 |
56 |
57 | .top-header {
58 | display: flex;
59 | flex-direction: row;
60 | justify-content: space-between;
61 | align-items: center;
62 | padding: 25px 100px;
63 |
64 | // nav bar
65 | .nav-bar {
66 | flex: 1;
67 |
68 | nav {
69 | font-size: 1.6em;
70 |
71 | ul {
72 | display: flex;
73 | flex-direction: row;
74 | justify-content: center;
75 |
76 | li {
77 | margin: 0 15px;
78 | position: relative;
79 | transition: all 500ms ease-in-out;
80 | color: var(--c-text-light);
81 |
82 | &.active {
83 | z-index: 1;
84 |
85 | a {
86 | color: var(--c-active);
87 | font-weight: bold;
88 | }
89 | }
90 |
91 | &::before {
92 | content: '';
93 | background-color: var(--c-active);
94 | position: absolute;
95 | left: 0;
96 | bottom: -10px;
97 | width: 0%;
98 | height: 0px;
99 | z-index: 2;
100 | transition: all .3s ease-in-out;
101 | }
102 |
103 | &:hover::before,
104 | &.active::before {
105 | width: 100%;
106 | height: 4px;
107 | }
108 |
109 | &:active::before {
110 | width: 100%;
111 | height: 200%;
112 | transition: all 300ms ease-in-out;
113 | }
114 |
115 |
116 | }
117 | }
118 | }
119 | }
120 | }
121 |
122 |
123 | .footer{
124 | display: flex;
125 | padding: 20px;
126 | flex-direction: row;
127 | justify-content: center;
128 | align-items: center;
129 | }
--------------------------------------------------------------------------------
/src/layout/main/Main.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 | import './Main.style.scss';
3 |
4 | import ErrorBoundary from "./ErrorBoundary";
5 | import LayoutHeader from "./LayoutHeader";
6 | import LayoutFooter from "./LayoutFooter";
7 |
8 | const MainLayout = ()=>{
9 | return(
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | export default MainLayout;
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/screen/About/About.style.scss:
--------------------------------------------------------------------------------
1 | .about-holder{
2 | font-size: 1.6em;
3 | text-align: center;
4 | }
--------------------------------------------------------------------------------
/src/screen/About/About.tsx:
--------------------------------------------------------------------------------
1 | import './About.style.scss'
2 |
3 | const About = () => {
4 | return (
5 | Randomuser User API
6 | by Hamid reza Salimian
7 | )
8 | }
9 | export default About
--------------------------------------------------------------------------------
/src/screen/Home/Home.style.scss:
--------------------------------------------------------------------------------
1 | .home-holder{
2 |
3 | .help-bar{
4 | position: absolute;
5 | width: 200px;
6 | left: 10px;
7 | right: 0px;
8 | top: 10%;
9 | background-color: rgba(0,0,0,.1);
10 | padding: 10px;
11 | border-radius: 10px;
12 | font-size: 1.2em;
13 | animation-name: fadeinleft;
14 | animation-duration: 500ms;
15 | }
16 | }
17 |
18 | @media only screen and (max-width: 600px) {
19 |
20 | .help-bar{
21 | display: none;
22 | }
23 | }
24 |
25 | @keyframes fadeinleft {
26 | from {
27 | opacity: 0;
28 | left: -10%;
29 | }
30 |
31 | to {
32 | opacity: 1;
33 | left: 10px;
34 | }
35 | }
--------------------------------------------------------------------------------
/src/screen/Home/Home.tsx:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from 'react';
2 | import { useTranslation } from 'react-i18next';
3 |
4 | import UserCard from "../../component/user/UserCard";
5 | import RefreshButton from "../../component/button/RefreshButton/RefreshButton";
6 | import context from '../../context/MainContext';
7 | import './Home.style.scss';
8 |
9 | const Home = () => {
10 |
11 | const { t } = useTranslation();
12 | const contextObj = useContext(context);
13 | var _self = this;
14 |
15 | useEffect(()=>{
16 | if(!contextObj.loading)
17 | contextObj.user.getCurrentUser?.();
18 |
19 | }, [])
20 | useEffect(()=>{
21 | document.addEventListener("keydown", onKeyDown);
22 | return (() => {
23 | document.removeEventListener("keydown", onKeyDown, false);
24 | })
25 | }, [contextObj])
26 |
27 | const onRefresh = async function(){
28 | contextObj.user.getCurrentUser?.();
29 |
30 | }
31 | const onPrevUser = async function(){
32 | let prev = await contextObj.user.getPrevUser?.();
33 | if(prev)
34 | {
35 | contextObj.user.setActiveUser?.(prev.results[0]);
36 | }
37 | }
38 | const onNextUser = async function(){
39 | let next = await contextObj.user.getNextUser?.();
40 | if (next)
41 | {
42 | contextObj.user.setActiveUser?.(next.results[0]);
43 | }
44 | }
45 |
46 |
47 | const onKeyDown = async (event:any)=>{
48 | if (event.code === "ArrowRight"){
49 | onNextUser();
50 | } else if (event.code === "ArrowLeft"){
51 | onPrevUser();
52 |
53 | } else if (event.code === "Space" || event.code === "Enter"){
54 | contextObj.user.getCurrentUser?.();
55 | }
56 | }
57 |
58 | return (
59 |
60 |
64 |
70 |
71 |
72 |
Use Arrow key to change user
73 |
← to previus user
74 |
→ to next user
75 |
↵ to new user
76 |
77 | );
78 | }
79 |
80 | export default Home;
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/util/LocalStorage.tsx:
--------------------------------------------------------------------------------
1 | const LS = {
2 |
3 | maxItemsStore :25,
4 |
5 | push : (key:string,value:any) => {
6 | try{
7 | let list = LS.get(key,LS.maxItemsStore);
8 | if(list && list.length)
9 | list.push(value);
10 | else
11 | list = [value]
12 | return LS.save(key, list);
13 | }catch(e){
14 | console.error('Local Storage Error', e);
15 | return false;
16 | }
17 |
18 | },
19 | get: (key: string,max=100) => {
20 | try {
21 | let list = JSON.parse(localStorage.getItem(key) ?? "");
22 | if(list && list.length)
23 | list = list.slice(-max);
24 | else
25 | list = [];
26 | return list;
27 | } catch (e) {
28 | console.error('Local Storage Error', e);
29 | return [];
30 | }
31 | },
32 | save:(key:string,value:any)=>{
33 | let valString = JSON.stringify(value);
34 | return localStorage.setItem(key,valString);
35 | }
36 | }
37 | export default LS;
--------------------------------------------------------------------------------
/src/util/fetch.tsx:
--------------------------------------------------------------------------------
1 | const fetch = {
2 |
3 | }
4 | export default fetch;
--------------------------------------------------------------------------------
/src/util/i18n.tsx:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import { initReactI18next } from "react-i18next";
3 |
4 | // the translations
5 | // (tip move them in a JSON file and import them,
6 | // or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files)
7 | const resources = {
8 | en: {
9 | translation: {
10 | "Welcome to React": "Welcome to React and react-i18next",
11 | "loading": "Loading ...",
12 | "refresh": "Refresh",
13 | }
14 | },
15 | sp: {
16 | translation: {
17 | "Welcome to React": "Bienvenido a React y react-i18next",
18 | "loading": "Cargando ...",
19 | "refresh": "Actualizar",
20 | }
21 | }
22 | };
23 |
24 | i18n
25 | .use(initReactI18next) // passes i18n down to react-i18next
26 | .init({
27 | resources,
28 | lng: "en", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
29 | // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
30 | // if you're using a language detector, do not define the lng option
31 |
32 | interpolation: {
33 | escapeValue: false // react already safes from xss
34 | }
35 | });
36 |
37 | export default i18n;
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------