├── README.md
├── backend_server
├── .dockerignore
├── .eslintrc.json
├── .gitignore
├── .idea
│ ├── backend_server.iml
│ ├── codeStyles
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── inspectionProfiles
│ │ └── Project_Default.xml
│ ├── modules.xml
│ ├── vcs.xml
│ └── workspace.xml
├── .prettierrc
├── Dockerfile
├── app.js
├── config.env
├── countries.json
├── package-lock.json
├── package.json
└── server.js
├── package-lock.json
├── pics
├── Layout.png
├── Layout_Resize.png
└── Table.png
└── react_project
├── .dockerignore
├── .eslintrc.cjs
├── .gitignore
├── Dockerfile
├── babel.config.cjs
├── index.html
├── jest.config.cjs
├── package-lock.json
├── package.json
├── public
└── vite.svg
├── src
├── App.jsx
├── assets
│ ├── emoji.png
│ └── react_logo.png
├── components
│ ├── dynamic-table
│ │ ├── dynamic-table.jsx
│ │ └── dynamic-table.module.css
│ ├── error-page
│ │ ├── error-page.jsx
│ │ └── error-page.module.css
│ └── navbar
│ │ └── navbar.jsx
├── containers
│ ├── home
│ │ ├── home.jsx
│ │ └── home.module.css
│ ├── layout
│ │ ├── layout.jsx
│ │ └── layout.module.css
│ ├── search-bar
│ │ ├── search-bar.jsx
│ │ └── search-bar.module.css
│ └── table
│ │ ├── table-data.json
│ │ └── table.jsx
├── index.css
├── main.jsx
├── routes.jsx
├── services
│ └── api.js
├── tests
│ └── search-bar.test.js
└── utils
│ ├── currency-data.js
│ └── use-search.js
└── vite.config.js
/README.md:
--------------------------------------------------------------------------------
1 | # React JavaScript Test
2 |
3 | A sample project with React to test your familiarities with this library as well as Html, CSS and JavaScript.
4 |
5 | ### Requirements
6 |
7 | You need [Node.js](https://nodejs.org/en/download) installed on you machine to run the Node server and the **React** project.
8 |
9 | ### Included in the Box
10 |
11 | You will find a simple Node server in `./backend_server` folder that provides the necessary API endpoints. Additionally, a boilerplate React project is placed in `./react_project` with the necessary libraries included. Use this folder as the starting point for completing the tasks described in the next steps.
12 |
13 | ## Step 1: Preparation
14 |
15 | In our project, **React Router** is already included. Configure the React Router to have these 4 routes:
16 |
17 | - /
18 | - /layout
19 | - /table
20 | - /searchbar
21 |
22 | > **Note:** Make use of Data API in the new versions of React Router
23 |
24 | ## Step 2: Layout
25 |
26 | Now that the setup is complete, we want to implement this typical sketch found in many web application user interfaces:
27 |
28 | 
29 |
30 | It is however important that the page is responsive, so if the page is resized and the width of `main content` has reached `500px` or less, the page should take this form:
31 |
32 | 
33 |
34 | ## Step 3: Cool Table!
35 |
36 | Under the `/table` route, create a table with these columns
37 |
38 | 
39 |
40 | and fill it with random data. To make the table cool, add **sort** functionality to each column, i.e, by clicking on each column header, the table should be sorted ascending or descending based on that column.
41 |
42 | ## Step 4: Searchbar
43 |
44 | At this point, we will need our simple backend server, so make sure the Node server in `./backend_server` is up and running. This web server provides two API endpoint:
45 |
46 | 1. The first API endpoint is accessible on `GET: http://localhost:3000/?q=[search query]` which returns a list of countries containing the search query. Example: `http://localhost:3000/?q=ira` returns list of all countries that include `ira` in their names.
47 | 2. The second API endpoint is accessible on `GET: http://localhost:3000/chart/[country code]` which returns the instantaneous fictional currency value of the given country code. Example: `http://localhost:3000/chart/IR` returns a single time point data where the `x` value is time in `HHMM` format and `y` is a fictional currency value.
48 |
49 | Now in the first part, the goal is to create a searchbar where the user can type and see the results in real-time. Create an input and style it to your liking and come up with an elegant way to show the results interactively as the user types a search query in the input. Here, there is no specific design guideline, so make the styling the way you find personally pleasing. It is also necessary to utilize **React Query** for data fetching.
50 |
51 | > **Note:** Reducing and optimizing the number of queries to the server is a definitely a plus
52 |
53 | > **Hint:** Focus more on the UX than the UI
54 |
55 | In the next part, we want to go one step further and add an interactive chart using [Chartjs](https://www.chartjs.org/) to show the fictional currency values of the selected country.
56 |
57 | First make sure this library in installed, then, create the functionality to fetch the currency value data when the user clicks on one of the search results. The currency value data is a time series that is updated every time you call the API and it should be plotted as a line chart. Make sure to call the API for currency data every second to get an updated time series and update the data in the chart accordingly.
58 |
59 | ## Step 5: CI/CD?
60 |
61 | Yes! First, create a unit test for the first part of the **Step 4** and assert the correct rendering of search results when a user input is given. Then, dockerize your backend server and your React web application to run in a production environment.
62 |
--------------------------------------------------------------------------------
/backend_server/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | .gitignore
3 | .eslintrc.json
4 | .prettierrc
5 | config.env
6 | node_modules
--------------------------------------------------------------------------------
/backend_server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb", "prettier", "plugin:node/recommended"],
3 | "plugins": ["prettier"],
4 | "rules": {
5 | "prettier/prettier": "error",
6 | "spaced-comment": "off",
7 | "no-console": "warn",
8 | "consistent-return": "off",
9 | "func-names": "off",
10 | "object-shorthand": "off",
11 | "no-process-exit": "off",
12 | "no-param-reassign": "off",
13 | "no-return-await": "off",
14 | "no-underscore-dangle": "off",
15 | "class-methods-use-this": "off",
16 | "prefer-destructuring": ["error", { "object": true, "array": false }],
17 | "no-unused-vars": ["error", { "argsIgnorePattern": "req|res|next|val" }]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/backend_server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/backend_server/.idea/backend_server.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/backend_server/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/backend_server/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/backend_server/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/backend_server/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/backend_server/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/backend_server/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
56 |
57 |
58 |
59 |
60 | 1686437805377
61 |
62 |
63 | 1686437805377
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/backend_server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/backend_server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:slim
2 |
3 | ENV NODE_ENV development
4 |
5 | WORKDIR /app
6 |
7 | COPY . .
8 |
9 | RUN npm install
10 |
11 | CMD [ "npm", "start" ]
12 |
13 | EXPOSE 3000
14 |
--------------------------------------------------------------------------------
/backend_server/app.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const cors = require('cors');
4 | const express = require('express');
5 | const morgan = require('morgan');
6 | const Fuse = require('fuse.js');
7 |
8 | const app = express();
9 |
10 | app.use(cors());
11 |
12 | if (process.env.NODE_ENV === 'development') {
13 | app.use(morgan('dev'));
14 | }
15 | app.use(express.json());
16 | app.use(express.static(`${__dirname}/public`));
17 |
18 | // reading the file
19 | const countries = JSON.parse(fs.readFileSync(`${__dirname}/countries.json`));
20 |
21 | // searching
22 | const options = {
23 | includeScore: true,
24 | keys: [
25 | {
26 | name: 'name',
27 | weight: 0.99,
28 | },
29 | {
30 | name: 'code',
31 | weight: 0.01,
32 | },
33 | ],
34 | };
35 |
36 | // initialize
37 | const fuse = new Fuse(countries, options);
38 | let fuseResult;
39 |
40 | app.get('/', (req, res) => {
41 | const query = req.query.q;
42 | fuseResult = fuse.search(query).slice(0, 10);
43 |
44 | return res.status(200).json({
45 | status: 'success',
46 | data: {
47 | results: fuseResult,
48 | },
49 | });
50 | });
51 |
52 | let points = [{ x: '2359', y: 1000 }];
53 |
54 | function getRandomInt(min, max) {
55 | min = Math.ceil(min);
56 | max = Math.floor(max);
57 | return Math.floor(Math.random() * (max - min + 1)) + min;
58 | }
59 |
60 | const createNewPoint = () => {
61 | const { x: lastX, y: lastY } = points[points.length - 1];
62 |
63 | const lastXHour = +lastX.substring(0, 2);
64 | const lastXMin = +lastX.substring(2, 4);
65 |
66 | const newXMin = lastXMin + 1 < 60 ? `${lastXMin + 1}`.padStart(2, '0') : '00';
67 | let newXHour =
68 | newXMin !== '00'
69 | ? `${lastXHour}`.padStart(2, '0')
70 | : `${lastXHour + 1}`.padStart(2, '0');
71 | if (newXHour === '24') {
72 | newXHour = '00';
73 | }
74 | const newX = `${newXHour}${newXMin}`;
75 | const newY = getRandomInt(lastY - 10, lastY + 10);
76 | points = [points[points.length - 1], { x: newX, y: newY }];
77 | console.log('Point created: ', points[points.length - 1]);
78 | };
79 |
80 | app.get('/chart/:country', (req, res) => {
81 | const { country } = req.params;
82 | const item = countries.find((el) => el.code === country);
83 |
84 | if (!item) {
85 | res.status(400).json({
86 | status: 'fail',
87 | message: `You didn't provide correct country code`,
88 | });
89 | } else {
90 | console.log('received country: ', item);
91 | createNewPoint();
92 |
93 | res.status(200).json({
94 | status: 'success',
95 | data: points[points.length - 1],
96 | });
97 | }
98 | });
99 |
100 | module.exports = app;
101 |
--------------------------------------------------------------------------------
/backend_server/config.env:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | PORT=3000
3 |
--------------------------------------------------------------------------------
/backend_server/countries.json:
--------------------------------------------------------------------------------
1 | [
2 | { "name": "Afghanistan", "code": "AF" },
3 | { "name": "Åland Islands", "code": "AX" },
4 | { "name": "Albania", "code": "AL" },
5 | { "name": "Algeria", "code": "DZ" },
6 | { "name": "American Samoa", "code": "AS" },
7 | { "name": "AndorrA", "code": "AD" },
8 | { "name": "Angola", "code": "AO" },
9 | { "name": "Anguilla", "code": "AI" },
10 | { "name": "Antarctica", "code": "AQ" },
11 | { "name": "Antigua and Barbuda", "code": "AG" },
12 | { "name": "Argentina", "code": "AR" },
13 | { "name": "Armenia", "code": "AM" },
14 | { "name": "Aruba", "code": "AW" },
15 | { "name": "Australia", "code": "AU" },
16 | { "name": "Austria", "code": "AT" },
17 | { "name": "Azerbaijan", "code": "AZ" },
18 | { "name": "Bahamas", "code": "BS" },
19 | { "name": "Bahrain", "code": "BH" },
20 | { "name": "Bangladesh", "code": "BD" },
21 | { "name": "Barbados", "code": "BB" },
22 | { "name": "Belarus", "code": "BY" },
23 | { "name": "Belgium", "code": "BE" },
24 | { "name": "Belize", "code": "BZ" },
25 | { "name": "Benin", "code": "BJ" },
26 | { "name": "Bermuda", "code": "BM" },
27 | { "name": "Bhutan", "code": "BT" },
28 | { "name": "Bolivia", "code": "BO" },
29 | { "name": "Bosnia and Herzegovina", "code": "BA" },
30 | { "name": "Botswana", "code": "BW" },
31 | { "name": "Bouvet Island", "code": "BV" },
32 | { "name": "Brazil", "code": "BR" },
33 | { "name": "British Indian Ocean Territory", "code": "IO" },
34 | { "name": "Brunei Darussalam", "code": "BN" },
35 | { "name": "Bulgaria", "code": "BG" },
36 | { "name": "Burkina Faso", "code": "BF" },
37 | { "name": "Burundi", "code": "BI" },
38 | { "name": "Cambodia", "code": "KH" },
39 | { "name": "Cameroon", "code": "CM" },
40 | { "name": "Canada", "code": "CA" },
41 | { "name": "Cape Verde", "code": "CV" },
42 | { "name": "Cayman Islands", "code": "KY" },
43 | { "name": "Central African Republic", "code": "CF" },
44 | { "name": "Chad", "code": "TD" },
45 | { "name": "Chile", "code": "CL" },
46 | { "name": "China", "code": "CN" },
47 | { "name": "Christmas Island", "code": "CX" },
48 | { "name": "Cocos (Keeling) Islands", "code": "CC" },
49 | { "name": "Colombia", "code": "CO" },
50 | { "name": "Comoros", "code": "KM" },
51 | { "name": "Congo", "code": "CG" },
52 | { "name": "Congo, The Democratic Republic of the", "code": "CD" },
53 | { "name": "Cook Islands", "code": "CK" },
54 | { "name": "Costa Rica", "code": "CR" },
55 | { "name": "Cote D'Ivoire", "code": "CI" },
56 | { "name": "Croatia", "code": "HR" },
57 | { "name": "Cuba", "code": "CU" },
58 | { "name": "Cyprus", "code": "CY" },
59 | { "name": "Czech Republic", "code": "CZ" },
60 | { "name": "Denmark", "code": "DK" },
61 | { "name": "Djibouti", "code": "DJ" },
62 | { "name": "Dominica", "code": "DM" },
63 | { "name": "Dominican Republic", "code": "DO" },
64 | { "name": "Ecuador", "code": "EC" },
65 | { "name": "Egypt", "code": "EG" },
66 | { "name": "El Salvador", "code": "SV" },
67 | { "name": "Equatorial Guinea", "code": "GQ" },
68 | { "name": "Eritrea", "code": "ER" },
69 | { "name": "Estonia", "code": "EE" },
70 | { "name": "Ethiopia", "code": "ET" },
71 | { "name": "Falkland Islands (Malvinas)", "code": "FK" },
72 | { "name": "Faroe Islands", "code": "FO" },
73 | { "name": "Fiji", "code": "FJ" },
74 | { "name": "Finland", "code": "FI" },
75 | { "name": "France", "code": "FR" },
76 | { "name": "French Guiana", "code": "GF" },
77 | { "name": "French Polynesia", "code": "PF" },
78 | { "name": "French Southern Territories", "code": "TF" },
79 | { "name": "Gabon", "code": "GA" },
80 | { "name": "Gambia", "code": "GM" },
81 | { "name": "Georgia", "code": "GE" },
82 | { "name": "Germany", "code": "DE" },
83 | { "name": "Ghana", "code": "GH" },
84 | { "name": "Gibraltar", "code": "GI" },
85 | { "name": "Greece", "code": "GR" },
86 | { "name": "Greenland", "code": "GL" },
87 | { "name": "Grenada", "code": "GD" },
88 | { "name": "Guadeloupe", "code": "GP" },
89 | { "name": "Guam", "code": "GU" },
90 | { "name": "Guatemala", "code": "GT" },
91 | { "name": "Guernsey", "code": "GG" },
92 | { "name": "Guinea", "code": "GN" },
93 | { "name": "Guinea-Bissau", "code": "GW" },
94 | { "name": "Guyana", "code": "GY" },
95 | { "name": "Haiti", "code": "HT" },
96 | { "name": "Heard Island and Mcdonald Islands", "code": "HM" },
97 | { "name": "Holy See (Vatican City State)", "code": "VA" },
98 | { "name": "Honduras", "code": "HN" },
99 | { "name": "Hong Kong", "code": "HK" },
100 | { "name": "Hungary", "code": "HU" },
101 | { "name": "Iceland", "code": "IS" },
102 | { "name": "India", "code": "IN" },
103 | { "name": "Indonesia", "code": "ID" },
104 | { "name": "Iran, Islamic Republic Of", "code": "IR" },
105 | { "name": "Iraq", "code": "IQ" },
106 | { "name": "Ireland", "code": "IE" },
107 | { "name": "Isle of Man", "code": "IM" },
108 | { "name": "Israel", "code": "IL" },
109 | { "name": "Italy", "code": "IT" },
110 | { "name": "Jamaica", "code": "JM" },
111 | { "name": "Japan", "code": "JP" },
112 | { "name": "Jersey", "code": "JE" },
113 | { "name": "Jordan", "code": "JO" },
114 | { "name": "Kazakhstan", "code": "KZ" },
115 | { "name": "Kenya", "code": "KE" },
116 | { "name": "Kiribati", "code": "KI" },
117 | { "name": "Korea, Democratic People'S Republic of", "code": "KP" },
118 | { "name": "Korea, Republic of", "code": "KR" },
119 | { "name": "Kuwait", "code": "KW" },
120 | { "name": "Kyrgyzstan", "code": "KG" },
121 | { "name": "Lao People'S Democratic Republic", "code": "LA" },
122 | { "name": "Latvia", "code": "LV" },
123 | { "name": "Lebanon", "code": "LB" },
124 | { "name": "Lesotho", "code": "LS" },
125 | { "name": "Liberia", "code": "LR" },
126 | { "name": "Libyan Arab Jamahiriya", "code": "LY" },
127 | { "name": "Liechtenstein", "code": "LI" },
128 | { "name": "Lithuania", "code": "LT" },
129 | { "name": "Luxembourg", "code": "LU" },
130 | { "name": "Macao", "code": "MO" },
131 | { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" },
132 | { "name": "Madagascar", "code": "MG" },
133 | { "name": "Malawi", "code": "MW" },
134 | { "name": "Malaysia", "code": "MY" },
135 | { "name": "Maldives", "code": "MV" },
136 | { "name": "Mali", "code": "ML" },
137 | { "name": "Malta", "code": "MT" },
138 | { "name": "Marshall Islands", "code": "MH" },
139 | { "name": "Martinique", "code": "MQ" },
140 | { "name": "Mauritania", "code": "MR" },
141 | { "name": "Mauritius", "code": "MU" },
142 | { "name": "Mayotte", "code": "YT" },
143 | { "name": "Mexico", "code": "MX" },
144 | { "name": "Micronesia, Federated States of", "code": "FM" },
145 | { "name": "Moldova, Republic of", "code": "MD" },
146 | { "name": "Monaco", "code": "MC" },
147 | { "name": "Mongolia", "code": "MN" },
148 | { "name": "Montserrat", "code": "MS" },
149 | { "name": "Morocco", "code": "MA" },
150 | { "name": "Mozambique", "code": "MZ" },
151 | { "name": "Myanmar", "code": "MM" },
152 | { "name": "Namibia", "code": "NA" },
153 | { "name": "Nauru", "code": "NR" },
154 | { "name": "Nepal", "code": "NP" },
155 | { "name": "Netherlands", "code": "NL" },
156 | { "name": "Netherlands Antilles", "code": "AN" },
157 | { "name": "New Caledonia", "code": "NC" },
158 | { "name": "New Zealand", "code": "NZ" },
159 | { "name": "Nicaragua", "code": "NI" },
160 | { "name": "Niger", "code": "NE" },
161 | { "name": "Nigeria", "code": "NG" },
162 | { "name": "Niue", "code": "NU" },
163 | { "name": "Norfolk Island", "code": "NF" },
164 | { "name": "Northern Mariana Islands", "code": "MP" },
165 | { "name": "Norway", "code": "NO" },
166 | { "name": "Oman", "code": "OM" },
167 | { "name": "Pakistan", "code": "PK" },
168 | { "name": "Palau", "code": "PW" },
169 | { "name": "Palestinian Territory, Occupied", "code": "PS" },
170 | { "name": "Panama", "code": "PA" },
171 | { "name": "Papua New Guinea", "code": "PG" },
172 | { "name": "Paraguay", "code": "PY" },
173 | { "name": "Peru", "code": "PE" },
174 | { "name": "Philippines", "code": "PH" },
175 | { "name": "Pitcairn", "code": "PN" },
176 | { "name": "Poland", "code": "PL" },
177 | { "name": "Portugal", "code": "PT" },
178 | { "name": "Puerto Rico", "code": "PR" },
179 | { "name": "Qatar", "code": "QA" },
180 | { "name": "Reunion", "code": "RE" },
181 | { "name": "Romania", "code": "RO" },
182 | { "name": "Russian Federation", "code": "RU" },
183 | { "name": "RWANDA", "code": "RW" },
184 | { "name": "Saint Helena", "code": "SH" },
185 | { "name": "Saint Kitts and Nevis", "code": "KN" },
186 | { "name": "Saint Lucia", "code": "LC" },
187 | { "name": "Saint Pierre and Miquelon", "code": "PM" },
188 | { "name": "Saint Vincent and the Grenadines", "code": "VC" },
189 | { "name": "Samoa", "code": "WS" },
190 | { "name": "San Marino", "code": "SM" },
191 | { "name": "Sao Tome and Principe", "code": "ST" },
192 | { "name": "Saudi Arabia", "code": "SA" },
193 | { "name": "Senegal", "code": "SN" },
194 | { "name": "Serbia and Montenegro", "code": "CS" },
195 | { "name": "Seychelles", "code": "SC" },
196 | { "name": "Sierra Leone", "code": "SL" },
197 | { "name": "Singapore", "code": "SG" },
198 | { "name": "Slovakia", "code": "SK" },
199 | { "name": "Slovenia", "code": "SI" },
200 | { "name": "Solomon Islands", "code": "SB" },
201 | { "name": "Somalia", "code": "SO" },
202 | { "name": "South Africa", "code": "ZA" },
203 | { "name": "South Georgia and the South Sandwich Islands", "code": "GS" },
204 | { "name": "Spain", "code": "ES" },
205 | { "name": "Sri Lanka", "code": "LK" },
206 | { "name": "Sudan", "code": "SD" },
207 | { "name": "Suriname", "code": "SR" },
208 | { "name": "Svalbard and Jan Mayen", "code": "SJ" },
209 | { "name": "Swaziland", "code": "SZ" },
210 | { "name": "Sweden", "code": "SE" },
211 | { "name": "Switzerland", "code": "CH" },
212 | { "name": "Syrian Arab Republic", "code": "SY" },
213 | { "name": "Taiwan, Province of China", "code": "TW" },
214 | { "name": "Tajikistan", "code": "TJ" },
215 | { "name": "Tanzania, United Republic of", "code": "TZ" },
216 | { "name": "Thailand", "code": "TH" },
217 | { "name": "Timor-Leste", "code": "TL" },
218 | { "name": "Togo", "code": "TG" },
219 | { "name": "Tokelau", "code": "TK" },
220 | { "name": "Tonga", "code": "TO" },
221 | { "name": "Trinidad and Tobago", "code": "TT" },
222 | { "name": "Tunisia", "code": "TN" },
223 | { "name": "Turkey", "code": "TR" },
224 | { "name": "Turkmenistan", "code": "TM" },
225 | { "name": "Turks and Caicos Islands", "code": "TC" },
226 | { "name": "Tuvalu", "code": "TV" },
227 | { "name": "Uganda", "code": "UG" },
228 | { "name": "Ukraine", "code": "UA" },
229 | { "name": "United Arab Emirates", "code": "AE" },
230 | { "name": "United Kingdom", "code": "GB" },
231 | { "name": "United States", "code": "US" },
232 | { "name": "United States Minor Outlying Islands", "code": "UM" },
233 | { "name": "Uruguay", "code": "UY" },
234 | { "name": "Uzbekistan", "code": "UZ" },
235 | { "name": "Vanuatu", "code": "VU" },
236 | { "name": "Venezuela", "code": "VE" },
237 | { "name": "Viet Nam", "code": "VN" },
238 | { "name": "Virgin Islands, British", "code": "VG" },
239 | { "name": "Virgin Islands, U.S.", "code": "VI" },
240 | { "name": "Wallis and Futuna", "code": "WF" },
241 | { "name": "Western Sahara", "code": "EH" },
242 | { "name": "Yemen", "code": "YE" },
243 | { "name": "Zambia", "code": "ZM" },
244 | { "name": "Zimbabwe", "code": "ZW" }
245 | ]
246 |
--------------------------------------------------------------------------------
/backend_server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chartbackend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "nodemon server.js"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "cors": "^2.8.5",
13 | "dotenv": "^16.0.3",
14 | "eslint": "^8.41.0",
15 | "eslint-config-airbnb": "^19.0.4",
16 | "eslint-config-prettier": "^8.8.0",
17 | "eslint-plugin-import": "^2.27.5",
18 | "eslint-plugin-jsx-a11y": "^6.7.1",
19 | "eslint-plugin-node": "^11.1.0",
20 | "eslint-plugin-prettier": "^4.2.1",
21 | "eslint-plugin-react": "^7.32.2",
22 | "express": "^4.18.2",
23 | "fuse.js": "^6.6.2",
24 | "morgan": "*",
25 | "nodemon": "^2.0.22",
26 | "prettier": "^2.8.8"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/backend_server/server.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv');
2 |
3 | // bringing env vars to the rest of app
4 | dotenv.config({ path: './config.env' });
5 |
6 | const app = require('./app');
7 |
8 | const port = process.env.PORT || 3000;
9 | app.listen(process.env.PORT, () => {
10 | console.log(`App running on port ${port}...`);
11 | });
12 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react_javascript_test",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {}
6 | }
7 |
--------------------------------------------------------------------------------
/pics/Layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/younes-nb/react_javascript_test/e3f97416bf78ed981a66c95e96b5cfced6a96fb6/pics/Layout.png
--------------------------------------------------------------------------------
/pics/Layout_Resize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/younes-nb/react_javascript_test/e3f97416bf78ed981a66c95e96b5cfced6a96fb6/pics/Layout_Resize.png
--------------------------------------------------------------------------------
/pics/Table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/younes-nb/react_javascript_test/e3f97416bf78ed981a66c95e96b5cfced6a96fb6/pics/Table.png
--------------------------------------------------------------------------------
/react_project/.dockerignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | .idea
3 | .eslintrc.cjs
4 | .dockerignore
5 | vite.config.js
6 | jest.config.cjs
7 | babel.config.cjs
8 | node_modules
9 |
10 |
--------------------------------------------------------------------------------
/react_project/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true, jest: true },
3 | extends: [
4 | "eslint:recommended",
5 | "plugin:react/recommended",
6 | "plugin:react/jsx-runtime",
7 | "plugin:react-hooks/recommended",
8 | ],
9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
10 | settings: { react: { version: "18.2" } },
11 | plugins: ["react-refresh", "jest"],
12 | rules: {
13 | "react-refresh/only-export-components": "warn",
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/react_project/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/react_project/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node
2 |
3 | WORKDIR /app
4 |
5 | COPY package.json .
6 | RUN npm i
7 |
8 | COPY . .
9 |
10 | EXPOSE 5173
11 |
12 | CMD ["npm", "run", "dev"]
--------------------------------------------------------------------------------
/react_project/babel.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@babel/preset-env',
4 | ['@babel/preset-react', {runtime: 'automatic'}],
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/react_project/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 | Interview
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/react_project/jest.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleNameMapper: {
3 | "\\.(css|less|sass|scss)$": "identity-obj-proxy",
4 | },
5 | testEnvironment: "jsdom",
6 | };
7 |
--------------------------------------------------------------------------------
/react_project/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview",
11 | "test": "jest"
12 | },
13 | "dependencies": {
14 | "@emotion/react": "^11.11.1",
15 | "@emotion/styled": "^11.11.0",
16 | "@mui/icons-material": "^5.11.16",
17 | "@mui/material": "^5.13.4",
18 | "@tanstack/react-query": "^4.29.12",
19 | "@tanstack/react-query-devtools": "^4.29.12",
20 | "chart.js": "^4.3.0",
21 | "classnames": "^2.3.2",
22 | "framer-motion": "^10.12.16",
23 | "react": "^18.2.0",
24 | "react-dom": "^18.2.0",
25 | "react-query": "^3.39.3",
26 | "react-router-dom": "^6.11.2",
27 | "react-table": "^7.8.0",
28 | "use-debounce": "^9.0.4"
29 | },
30 | "devDependencies": {
31 | "@babel/preset-env": "^7.22.5",
32 | "@babel/preset-react": "^7.22.5",
33 | "@testing-library/jest-dom": "^5.16.5",
34 | "@testing-library/react": "^14.0.0",
35 | "@types/react": "^18.0.37",
36 | "@types/react-dom": "^18.0.11",
37 | "@vitejs/plugin-react": "^4.0.0",
38 | "babel-jest": "^29.5.0",
39 | "eslint": "^8.38.0",
40 | "eslint-plugin-jest": "^27.2.1",
41 | "eslint-plugin-react": "^7.32.2",
42 | "eslint-plugin-react-hooks": "^4.6.0",
43 | "eslint-plugin-react-refresh": "^0.3.4",
44 | "identity-obj-proxy": "^3.0.0",
45 | "jest": "^29.5.0",
46 | "jest-environment-jsdom": "^29.5.0",
47 | "react-test-renderer": "^18.2.0",
48 | "vite": "^4.3.9"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/react_project/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/react_project/src/App.jsx:
--------------------------------------------------------------------------------
1 | import Navbar from "./components/navbar/navbar.jsx";
2 | import {Outlet} from "react-router-dom";
3 |
4 | function App() {
5 | return (<>
6 |
7 |
8 | >
9 | )
10 | }
11 |
12 | export default App;
13 |
--------------------------------------------------------------------------------
/react_project/src/assets/emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/younes-nb/react_javascript_test/e3f97416bf78ed981a66c95e96b5cfced6a96fb6/react_project/src/assets/emoji.png
--------------------------------------------------------------------------------
/react_project/src/assets/react_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/younes-nb/react_javascript_test/e3f97416bf78ed981a66c95e96b5cfced6a96fb6/react_project/src/assets/react_logo.png
--------------------------------------------------------------------------------
/react_project/src/components/dynamic-table/dynamic-table.jsx:
--------------------------------------------------------------------------------
1 | import {useTable, useSortBy} from "react-table";
2 | import PropTypes from "prop-types";
3 | import cx from "classnames";
4 | import styles from "./dynamic-table.module.css";
5 |
6 | function DynamicTable({columns, data, onRowClick, selectedRow}) {
7 | const {getTableProps, getTableBodyProps, headerGroups, rows, prepareRow} =
8 | useTable({columns, data}, useSortBy);
9 |
10 | DynamicTable.propTypes = {
11 | columns: PropTypes.arrayOf(
12 | PropTypes.shape({
13 | Header: PropTypes.string,
14 | accessor: PropTypes.string,
15 | })
16 | ).isRequired,
17 | data: PropTypes.arrayOf(PropTypes.object).isRequired,
18 | onRowClick: PropTypes.func.isRequired,
19 | selectedRow: PropTypes.string,
20 | };
21 |
22 | return (
23 |
24 |
25 |
26 | {headerGroups.map((headerGroup) => (
27 |
28 | {headerGroup.headers.map((column) => (
29 |
33 | {column.render("Header")}
34 |
35 | {column.isSorted ? (column.isSortedDesc ? " ↓" : " ↑") : ""}
36 |
37 |
38 | ))}
39 |
40 | ))}
41 |
42 |
43 | {rows.map((row) => {
44 | prepareRow(row);
45 | const isRowSelected = row.original.code === selectedRow;
46 | const rowClasses = cx({
47 | [styles.selectedRow]: isRowSelected,
48 | });
49 | return (
50 | onRowClick(row.original)}
54 | className={rowClasses}
55 | >
56 | {row.cells.map((cell) => (
57 |
58 | {cell.render("Cell")}
59 |
60 | ))}
61 |
62 | );
63 | })}
64 |
65 |
66 |
67 | );
68 | }
69 |
70 | export default DynamicTable;
71 |
--------------------------------------------------------------------------------
/react_project/src/components/dynamic-table/dynamic-table.module.css:
--------------------------------------------------------------------------------
1 | .tableWrapper {
2 | max-height: 70vh;
3 | overflow: auto;
4 | border-radius: 8px;
5 | box-shadow: var(--box-shadow-dark);
6 | }
7 |
8 | .table {
9 | width: 100%;
10 | margin: 0 auto;
11 | border-collapse: separate;
12 | border-spacing: 0;
13 | }
14 |
15 | .table thead th {
16 | background-color: var(--clr-blue-primary);
17 | color: var(--clr-white);
18 | position: sticky;
19 | top: 0;
20 | font-family: serif;
21 | }
22 |
23 | .table th,
24 | .table td {
25 | padding: var(--spacing-3) 0 var(--spacing-3) var(--spacing-3);
26 | text-align: right;
27 | vertical-align: middle;
28 | border-top: 1px solid var(--clr-gray-200);
29 | cursor: pointer;
30 | }
31 |
32 | .table tbody tr:nth-child(even) {
33 | background-color: var(--clr-gray-100);
34 | }
35 |
36 | .table tbody tr:hover {
37 | background-color: var(--clr-blue-tint);
38 | }
39 |
40 | .table td:first-child,
41 | .table th:first-child {
42 | padding-right: var(--spacing-5);
43 | }
44 |
45 | .selectedRow {
46 | color: var(--clr-blue-primary);
47 | }
48 |
--------------------------------------------------------------------------------
/react_project/src/components/error-page/error-page.jsx:
--------------------------------------------------------------------------------
1 | import {Link} from "react-router-dom";
2 | import styles from "./error-page.module.css"
3 | import {Button} from "@mui/material";
4 |
5 | export default function ErrorPage() {
6 |
7 | return (
8 |
9 |
10 |
11 |
404
12 |
Oops! Page Not Be Found
13 |
Sorry but the page you are looking for does not exist, have been removed. name changed or is temporarily
14 | unavailable
15 |
Back to homepage
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/react_project/src/components/error-page/error-page.module.css:
--------------------------------------------------------------------------------
1 | .notfoundContainer {
2 | position: relative;
3 | height: 100vh;
4 | }
5 |
6 | .notfoundContainer .notfound {
7 | position: absolute;
8 | left: 50%;
9 | top: 50%;
10 | transform: translate(-50%, -50%);
11 | }
12 |
13 | .notfound {
14 | max-width: 560px;
15 | width: 100%;
16 | padding-left: 160px;
17 | line-height: 1.1;
18 | }
19 |
20 | .notfound .notfound404 {
21 | position: absolute;
22 | left: 0;
23 | top: 0;
24 | display: inline-block;
25 | width: 140px;
26 | height: 140px;
27 | background-image: url('../../assets/emoji.png');
28 | background-size: cover;
29 | }
30 |
31 | .notfound .notfound404:before {
32 | content: '';
33 | position: absolute;
34 | width: 100%;
35 | height: 100%;
36 | transform: scale(2.4);
37 | border-radius: 50%;
38 | background-color: #f2f5f8;
39 | z-index: -1;
40 | }
41 |
42 | .notfound h1 {
43 | font-family: 'Nunito', sans-serif;
44 | font-size: 65px;
45 | font-weight: 700;
46 | margin-top: 0;
47 | margin-bottom: 10px;
48 | color: #151723;
49 | text-transform: uppercase;
50 | }
51 |
52 | .notfound h2 {
53 | font-family: 'Nunito', sans-serif;
54 | font-size: 21px;
55 | font-weight: 400;
56 | margin: 0;
57 | text-transform: uppercase;
58 | color: #151723;
59 | }
60 |
61 | .notfound p {
62 | font-family: 'Nunito', sans-serif;
63 | color: #999fa5;
64 | font-weight: 400;
65 | }
66 |
67 | .notfound .back {
68 | font-weight: 700;
69 | font-size: 1.2rem;
70 | padding-left: 0;
71 | }
72 |
73 | .notfound .back a {
74 | text-decoration: none;
75 | color: #388dbc;
76 | }
77 |
78 | @media only screen and (max-width: 767px) {
79 | .notfound .notfound404 {
80 | width: 110px;
81 | height: 110px;
82 | }
83 |
84 | .notfound {
85 | padding-left: 15px;
86 | padding-right: 15px;
87 | padding-top: 110px;
88 | }
89 | }
--------------------------------------------------------------------------------
/react_project/src/components/navbar/navbar.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import AppBar from '@mui/material/AppBar';
3 | import Box from '@mui/material/Box';
4 | import Toolbar from '@mui/material/Toolbar';
5 | import IconButton from '@mui/material/IconButton';
6 | import Typography from '@mui/material/Typography';
7 | import Menu from '@mui/material/Menu';
8 | import MenuIcon from '@mui/icons-material/Menu';
9 | import Container from '@mui/material/Container';
10 | import Button from '@mui/material/Button';
11 | import MenuItem from '@mui/material/MenuItem';
12 | import AdbIcon from '@mui/icons-material/Adb';
13 | import {Link} from "react-router-dom";
14 | import logo from "../../assets/react_logo.png"
15 |
16 | const pages = [{
17 | link: '/',
18 | name: 'Home'
19 | }, {link: '/layout', name: 'Layout'}, {link: '/table', name: 'Table'}, {link: '/searchbar', name: 'Search'}];
20 |
21 | function Navbar() {
22 | const [anchorElNav, setAnchorElNav] = React.useState(null);
23 |
24 | const handleOpenNavMenu = (event) => {
25 | setAnchorElNav(event.currentTarget);
26 | };
27 |
28 | const handleCloseNavMenu = () => {
29 | setAnchorElNav(null);
30 | };
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
46 |
47 |
48 |
72 |
73 |
74 |
90 | LOGO
91 |
92 |
93 | {pages.map((page) => (
94 |
99 | {page.name}
100 |
101 | ))}
102 |
103 |
104 |
105 |
106 | );
107 | }
108 |
109 | export default Navbar;
110 |
--------------------------------------------------------------------------------
/react_project/src/containers/home/home.jsx:
--------------------------------------------------------------------------------
1 | import styles from "./home.module.css";
2 | import {Link} from "react-router-dom";
3 | import logo from "../../assets/react_logo.png"
4 |
5 | function Home() {
6 |
7 | return (
8 | <>
9 |
10 |
11 |
12 | React Interview Test Project
13 |
14 |
15 | Source Code
16 | Layout
17 | Table
18 | Search
19 |
20 |
21 |
22 |
23 | >
24 | )
25 | }
26 |
27 | export default Home;
--------------------------------------------------------------------------------
/react_project/src/containers/home/home.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | width: min(90%, 114rem);
3 | padding: var(--spacing-6) var(--spacing-4);
4 | margin: 0 auto;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | align-items: center;
9 | }
10 |
11 | .main h1 {
12 | margin-top: var(--spacing-5);
13 | }
14 |
15 | .instructions {
16 | margin-top: var(--spacing-3);
17 | }
18 |
19 | .list {
20 | margin-top: var(--spacing-5);
21 | }
22 |
23 | .list > * {
24 | font-size: var(--fs-2);
25 | text-decoration: none;
26 | color: var(--clr-black);
27 | padding-left: var(--spacing-3);
28 | cursor: pointer;
29 | transition: color 0.3s;
30 | }
31 |
32 | .list > *:hover {
33 | color: var(--clr-blue-primary);
34 | }
35 |
--------------------------------------------------------------------------------
/react_project/src/containers/layout/layout.jsx:
--------------------------------------------------------------------------------
1 | import {useEffect, useRef} from "react";
2 | import styles from "./layout.module.css";
3 |
4 | function Layout() {
5 | const mainRef = useRef(null);
6 | const layoutRef = useRef(null);
7 |
8 | useEffect(() => {
9 | function handleResize() {
10 | if (mainRef.current) {
11 | const mainWidth = mainRef.current.offsetWidth;
12 | if (mainWidth <= 500) {
13 | layoutRef.current.classList.add(styles.mobileLayout);
14 | } else {
15 | layoutRef.current.classList.remove(styles.mobileLayout);
16 | }
17 | }
18 | }
19 |
20 | window.addEventListener("resize", handleResize);
21 | handleResize();
22 | return () => window.removeEventListener("resize", handleResize);
23 | }, []);
24 |
25 | return (
26 | <>
27 |
28 |
29 |
30 | متن اصلی
31 |
32 | لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با
33 | استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله
34 | در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد
35 | نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد،
36 | کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان
37 | جامعه و متخصصان را می طلبد، تا با نرم افزارها شناخت بیشتری را برای
38 | طراحان رایانه ای علی الخصوص طراحان خلاقی، و فرهنگ پیشرو در زبان
39 | فارسی ایجاد کرد، در این صورت می توان امید داشت که تمام و دشواری
40 | موجود در ارائه راهکارها، و شرایط سخت تایپ به پایان رسد و زمان مورد
41 | نیاز شامل حروفچینی دستاوردهای اصلی، و جوابگوی سوالات پیوسته اهل
42 | دنیای موجود طراحی اساسا مورد استفاده قرار گیرد.لورم ایپسوم متن
43 | ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان
44 | گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان
45 | که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای
46 | متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت
47 | و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می
48 | طلبد، تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی
49 | الخصوص طراحان خلاقی، و فرهنگ پیشرو در زبان فارسی ایجاد کرد، در این
50 | صورت می توان امید داشت که تمام و دشواری موجود در ارائه راهکارها، و
51 | شرایط سخت تایپ به پایان رسد و زمان مورد نیاز شامل حروفچینی
52 | دستاوردهای اصلی، و جوابگوی سوالات پیوسته اهل دنیای موجود طراحی
53 | اساسا مورد استفاده قرار گیرد.
54 |
55 |
56 |
57 | متن کناری
58 |
59 | لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با
60 | استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله
61 | در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد
62 | نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد،
63 | کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان
64 | جامعه و متخصصان را می طلبد، تا با نرم افزارها شناخت بیشتری را برای
65 | طراحان رایانه ای علی الخصوص طراحان خلاقی، و فرهنگ پیشرو در زبان
66 | فارسی ایجاد کرد، در این صورت می توان امید داشت که تمام و دشواری
67 | موجود در ارائه راهکارها، و شرایط سخت تایپ به پایان رسد و زمان مورد
68 | نیاز شامل حروفچینی دستاوردهای اصلی، و جوابگوی سوالات پیوسته اهل
69 | دنیای موجود طراحی اساسا مورد استفاده قرار گیرد.
70 |
71 |
72 |
73 |
74 | >
75 | );
76 | }
77 |
78 | export default Layout;
79 |
--------------------------------------------------------------------------------
/react_project/src/containers/layout/layout.module.css:
--------------------------------------------------------------------------------
1 | .layoutContainer {
2 | display: grid;
3 | grid-template-columns: 1fr 350px;
4 | column-gap: var(--spacing-6);
5 | font-family: serif;
6 | }
7 |
8 | .mobileLayout {
9 | grid-template-columns: 1fr;
10 | grid-template-rows: auto auto;
11 | column-gap: 0;
12 | row-gap: var(--spacing-6);
13 | }
14 |
15 | .main,
16 | .sidebar {
17 | border-radius: 10px;
18 | box-shadow: var(--box-shadow-light);
19 | padding: var(--spacing-5);
20 | text-align: justify;
21 | }
22 |
23 | .main {
24 | background-color: var(--clr-gray-100);
25 | }
26 |
27 | .sidebar {
28 | background-color: var(--clr-gray-200);
29 | }
30 |
--------------------------------------------------------------------------------
/react_project/src/containers/search-bar/search-bar.jsx:
--------------------------------------------------------------------------------
1 | import {TextField, CircularProgress} from '@mui/material';
2 | import {useEffect, useRef, useState} from 'react';
3 | import DynamicTable from '../../components/dynamic-table/dynamic-table.jsx';
4 | import styles from './search-bar.module.css';
5 | import Chart from 'chart.js/auto';
6 | import {useSearch} from '../../utils/use-search.js';
7 | import {QueryClient, QueryClientProvider} from "react-query";
8 | import {getCurrencyDataPoint} from "../../utils/currency-data.js";
9 |
10 | const queryClient = new QueryClient();
11 | const SearchBar = () => {
12 | const tableColumns = [
13 | {Header: 'کد', accessor: 'code'},
14 | {Header: 'کشور', accessor: 'name'},
15 | ];
16 |
17 | const {
18 | searchQuery,
19 | setSearchQuery,
20 | searchResults,
21 | isLoading,
22 | } = useSearch();
23 |
24 | const [currencyData, setCurrencyData] = useState([]);
25 | const [countryCode, setCountryCode] = useState(null);
26 | const chartRef = useRef(null);
27 |
28 | const handleInputChange = (event) => {
29 | setSearchQuery(event.target.value);
30 | };
31 |
32 | useEffect(() => {
33 | if (!chartRef.current) return;
34 |
35 | const ctx = chartRef.current.getContext('2d');
36 | const chart = new Chart(ctx, {
37 | type: 'line',
38 | data: {
39 | labels: currencyData.map((point) => point.x),
40 | datasets: [
41 | {
42 | label: `Fictional Currency Value of ${countryCode}`,
43 | data: currencyData.map((point) => point.y),
44 | borderColor: 'rgb(89, 65, 205)',
45 | fill: false,
46 | },
47 | ],
48 | },
49 | options: {
50 | responsive: true,
51 | interaction: {
52 | intersect: false,
53 | },
54 | scales: {
55 | x: {
56 | display: true,
57 | title: {
58 | display: true,
59 | text: 'Time',
60 | },
61 | ticks: {
62 | precision: 0,
63 | },
64 | },
65 | y: {
66 | display: true,
67 | title: {
68 | display: true,
69 | text: 'Currency Value',
70 | },
71 | },
72 | },
73 | },
74 | });
75 |
76 | let intervalId = null;
77 |
78 | const updateChart = async () => {
79 | if (
80 | currencyData.length === 0 ||
81 | currencyData.length >= 20 ||
82 | !countryCode
83 | )
84 | return;
85 |
86 | try {
87 | const currencyDataPoint = await getCurrencyDataPoint(countryCode);
88 | setCurrencyData((prevData) => [...prevData, currencyDataPoint]);
89 | } catch (error) {
90 | console.error('Error fetching currency data:', error);
91 | }
92 | };
93 |
94 | intervalId = setInterval(updateChart, 1000);
95 |
96 | return () => {
97 | clearInterval(intervalId);
98 | chart.destroy();
99 | };
100 | }, [currencyData, countryCode]);
101 |
102 | let textFieldColor = 'primary';
103 | if (searchQuery && searchQuery.trim() !== '') {
104 | if (isLoading) {
105 | textFieldColor = 'info';
106 | } else if (Array.isArray(searchResults) && searchResults.length > 0) {
107 | textFieldColor = 'success';
108 | } else {
109 | textFieldColor = 'warning';
110 | }
111 | }
112 |
113 | const handleRowClick = async (row) => {
114 | setCountryCode(row.code);
115 | setCurrencyData([await getCurrencyDataPoint(row.code)]);
116 | };
117 |
118 | return (
119 |
120 |
129 |
130 | {isLoading ? (
131 |
132 | ) : searchResults.length > 0 ? (
133 |
134 |
135 |
142 |
143 |
144 | {currencyData.length > 0 ? (
145 |
146 | ) : (
147 |
148 | Select a country to see the fictional currency values of it.
149 |
150 | )}
151 |
152 |
153 | ) : searchQuery.trim() !== '' ? (
154 |
No results found.
155 | ) : (
156 |
Nothing to show.
157 | )}
158 |
159 |
160 | );
161 | };
162 |
163 | const SearchBarApp = () => {
164 | return (
165 |
166 |
167 |
168 | );
169 | };
170 | export default SearchBarApp;
171 |
--------------------------------------------------------------------------------
/react_project/src/containers/search-bar/search-bar.module.css:
--------------------------------------------------------------------------------
1 | .searchBar {
2 | width: 50%;
3 | }
4 |
5 | .result {
6 | margin-top: var(--spacing-6);
7 | }
8 |
9 | .noResults,
10 | .nothingToShow {
11 | color: var(--clr-gray-600);
12 | }
13 |
14 | .nothingToShow {
15 | text-align: center;
16 | margin-top: var(--spacing-7);
17 | font-size: var(--fs-4);
18 | }
19 |
20 | .resultContainer {
21 | display: flex;
22 | flex-direction: row;
23 | align-items: flex-start;
24 | justify-content: space-between;
25 | }
26 |
27 | .tableContainer {
28 | flex: 1;
29 | margin-right: 16px;
30 | }
31 |
32 | .chartContainer {
33 | flex: 3;
34 | margin-left: 16px;
35 | }
36 |
37 | .chart {
38 | width: 100%;
39 | height: auto;
40 | }
41 |
--------------------------------------------------------------------------------
/react_project/src/containers/table/table-data.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "code": "1",
4 | "fullName": "Ulises Halksworth",
5 | "employeeNumber": "3061135006",
6 | "employmentDate": "4/6/2021",
7 | "serviceHistory": 27
8 | },
9 | {
10 | "code": "2",
11 | "fullName": "Charyl Cossey",
12 | "employeeNumber": "7519851222",
13 | "employmentDate": "7/16/2020",
14 | "serviceHistory": 6
15 | },
16 | {
17 | "code": "3",
18 | "fullName": "Crin Bowring",
19 | "employeeNumber": "4001702398",
20 | "employmentDate": "1/23/2023",
21 | "serviceHistory": 3
22 | },
23 | {
24 | "code": "4",
25 | "fullName": "Kara Boullin",
26 | "employeeNumber": "9865475545",
27 | "employmentDate": "7/21/2020",
28 | "serviceHistory": 26
29 | },
30 | {
31 | "code": "5",
32 | "fullName": "Trev Wonter",
33 | "employeeNumber": "7806311866",
34 | "employmentDate": "6/18/2020",
35 | "serviceHistory": 8
36 | },
37 | {
38 | "code": "6",
39 | "fullName": "Flem Currier",
40 | "employeeNumber": "0592093506",
41 | "employmentDate": "1/24/2021",
42 | "serviceHistory": 27
43 | },
44 | {
45 | "code": "7",
46 | "fullName": "Hattie Shipcott",
47 | "employeeNumber": "5073071646",
48 | "employmentDate": "12/13/2020",
49 | "serviceHistory": 15
50 | },
51 | {
52 | "code": "8",
53 | "fullName": "Theobald Betty",
54 | "employeeNumber": "9642991772",
55 | "employmentDate": "11/10/2021",
56 | "serviceHistory": 5
57 | },
58 | {
59 | "code": "9",
60 | "fullName": "Gustavo Mishow",
61 | "employeeNumber": "7866493588",
62 | "employmentDate": "4/15/2022",
63 | "serviceHistory": 27
64 | },
65 | {
66 | "code": "10",
67 | "fullName": "Von Morena",
68 | "employeeNumber": "6704931678",
69 | "employmentDate": "1/30/2023",
70 | "serviceHistory": 16
71 | },
72 | {
73 | "code": "11",
74 | "fullName": "Stern Conquest",
75 | "employeeNumber": "7574549125",
76 | "employmentDate": "9/27/2021",
77 | "serviceHistory": 2
78 | },
79 | {
80 | "code": "12",
81 | "fullName": "Nicolle Thrower",
82 | "employeeNumber": "1078701210",
83 | "employmentDate": "1/8/2023",
84 | "serviceHistory": 7
85 | },
86 | {
87 | "code": "13",
88 | "fullName": "Arron Kryska",
89 | "employeeNumber": "3050182849",
90 | "employmentDate": "8/9/2020",
91 | "serviceHistory": 11
92 | },
93 | {
94 | "code": "14",
95 | "fullName": "Phyllis Hellin",
96 | "employeeNumber": "7153009345",
97 | "employmentDate": "8/31/2020",
98 | "serviceHistory": 17
99 | },
100 | {
101 | "code": "15",
102 | "fullName": "Michal Guesford",
103 | "employeeNumber": "1308312989",
104 | "employmentDate": "3/13/2022",
105 | "serviceHistory": 30
106 | },
107 | {
108 | "code": "16",
109 | "fullName": "Tybalt Bertot",
110 | "employeeNumber": "1143667263",
111 | "employmentDate": "1/21/2023",
112 | "serviceHistory": 2
113 | },
114 | {
115 | "code": "17",
116 | "fullName": "Carter Butchart",
117 | "employeeNumber": "7505333909",
118 | "employmentDate": "5/18/2023",
119 | "serviceHistory": 26
120 | },
121 | {
122 | "code": "18",
123 | "fullName": "Cathleen Cawdron",
124 | "employeeNumber": "0232596700",
125 | "employmentDate": "5/5/2022",
126 | "serviceHistory": 27
127 | },
128 | {
129 | "code": "19",
130 | "fullName": "Kate Vokes",
131 | "employeeNumber": "3498691058",
132 | "employmentDate": "3/1/2023",
133 | "serviceHistory": 18
134 | },
135 | {
136 | "code": "20",
137 | "fullName": "Sigrid Branchett",
138 | "employeeNumber": "5288178895",
139 | "employmentDate": "7/3/2022",
140 | "serviceHistory": 13
141 | }
142 | ]
143 |
--------------------------------------------------------------------------------
/react_project/src/containers/table/table.jsx:
--------------------------------------------------------------------------------
1 | import {useMemo, useState} from "react";
2 | import DynamicTable from "../../components/dynamic-table/dynamic-table.jsx";
3 | import tableData from "./table-data.json";
4 |
5 | function Table() {
6 | const columns = useMemo(
7 | () => [
8 | {Header: "ردیف", accessor: "code"},
9 | {Header: "نام و نام خانوادگی", accessor: "fullName"},
10 | {Header: "شماره پرسنلی", accessor: "employeeNumber"},
11 | {Header: "تاریخ استخدام", accessor: "employmentDate"},
12 | {Header: "سابقه خدمت", accessor: "serviceHistory"},
13 | ],
14 | []
15 | );
16 |
17 | const [code, setCode] = useState(null);
18 | const data = useMemo(() => tableData, []);
19 |
20 | const handleRowClick = async (row) => {
21 | setCode(row.code.toString());
22 | };
23 |
24 | return (
25 | <>
26 |
27 |
28 |
29 | >
30 | );
31 | }
32 |
33 | export default Table;
34 |
--------------------------------------------------------------------------------
/react_project/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /* color system */
3 | --clr-bg: #fff;
4 | --clr-fg: #202020;
5 | --clr-white: #fff;
6 | --clr-black: #202020;
7 | --clr-primary: rgba(111, 192, 155, 1);
8 | --clr-primary-tint: rgba(111, 192, 155, 0.2);
9 | --clr-accent: rgba(213, 101, 56, 1);
10 | --clr-accent-tint: rgba(213, 101, 56, 0.2);
11 | --clr-primary-2: rgba(49, 47, 47, 1);
12 | --clr-primary-2-tint: rgba(49, 47, 47, 0.2);
13 | --clr-accent-2: rgba(237, 139, 32, 1);
14 | --clr-accent-2-tint: rgba(237, 139, 32, 0.2);
15 | --clr-gray-100: rgb(245, 245, 245);
16 | --clr-gray-200: rgb(238, 238, 238);
17 | --clr-gray-300: rgb(214, 214, 214);
18 | --clr-gray-400: rgb(189, 189, 189);
19 | --clr-gray-500: rgb(158, 158, 158);
20 | --clr-gray-600: rgb(117, 117, 117);
21 | --clr-gray-700: rgb(97, 97, 97);
22 | --clr-gray-800: rgb(75, 75, 75);
23 | --clr-blue-primary: rgb(89, 65, 205);
24 | --clr-blue-tint: rgb(213, 206, 242);
25 | --clr-error: #dc2626;
26 |
27 | /* font system */
28 | --fs-0: 1rem;
29 | --fs-00: 1.1rem;
30 | --fs-1: 1.2rem;
31 | --fs-2: 1.4rem;
32 | --fs-3: 1.6rem;
33 | --fs-4: 1.8rem;
34 | --fs-5: 2rem;
35 | --fs-6: 2.4rem;
36 | --fs-7: 3rem;
37 | --fs-8: 3.6rem;
38 | --fs-9: 4.8rem;
39 | --fs-10: 6rem;
40 | --fs-11: 7.2rem;
41 |
42 | /* spacing system */
43 | --spacing-1: 0.4rem;
44 | --spacing-2: 0.8rem;
45 | --spacing-3: 1.2rem;
46 | --spacing-4: 1.6rem;
47 | --spacing-5: 2.4rem;
48 | --spacing-6: 3.2rem;
49 | --spacing-7: 4.8rem;
50 | --spacing-8: 6.4rem;
51 | --spacing-9: 9.6rem;
52 | --spacing-10: 12.8rem;
53 | --spacing-11: 19.2rem;
54 | --spacing-12: 25.6rem;
55 | --spacing-13: 38.4rem;
56 | --spacing-14: 51.2rem;
57 | --spacing-15: 64rem;
58 | --spacing-16: 76.8rem;
59 | --card-padding-inline: var(--spacing-4);
60 |
61 | --br: 2px;
62 | --br-2: 4px;
63 |
64 | /* box-shadows */
65 | --box-shadow-light: 0 2px 6px rgba(0, 0, 0, 0.13);
66 | --box-shadow-dark: 0 2px 6px rgba(0, 0, 0, 0.28);
67 | }
68 |
69 | *,
70 | *::before,
71 | *::after {
72 | box-sizing: border-box;
73 | }
74 |
75 | * {
76 | margin: 0;
77 | padding: 0;
78 | }
79 |
80 | html,
81 | body {
82 | height: 100%;
83 | }
84 |
85 | body {
86 | line-height: 1.5;
87 | -webkit-font-smoothing: antialiased;
88 | }
89 |
90 | img,
91 | picture,
92 | video,
93 | canvas,
94 | svg {
95 | display: block;
96 | max-width: 100%;
97 | }
98 |
99 | input,
100 | button,
101 | textarea,
102 | select {
103 | font: inherit;
104 | }
105 |
106 | ul {
107 | list-style: none;
108 | }
109 |
110 | p,
111 | h1,
112 | h2,
113 | h3,
114 | h4,
115 | h5,
116 | h6 {
117 | overflow-wrap: break-word;
118 | }
119 |
120 | a {
121 | text-decoration: none;
122 | color: white;
123 | }
124 |
125 | #root,
126 | #__next {
127 | isolation: isolate;
128 | }
129 |
130 | html {
131 | overflow: auto;
132 | scrollbar-gutter: stable;
133 | }
134 |
135 | body {
136 | font-family: "Roboto", sans-serif;
137 | font-size: var(--fs-1);
138 | font-weight: 400;
139 | color: var(--clr-fg);
140 | background-color: #fff;
141 | }
142 |
143 | .container {
144 | padding: var(--spacing-5) var(--spacing-8);
145 | }
146 |
--------------------------------------------------------------------------------
/react_project/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import {RouterProvider} from "react-router-dom";
5 | import {router} from "./routes.jsx";
6 |
7 | ReactDOM.createRoot(document.getElementById("root")).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/react_project/src/routes.jsx:
--------------------------------------------------------------------------------
1 | import {createBrowserRouter} from "react-router-dom";
2 | import App from "./App.jsx";
3 | import ErrorPage from "./components/error-page/error-page.jsx";
4 | import Layout from "./containers/layout/layout.jsx";
5 | import Table from "./containers/table/table.jsx";
6 | import SearchBar from "./containers/search-bar/search-bar.jsx";
7 | import Home from "./containers/home/home.jsx";
8 |
9 | export const router = createBrowserRouter([
10 | {
11 | path: "/",
12 | element: ,
13 | errorElement: ,
14 | children: [
15 | {
16 | path: "/",
17 | element:
18 | },
19 | {
20 | path: "/layout",
21 | element: ,
22 | },
23 | {
24 | path: "/table",
25 | element: ,
26 | },
27 | {
28 | path: "/searchbar",
29 | element: ,
30 | },
31 | ]
32 | }
33 | ]);
34 |
--------------------------------------------------------------------------------
/react_project/src/services/api.js:
--------------------------------------------------------------------------------
1 | const BASE_API = "http://localhost:3000/"
2 |
3 | export async function searchCountries(query) {
4 | const response = await fetch(`${BASE_API}?q=${query}`);
5 | const data = await response.json();
6 | return data.data.results;
7 | }
8 |
9 | export async function fetchCurrencyData(countryCode) {
10 | const response = await fetch(`${BASE_API}chart/${countryCode}`);
11 | const data = await response.json();
12 | return data.data;
13 | }
14 |
--------------------------------------------------------------------------------
/react_project/src/tests/search-bar.test.js:
--------------------------------------------------------------------------------
1 | import {render, screen, fireEvent} from '@testing-library/react';
2 | import SearchBar from '../containers/search-bar/search-bar.jsx';
3 | import {QueryClient, QueryClientProvider} from "react-query";
4 |
5 | jest.mock('../utils/use-search', () => ({
6 | useSearch: () => ({
7 | searchResults: [
8 | {code: '1', name: 'Country 1'},
9 | ],
10 | isLoading: false,
11 | searchQuery: "country"
12 | }),
13 | }));
14 |
15 | describe('SearchBar', () => {
16 | test('displays search results in the table', async () => {
17 | const queryClient = new QueryClient();
18 | render(
19 |
20 | );
21 |
22 | const searchInput = screen.getByLabelText('Search countries...');
23 | fireEvent.change(searchInput, {target: {value: 'country'}});
24 |
25 | const cells = screen.getAllByRole('cell');
26 |
27 | expect(cells.length).toBe(2);
28 | expect(cells[0].textContent).toBe('1');
29 | expect(cells[1].textContent).toBe('Country 1');
30 | });
31 | });
--------------------------------------------------------------------------------
/react_project/src/utils/currency-data.js:
--------------------------------------------------------------------------------
1 | import {fetchCurrencyData} from "../services/api.js";
2 |
3 | export async function getCurrencyDataPoint(countryCode) {
4 | const {x, y} = await fetchCurrencyData(countryCode);
5 | return {x, y};
6 | }
--------------------------------------------------------------------------------
/react_project/src/utils/use-search.js:
--------------------------------------------------------------------------------
1 | import {useState} from 'react';
2 | import {useQuery} from 'react-query';
3 | import {useDebounce} from 'use-debounce';
4 | import {searchCountries} from '../services/api.js';
5 |
6 | export function useSearch() {
7 | const [searchQuery, setSearchQuery] = useState('');
8 | const [debouncedSearchQuery] = useDebounce(searchQuery, 500);
9 |
10 | let {data: searchResults, isLoading} = useQuery(
11 | ['searchResults', debouncedSearchQuery],
12 | async () => {
13 | if (debouncedSearchQuery && debouncedSearchQuery.trim() === '') {
14 | return [];
15 | }
16 | return searchCountries(debouncedSearchQuery);
17 | }
18 | );
19 |
20 | searchResults = searchResults?.map((result) => result.item);
21 | return {searchQuery, setSearchQuery, searchResults, isLoading};
22 | }
23 |
--------------------------------------------------------------------------------
/react_project/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | server: {
7 | watch: {
8 | usePolling: true,
9 | },
10 | host: true,
11 | strictPort: true,
12 | port: 5173,
13 | },
14 | });
15 |
--------------------------------------------------------------------------------