├── .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 | ![randomuser](https://user-images.githubusercontent.com/1645233/173552010-532703f9-8b69-4fd5-b41f-04a69a91f1e7.gif) 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 |
14 | 20 |
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 |
48 | 54 |
55 |
56 | 60 | 64 |
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 | {props.alt 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 | --------------------------------------------------------------------------------